summaryrefslogtreecommitdiffstats
path: root/browser/components/firefoxview/tests
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/firefoxview/tests')
-rw-r--r--browser/components/firefoxview/tests/browser/browser.ini33
-rw-r--r--browser/components/firefoxview/tests/browser/browser_cfr_message.js67
-rw-r--r--browser/components/firefoxview/tests/browser/browser_colorways_card.js443
-rw-r--r--browser/components/firefoxview/tests/browser/browser_dragDrop_after_opening_fxViewTab.js120
-rw-r--r--browser/components/firefoxview/tests/browser/browser_entrypoint_management.js67
-rw-r--r--browser/components/firefoxview/tests/browser/browser_feature_callout.js655
-rw-r--r--browser/components/firefoxview/tests/browser/browser_feature_callout_position.js403
-rw-r--r--browser/components/firefoxview/tests/browser/browser_feature_callout_resize.js127
-rw-r--r--browser/components/firefoxview/tests/browser/browser_feature_callout_targeting.js175
-rw-r--r--browser/components/firefoxview/tests/browser/browser_firefoxview.js18
-rw-r--r--browser/components/firefoxview/tests/browser/browser_firefoxview_accessibility.js110
-rw-r--r--browser/components/firefoxview/tests/browser/browser_firefoxview_feature_callout_a11y.js55
-rw-r--r--browser/components/firefoxview/tests/browser/browser_firefoxview_tab.js249
-rw-r--r--browser/components/firefoxview/tests/browser/browser_keyboard_focus.js93
-rw-r--r--browser/components/firefoxview/tests/browser/browser_media_query_dom_sorting.js54
-rw-r--r--browser/components/firefoxview/tests/browser/browser_notification_dot.js335
-rw-r--r--browser/components/firefoxview/tests/browser/browser_recently_closed_tabs.js798
-rw-r--r--browser/components/firefoxview/tests/browser/browser_recently_closed_tabs_keyboard.js255
-rw-r--r--browser/components/firefoxview/tests/browser/browser_reload_firefoxview.js36
-rw-r--r--browser/components/firefoxview/tests/browser/browser_setup_errors.js374
-rw-r--r--browser/components/firefoxview/tests/browser/browser_setup_primary_password.js150
-rw-r--r--browser/components/firefoxview/tests/browser/browser_setup_state.js769
-rw-r--r--browser/components/firefoxview/tests/browser/browser_setup_synced_tabs_loading.js180
-rw-r--r--browser/components/firefoxview/tests/browser/browser_sync_admin_disabled.js72
-rw-r--r--browser/components/firefoxview/tests/browser/browser_tab_close_last_tab.js43
-rw-r--r--browser/components/firefoxview/tests/browser/browser_tab_on_close_warning.js63
-rw-r--r--browser/components/firefoxview/tests/browser/browser_tab_pickup_list.js607
-rw-r--r--browser/components/firefoxview/tests/browser/browser_ui_state.js145
-rw-r--r--browser/components/firefoxview/tests/browser/head.js599
29 files changed, 7095 insertions, 0 deletions
diff --git a/browser/components/firefoxview/tests/browser/browser.ini b/browser/components/firefoxview/tests/browser/browser.ini
new file mode 100644
index 0000000000..3e0507b747
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser.ini
@@ -0,0 +1,33 @@
+[DEFAULT]
+support-files = head.js
+prefs =
+ browser.tabs.firefox-view.logLevel=All
+
+[browser_dragDrop_after_opening_fxViewTab.js]
+[browser_entrypoint_management.js]
+[browser_firefoxview.js]
+[browser_firefoxview_accessibility.js]
+[browser_firefoxview_feature_callout_a11y.js]
+[browser_firefoxview_tab.js]
+[browser_keyboard_focus.js]
+[browser_media_query_dom_sorting.js]
+[browser_notification_dot.js]
+[browser_recently_closed_tabs.js]
+[browser_recently_closed_tabs_keyboard.js]
+[browser_reload_firefoxview.js]
+[browser_setup_errors.js]
+[browser_setup_primary_password.js]
+[browser_setup_state.js]
+[browser_setup_synced_tabs_loading.js]
+[browser_sync_admin_disabled.js]
+[browser_tab_pickup_list.js]
+[browser_colorways_card.js]
+[browser_cfr_message.js]
+skip-if = true # Bug 1783684
+[browser_feature_callout.js]
+[browser_feature_callout_position.js]
+[browser_feature_callout_resize.js]
+[browser_feature_callout_targeting.js]
+[browser_tab_close_last_tab.js]
+[browser_tab_on_close_warning.js]
+[browser_ui_state.js]
diff --git a/browser/components/firefoxview/tests/browser/browser_cfr_message.js b/browser/components/firefoxview/tests/browser/browser_cfr_message.js
new file mode 100644
index 0000000000..ee9df2f105
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_cfr_message.js
@@ -0,0 +1,67 @@
+const { ASRouter } = ChromeUtils.import(
+ "resource://activity-stream/lib/ASRouter.jsm"
+);
+const { ASRouterTriggerListeners } = ChromeUtils.import(
+ "resource://activity-stream/lib/ASRouterTriggerListeners.jsm"
+);
+
+const { SpecialMessageActions } = ChromeUtils.import(
+ "resource://messaging-system/lib/SpecialMessageActions.jsm"
+);
+
+add_task(async function cfr_firefoxview_should_show() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.firefox-view.view-count", 0]],
+ });
+
+ let cfrSpy = sinon.spy(ASRouter, "routeCFRMessage");
+ let specialMessageActionsSpy = sinon.spy(
+ SpecialMessageActions,
+ "handleAction"
+ );
+ registerCleanupFunction(() => {
+ cfrSpy.restore();
+ specialMessageActionsSpy.restore();
+ ASRouter.resetMessageState();
+ ASRouter.unblockMessageById("CFR_FIREFOX_VIEW");
+ ASRouterTriggerListeners.get("nthTabClosed").uninit();
+ });
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ let tab3 = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ const showPanel = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown",
+ target => {
+ return target;
+ }
+ );
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+
+ await showPanel;
+
+ Assert.equal(cfrSpy.lastCall.args[0].id, "CFR_FIREFOX_VIEW");
+
+ const notification = document.querySelector(
+ "#contextual-feature-recommendation-notification"
+ );
+
+ Assert.ok(notification);
+ Assert.ok(document.querySelector(".popup-notification-primary-button"));
+
+ Assert.ok(document.querySelector(".popup-notification-secondary-button"));
+
+ await notification.button.click();
+
+ Assert.equal(
+ specialMessageActionsSpy.firstCall.args[0].type,
+ "OPEN_FIREFOX_VIEW"
+ );
+ await SpecialPowers.popPrefEnv();
+
+ closeFirefoxViewTab(window);
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_colorways_card.js b/browser/components/firefoxview/tests/browser/browser_colorways_card.js
new file mode 100644
index 0000000000..48723905ff
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_colorways_card.js
@@ -0,0 +1,443 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { BuiltInThemes } = ChromeUtils.importESModule(
+ "resource:///modules/BuiltInThemes.sys.mjs"
+);
+const { AddonManager } = ChromeUtils.import(
+ "resource://gre/modules/AddonManager.jsm"
+);
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+
+const TEST_COLLECTION_FIGURE_URL = "https://www.example.com/collection.avif";
+const TEST_COLORWAY_FIGURE_URL = "https://www.example.com/colorway.avif";
+
+const TEST_COLORWAY_COLLECTION = {
+ id: "independent-voices",
+ expiry: new Date("3000-01-01"),
+ l10nId: {
+ title: "colorway-collection-independent-voices",
+ description: "colorway-collection-independent-voices-description",
+ },
+ figureUrl: TEST_COLLECTION_FIGURE_URL,
+};
+
+const SOFT_COLORWAY_THEME_ID = "mocktheme-soft-colorway@mozilla.org";
+const BALANCED_COLORWAY_THEME_ID = "mocktheme-balanced-colorway@mozilla.org";
+const BOLD_COLORWAY_THEME_ID = "mocktheme-bold-colorway@mozilla.org";
+const NO_INTENSITY_COLORWAY_THEME_ID = "mocktheme-colorway@mozilla.org";
+const OUTDATED_COLORWAY_THEME_ID = "outdatedtheme-colorway@mozilla.org";
+
+const EXPIRY_DATE_L10N_ID = "colorway-collection-expiry-label";
+const COLORWAY_DESCRIPTION_L10N_ID = "firefoxview-colorway-description";
+const MOCK_THEME_L10N_VALUE = "Mock Theme";
+const SOFT_L10N_VALUE = "Soft";
+
+const TRY_COLORWAYS_EVENT = [
+ ["colorways_modal", "try_colorways", "firefoxview", undefined],
+];
+
+const CHANGE_COLORWAY_EVENT = [
+ ["colorways_modal", "change_colorway", "firefoxview", undefined],
+];
+
+function getTestElements(document) {
+ return {
+ container: document.getElementById("colorways"),
+ title: document.getElementById("colorways-collection-title"),
+ description: document.getElementById("colorways-collection-description"),
+ expiryPill: document.querySelector("#colorways-collection-expiry-date"),
+ expiry: document.querySelector("#colorways-collection-expiry-date > span"),
+ figure: document.getElementById("colorways-collection-figure"),
+ };
+}
+
+async function createTempTheme(id) {
+ const xpi = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ name: "Monochromatic Theme",
+ browser_specific_settings: { gecko: { id } },
+ theme: {},
+ },
+ });
+ return AddonTestUtils.promiseInstallFile(xpi);
+}
+
+let gCollectionEnabled = true;
+
+// TODO: use Colorway Closet mocks and helper functions (Bug 1783675)
+
+add_setup(async function setup_tests() {
+ const sandbox = sinon.createSandbox();
+ sandbox
+ .stub(BuiltInThemes, "findActiveColorwayCollection")
+ .callsFake(() => (gCollectionEnabled ? TEST_COLORWAY_COLLECTION : null));
+ sandbox
+ .stub(BuiltInThemes, "isColorwayFromCurrentCollection")
+ .callsFake(
+ id =>
+ id === SOFT_COLORWAY_THEME_ID ||
+ id === BALANCED_COLORWAY_THEME_ID ||
+ id === BOLD_COLORWAY_THEME_ID ||
+ id === NO_INTENSITY_COLORWAY_THEME_ID
+ );
+ sandbox
+ .stub(BuiltInThemes, "getLocalizedColorwayGroupName")
+ .returns(MOCK_THEME_L10N_VALUE);
+ sandbox.stub(BuiltInThemes.builtInThemeMap, "get").returns({
+ figureUrl: TEST_COLORWAY_FIGURE_URL,
+ });
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.theme.colorway-closet", true]],
+ });
+ const tempThemes = await Promise.all(
+ [
+ SOFT_COLORWAY_THEME_ID,
+ BALANCED_COLORWAY_THEME_ID,
+ BOLD_COLORWAY_THEME_ID,
+ NO_INTENSITY_COLORWAY_THEME_ID,
+ OUTDATED_COLORWAY_THEME_ID,
+ ].map(createTempTheme)
+ );
+ registerCleanupFunction(async () => {
+ sandbox.restore();
+ await SpecialPowers.popPrefEnv();
+ for (const { addon } of tempThemes) {
+ await addon.disable();
+ await addon.uninstall(true);
+ }
+ });
+});
+
+add_task(async function no_collection_test() {
+ gCollectionEnabled = false;
+ try {
+ await withFirefoxView({ win: window }, async browser => {
+ const { document } = browser.contentWindow;
+ const { container } = getTestElements(document);
+ ok(!BrowserTestUtils.is_visible(container), "Colorways card is hidden");
+ });
+ } finally {
+ gCollectionEnabled = true;
+ }
+});
+
+add_task(async function colorway_closet_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.theme.colorway-closet", false]],
+ });
+ try {
+ await withFirefoxView({ win: window }, async browser => {
+ const { document } = browser.contentWindow;
+ const { container } = getTestElements(document);
+ ok(
+ !BrowserTestUtils.is_visible(container),
+ "Colorways card is hidden when Colorway Closet is disabled"
+ );
+ });
+ } finally {
+ await SpecialPowers.popPrefEnv();
+ }
+});
+
+add_task(async function no_active_colorway_test() {
+ // Set to default theme to unapply any enabled colorways
+ const theme = await AddonManager.getAddonByID("default-theme@mozilla.org");
+ await theme.enable();
+ try {
+ await clearAllParentTelemetryEvents();
+ await withFirefoxView({ win: window }, async browser => {
+ const { document } = browser.contentWindow;
+ const el = getTestElements(document);
+ ok(
+ BrowserTestUtils.is_visible(el.description),
+ "Colorway description should be visible"
+ );
+ is(
+ el.figure.src,
+ TEST_COLLECTION_FIGURE_URL,
+ "Collection figure should be shown"
+ );
+ is(
+ document.l10n.getAttributes(el.title).id,
+ TEST_COLORWAY_COLLECTION.l10nId.title,
+ "Collection title should be shown"
+ );
+ is(
+ document.l10n.getAttributes(el.description).id,
+ TEST_COLORWAY_COLLECTION.l10nId.description,
+ "Collection description should be shown"
+ );
+ ok(!el.expiryPill.hidden, "Expiry pill is shown");
+ const expiryL10nAttributes = document.l10n.getAttributes(el.expiry);
+ is(
+ expiryL10nAttributes.args.expiryDate,
+ TEST_COLORWAY_COLLECTION.expiry.getTime(),
+ "Correct expiry date should be shown"
+ );
+ is(
+ expiryL10nAttributes.id,
+ EXPIRY_DATE_L10N_ID,
+ "Correct expiry date format should be shown"
+ );
+
+ document.querySelector("#colorways-button").click();
+
+ await TestUtils.waitForCondition(
+ () => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ ).parent;
+ let colorwayEvents = events.filter(e => e[1] === "colorways_modal");
+ return colorwayEvents && colorwayEvents.length;
+ },
+ "Waiting for try_colorways colorways telemetry event.",
+ 200,
+ 100
+ );
+
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ ).parent;
+ let colorwayEvents = events.filter(e => e[1] === "colorways_modal");
+
+ info(JSON.stringify(colorwayEvents));
+
+ TelemetryTestUtils.assertEvents(
+ TRY_COLORWAYS_EVENT,
+ { category: "colorways_modal" },
+ { clear: true, process: "parent" }
+ );
+ });
+ } finally {
+ await theme.disable();
+ }
+});
+
+add_task(async function active_colorway_test() {
+ const theme = await AddonManager.getAddonByID(SOFT_COLORWAY_THEME_ID);
+ await theme.enable();
+ try {
+ await clearAllParentTelemetryEvents();
+ await withFirefoxView({ win: window }, async browser => {
+ const { document } = browser.contentWindow;
+ const el = getTestElements(document);
+ ok(
+ BrowserTestUtils.is_visible(el.description),
+ "Colorway description should be visible"
+ );
+ is(
+ el.figure.src,
+ TEST_COLORWAY_FIGURE_URL,
+ "Colorway figure should be shown"
+ );
+ is(
+ el.title.textContent,
+ MOCK_THEME_L10N_VALUE,
+ "Colorway title should be shown"
+ );
+ const descriptionL10nAttributes = document.l10n.getAttributes(
+ el.description
+ );
+ is(
+ descriptionL10nAttributes.id,
+ COLORWAY_DESCRIPTION_L10N_ID,
+ "Colorway description should be shown"
+ );
+ is(
+ descriptionL10nAttributes.args.intensity,
+ SOFT_L10N_VALUE,
+ "Colorway intensity should be shown"
+ );
+ is(
+ descriptionL10nAttributes.args.collection,
+ "Independent Voices",
+ "Collection name should be shown"
+ );
+ ok(el.expiryPill.hidden, "Expiry pill is hidden");
+
+ document.querySelector("#colorways-button").click();
+
+ await TestUtils.waitForCondition(
+ () => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ ).parent;
+ let colorwayEvents = events.filter(e => e[1] === "colorways_modal");
+ return colorwayEvents && colorwayEvents.length;
+ },
+ "Waiting for change_colorway colorways telemetry event.",
+ 200,
+ 100
+ );
+
+ TelemetryTestUtils.assertEvents(
+ CHANGE_COLORWAY_EVENT,
+ { category: "colorways_modal" },
+ { clear: true, process: "parent" }
+ );
+ });
+ } finally {
+ await theme.disable();
+ }
+});
+
+add_task(async function active_colorway_without_intensity_test() {
+ const theme = await AddonManager.getAddonByID(NO_INTENSITY_COLORWAY_THEME_ID);
+ await theme.enable();
+ try {
+ await withFirefoxView({ win: window }, async browser => {
+ const { document } = browser.contentWindow;
+ const el = getTestElements(document);
+ ok(
+ BrowserTestUtils.is_visible(el.description),
+ "Colorway description should be visible"
+ );
+ is(
+ el.figure.src,
+ TEST_COLORWAY_FIGURE_URL,
+ "Colorway figure should be shown"
+ );
+ is(
+ el.title.textContent,
+ MOCK_THEME_L10N_VALUE,
+ "Colorway title should be shown"
+ );
+ is(
+ document.l10n.getAttributes(el.description).id,
+ TEST_COLORWAY_COLLECTION.l10nId.title,
+ "Collection name should be shown as the description"
+ );
+ ok(el.expiryPill.hidden, "Expiry pill is hidden");
+ });
+ } finally {
+ await theme.disable();
+ }
+});
+
+add_task(async function active_colorway_is_outdated_test() {
+ const theme = await AddonManager.getAddonByID(OUTDATED_COLORWAY_THEME_ID);
+ await theme.enable();
+ try {
+ await withFirefoxView({ win: window }, async browser => {
+ const { document } = browser.contentWindow;
+ const el = getTestElements(document);
+ ok(
+ BrowserTestUtils.is_visible(el.description),
+ "Description should be visible"
+ );
+ is(
+ el.figure.src,
+ TEST_COLLECTION_FIGURE_URL,
+ "Collection figure should be shown"
+ );
+ is(
+ document.l10n.getAttributes(el.title).id,
+ TEST_COLORWAY_COLLECTION.l10nId.title,
+ "Collection title should be shown"
+ );
+ is(
+ document.l10n.getAttributes(el.description).id,
+ TEST_COLORWAY_COLLECTION.l10nId.description,
+ "Collection description should be shown"
+ );
+ ok(!el.expiryPill.hidden, "Expiry pill is shown");
+ const expiryL10nAttributes = document.l10n.getAttributes(el.expiry);
+ is(
+ expiryL10nAttributes.args.expiryDate,
+ TEST_COLORWAY_COLLECTION.expiry.getTime(),
+ "Correct expiry date should be shown"
+ );
+ is(
+ expiryL10nAttributes.id,
+ EXPIRY_DATE_L10N_ID,
+ "Correct expiry date format should be shown"
+ );
+ });
+ } finally {
+ await theme.disable();
+ }
+});
+
+add_task(async function change_active_colorway_test() {
+ let theme = await AddonManager.getAddonByID(NO_INTENSITY_COLORWAY_THEME_ID);
+ await theme.enable();
+ try {
+ await withFirefoxView({ win: window }, async browser => {
+ info("Start with no intensity theme");
+ const { document } = browser.contentWindow;
+ let el = getTestElements(document);
+ ok(
+ BrowserTestUtils.is_visible(el.description),
+ "Colorway description should be visible"
+ );
+ is(
+ el.figure.src,
+ TEST_COLORWAY_FIGURE_URL,
+ "Colorway figure should be shown"
+ );
+ is(
+ el.title.textContent,
+ MOCK_THEME_L10N_VALUE,
+ "Colorway title should be shown"
+ );
+ info("Revert to default theme");
+ await theme.disable();
+ el = getTestElements(document);
+ ok(
+ BrowserTestUtils.is_visible(el.description),
+ "Colorway description should be visible"
+ );
+ is(
+ el.figure.src,
+ TEST_COLLECTION_FIGURE_URL,
+ "Collection figure should be shown"
+ );
+ is(
+ document.l10n.getAttributes(el.title).id,
+ TEST_COLORWAY_COLLECTION.l10nId.title,
+ "Collection title should be shown"
+ );
+ is(
+ document.l10n.getAttributes(el.description).id,
+ TEST_COLORWAY_COLLECTION.l10nId.description,
+ "Collection description should be shown"
+ );
+ info("Enable a different theme");
+ theme = await AddonManager.getAddonByID(SOFT_COLORWAY_THEME_ID);
+ await theme.enable();
+ is(
+ el.title.textContent,
+ MOCK_THEME_L10N_VALUE,
+ "Colorway title should be shown"
+ );
+ const descriptionL10nAttributes = document.l10n.getAttributes(
+ el.description
+ );
+ is(
+ descriptionL10nAttributes.id,
+ COLORWAY_DESCRIPTION_L10N_ID,
+ "Colorway description should be shown"
+ );
+ is(
+ descriptionL10nAttributes.args.intensity,
+ SOFT_L10N_VALUE,
+ "Colorway intensity should be shown"
+ );
+ is(
+ descriptionL10nAttributes.args.collection,
+ "Independent Voices",
+ "Collection name should be shown"
+ );
+ });
+ } finally {
+ await theme.disable();
+ }
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_dragDrop_after_opening_fxViewTab.js b/browser/components/firefoxview/tests/browser/browser_dragDrop_after_opening_fxViewTab.js
new file mode 100644
index 0000000000..e2255fabbf
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_dragDrop_after_opening_fxViewTab.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that dragging and dropping tabs into tabbrowser works as intended
+ * after opening the Firefox View tab for RTL builds. There was an issue where
+ * tabs from dragged links were not dropped in the correct tab indexes
+ * for RTL builds because logic for RTL builds did not take into consideration
+ * hidden tabs like the Firefox View tab. This test makes sure that this behavior does not reoccur.
+ */
+add_task(async function() {
+ info("Setting browser to RTL locale");
+ await SpecialPowers.pushPrefEnv({ set: [["intl.l10n.pseudo", "bidi"]] });
+
+ // window.RTL_UI doesn't update in existing windows when this pref is changed,
+ // so we need to test in a new window.
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ );
+ let newTab = win.gBrowser.tabs[0];
+
+ let waitForTestTabPromise = BrowserTestUtils.waitForNewTab(
+ win.gBrowser,
+ TEST_ROOT + "file_new_tab_page.html"
+ );
+ let testTab = await BrowserTestUtils.openNewForegroundTab(
+ win.gBrowser,
+ TEST_ROOT + "file_new_tab_page.html"
+ );
+ await waitForTestTabPromise;
+
+ let linkSrcEl = win.document.querySelector("a");
+ ok(linkSrcEl, "Link exists");
+
+ let dropPromise = BrowserTestUtils.waitForEvent(
+ win.gBrowser.tabContainer,
+ "drop"
+ );
+
+ /**
+ * There should be 2 tabs:
+ * 1. new tab (auto-generated)
+ * 2. test tab
+ */
+ is(win.gBrowser.visibleTabs.length, 2, "There should be 2 tabs");
+
+ // Now open Firefox View tab
+ info("Opening Firefox View tab");
+ await openFirefoxViewTab(win);
+
+ /**
+ * There should be 2 visible tabs:
+ * 1. new tab (auto-generated)
+ * 2. test tab
+ * Firefox View tab is hidden.
+ */
+ is(
+ win.gBrowser.visibleTabs.length,
+ 2,
+ "There should still be 2 visible tabs after opening Firefox View tab"
+ );
+
+ info("Switching to test tab");
+ await BrowserTestUtils.switchTab(win.gBrowser, testTab);
+
+ let waitForDraggedTabPromise = BrowserTestUtils.waitForNewTab(
+ win.gBrowser,
+ "https://example.com/#test"
+ );
+
+ info("Dragging link between test tab and new tab");
+ EventUtils.synthesizeDrop(
+ linkSrcEl,
+ win.gBrowser.tabContainer,
+ [[{ type: "text/plain", data: "https://example.com/#test" }]],
+ "link",
+ win,
+ win,
+ {
+ clientX: testTab.getBoundingClientRect().right,
+ }
+ );
+
+ info("Waiting for drop event");
+ await dropPromise;
+ info("Waiting for dragged tab to be created");
+ let draggedTab = await waitForDraggedTabPromise;
+
+ /**
+ * There should be 3 visible tabs:
+ * 1. new tab (auto-generated)
+ * 2. new tab from dragged link
+ * 3. test tab
+ *
+ * In RTL build, it should appear in the following order:
+ * <test tab> <link dragged tab> <new tab> | <FxView tab>
+ */
+ is(win.gBrowser.visibleTabs.length, 3, "There should be 3 tabs");
+ is(
+ win.gBrowser.visibleTabs.indexOf(newTab),
+ 0,
+ "New tab should still be rightmost visible tab"
+ );
+ is(
+ win.gBrowser.visibleTabs.indexOf(draggedTab),
+ 1,
+ "Dragged link should positioned at new index"
+ );
+ is(
+ win.gBrowser.visibleTabs.indexOf(testTab),
+ 2,
+ "Test tab should be to the left of dragged tab"
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_entrypoint_management.js b/browser/components/firefoxview/tests/browser/browser_entrypoint_management.js
new file mode 100644
index 0000000000..ef6b0c99f5
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_entrypoint_management.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_removing_button_should_close_tab() {
+ await withFirefoxView({}, async browser => {
+ let win = browser.ownerGlobal;
+ let tab = browser.getTabBrowser().getTabForBrowser(browser);
+ let button = win.document.getElementById("firefox-view-button");
+ await win.gCustomizeMode.removeFromArea(button, "toolbar-context-menu");
+ ok(!tab.isConnected, "Tab should have been removed.");
+ isnot(win.gBrowser.selectedTab, tab, "A different tab should be selected.");
+ });
+ CustomizableUI.reset();
+});
+
+add_task(async function test_button_auto_readd() {
+ await withFirefoxView({}, async browser => {
+ let { FirefoxViewHandler } = browser.ownerGlobal;
+
+ CustomizableUI.removeWidgetFromArea("firefox-view-button");
+ ok(
+ !CustomizableUI.getPlacementOfWidget("firefox-view-button"),
+ "Button has no placement"
+ );
+ ok(!FirefoxViewHandler.tab, "Shouldn't have tab reference");
+ ok(!FirefoxViewHandler.button, "Shouldn't have button reference");
+
+ FirefoxViewHandler.openTab();
+ ok(FirefoxViewHandler.tab, "Tab re-opened");
+ ok(FirefoxViewHandler.button, "Button re-added");
+ let placement = CustomizableUI.getPlacementOfWidget("firefox-view-button");
+ is(
+ placement.area,
+ CustomizableUI.AREA_TABSTRIP,
+ "Button re-added to the tabs toolbar"
+ );
+ is(placement.position, 0, "Button re-added as the first toolbar element");
+ });
+ CustomizableUI.reset();
+});
+
+add_task(async function test_button_moved() {
+ await withFirefoxView({}, async browser => {
+ let { FirefoxViewHandler } = browser.ownerGlobal;
+ CustomizableUI.addWidgetToArea(
+ "firefox-view-button",
+ CustomizableUI.AREA_NAVBAR,
+ 0
+ );
+ is(
+ FirefoxViewHandler.button.closest("toolbar").id,
+ "nav-bar",
+ "Button is in the navigation toolbar"
+ );
+ });
+ await withFirefoxView({}, async browser => {
+ let { FirefoxViewHandler } = browser.ownerGlobal;
+ is(
+ FirefoxViewHandler.button.closest("toolbar").id,
+ "nav-bar",
+ "Button remains in the navigation toolbar"
+ );
+ });
+ CustomizableUI.reset();
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_feature_callout.js b/browser/components/firefoxview/tests/browser/browser_feature_callout.js
new file mode 100644
index 0000000000..e90b7ab7ac
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_feature_callout.js
@@ -0,0 +1,655 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+const { MessageLoaderUtils } = ChromeUtils.import(
+ "resource://activity-stream/lib/ASRouter.jsm"
+);
+
+const { BuiltInThemes } = ChromeUtils.importESModule(
+ "resource:///modules/BuiltInThemes.sys.mjs"
+);
+
+const featureTourPref = "browser.firefox-view.feature-tour";
+const defaultPrefValue = getPrefValueByScreen(1);
+
+add_setup(async function() {
+ requestLongerTimeout(2);
+ registerCleanupFunction(() => ASRouter.resetMessageState());
+});
+
+add_task(async function feature_callout_renders_in_firefox_view() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[featureTourPref, defaultPrefValue]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+ await waitForCalloutScreen(document, "FEATURE_CALLOUT_1");
+
+ ok(
+ document.querySelector(calloutSelector),
+ "Feature Callout element exists"
+ );
+ }
+ );
+});
+
+add_task(async function feature_callout_is_not_shown_twice() {
+ // Third comma-separated value of the pref is set to a string value once a user completes the tour
+ await SpecialPowers.pushPrefEnv({
+ set: [[featureTourPref, '{"message":"","screen":"","complete":true}']],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+
+ ok(
+ !document.querySelector(calloutSelector),
+ "Feature Callout tour does not render if the user finished it previously"
+ );
+ }
+ );
+ Services.prefs.clearUserPref(featureTourPref);
+});
+
+add_task(async function feature_callout_syncs_across_visits_and_tabs() {
+ // Second comma-separated value of the pref is the id
+ // of the last viewed screen of the feature tour
+ await SpecialPowers.pushPrefEnv({
+ set: [[featureTourPref, '{"screen":"FEATURE_CALLOUT_1","complete":false}']],
+ });
+ // Open an about:firefoxview tab
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:firefoxview"
+ );
+ let tab1Doc = tab1.linkedBrowser.contentWindow.document;
+ await waitForCalloutScreen(tab1Doc, "FEATURE_CALLOUT_1");
+
+ ok(
+ tab1Doc.querySelector(".FEATURE_CALLOUT_1"),
+ "First tab's Feature Callout shows the tour screen saved in the user pref"
+ );
+
+ // Open a second about:firefoxview tab
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:firefoxview"
+ );
+ let tab2Doc = tab2.linkedBrowser.contentWindow.document;
+ await waitForCalloutScreen(tab2Doc, "FEATURE_CALLOUT_1");
+
+ ok(
+ tab2Doc.querySelector(".FEATURE_CALLOUT_1"),
+ "Second tab's Feature Callout shows the tour screen saved in the user pref"
+ );
+
+ await clickPrimaryButton(tab2Doc);
+
+ gBrowser.selectedTab = tab1;
+ tab1.focus();
+ await waitForCalloutScreen(tab1Doc, "FEATURE_CALLOUT_2");
+ ok(
+ tab1Doc.querySelector(".FEATURE_CALLOUT_2"),
+ "First tab's Feature Callout advances to the next screen when the tour is advanced in second tab"
+ );
+
+ await clickPrimaryButton(tab1Doc);
+ gBrowser.selectedTab = tab1;
+ await waitForCalloutRemoved(tab1Doc);
+
+ ok(
+ !tab1Doc.body.querySelector(calloutSelector),
+ "Feature Callout is removed in first tab after being dismissed in first tab"
+ );
+
+ gBrowser.selectedTab = tab2;
+ tab2.focus();
+ await waitForCalloutRemoved(tab2Doc);
+
+ ok(
+ !tab2Doc.body.querySelector(calloutSelector),
+ "Feature Callout is removed in second tab after tour was dismissed in first tab"
+ );
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ Services.prefs.clearUserPref(featureTourPref);
+});
+
+add_task(async function feature_callout_closes_on_dismiss() {
+ const testMessage = getCalloutMessageById(
+ "FIREFOX_VIEW_FEATURE_TOUR_2_NO_CWS"
+ );
+ const sandbox = createSandboxWithCalloutTriggerStub(testMessage);
+ const spy = new TelemetrySpy(sandbox);
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+
+ await waitForCalloutScreen(document, "FEATURE_CALLOUT_2");
+
+ document.querySelector(".dismiss-button").click();
+ await waitForCalloutRemoved(document);
+
+ ok(
+ !document.querySelector(calloutSelector),
+ "Callout is removed from screen on dismiss"
+ );
+
+ let tourComplete = JSON.parse(
+ Services.prefs.getStringPref(featureTourPref)
+ ).complete;
+ ok(
+ tourComplete,
+ `Tour is recorded as complete in ${featureTourPref} preference value`
+ );
+
+ // Test that appropriate telemetry is sent
+ spy.assertCalledWith({
+ event: "CLICK_BUTTON",
+ event_context: {
+ source: "dismiss_button",
+ page: document.location.href,
+ },
+ message_id: sinon.match("FEATURE_CALLOUT_2"),
+ });
+ spy.assertCalledWith({
+ event: "DISMISS",
+ event_context: {
+ source: "dismiss_button",
+ page: document.location.href,
+ },
+ message_id: sinon.match("FEATURE_CALLOUT_2"),
+ });
+ }
+ );
+ sandbox.restore();
+});
+
+add_task(async function feature_callout_only_highlights_existing_elements() {
+ const testMessage = getCalloutMessageById(
+ "FIREFOX_VIEW_FEATURE_TOUR_1_NO_CWS"
+ );
+ testMessage.message.content.screens[0].parent_selector = "#fake-selector";
+ const sandbox = createSandboxWithCalloutTriggerStub(testMessage);
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+ ok(
+ !document.querySelector(`${calloutSelector}:not(.hidden)`),
+ "Feature Callout screen does not render if its parent element does not exist"
+ );
+ }
+ );
+ sandbox.restore();
+});
+
+add_task(async function feature_callout_arrow_class_exists() {
+ const testMessage = getCalloutMessageById(
+ "FIREFOX_VIEW_FEATURE_TOUR_1_NO_CWS"
+ );
+ const sandbox = createSandboxWithCalloutTriggerStub(testMessage);
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+ await waitForCalloutScreen(document, "FEATURE_CALLOUT_1");
+
+ const arrowParent = document.querySelector(".callout-arrow.arrow-top");
+ ok(arrowParent, "Arrow class exists on parent container");
+ }
+ );
+ sandbox.restore();
+});
+
+add_task(async function feature_callout_arrow_is_not_flipped_on_ltr() {
+ const testMessage = getCalloutMessageById(
+ "FIREFOX_VIEW_FEATURE_TOUR_1_NO_CWS"
+ );
+ testMessage.message.content.screens[0].content.arrow_position = "start";
+ testMessage.message.content.screens[0].parent_selector = "span.brand-icon";
+ const sandbox = createSandboxWithCalloutTriggerStub(testMessage);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+ await BrowserTestUtils.waitForCondition(() => {
+ return document.querySelector(
+ `${calloutSelector}.arrow-inline-start:not(.hidden)`
+ );
+ });
+ ok(
+ true,
+ "Feature Callout arrow parent has arrow-start class when arrow direction is set to 'start'"
+ );
+ }
+ );
+ sandbox.restore();
+});
+
+add_task(async function feature_callout_respects_cfr_features_pref() {
+ async function toggleCFRFeaturesPref(value, extraPrefs = []) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features",
+ value,
+ ],
+ ...extraPrefs,
+ ],
+ });
+ }
+
+ await toggleCFRFeaturesPref(true, [[featureTourPref, defaultPrefValue]]);
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+
+ await waitForCalloutScreen(document, "FEATURE_CALLOUT_1");
+ ok(
+ document.querySelector(calloutSelector),
+ "Feature Callout element exists"
+ );
+
+ await toggleCFRFeaturesPref(false);
+ await waitForCalloutRemoved(document);
+ ok(
+ !document.querySelector(calloutSelector),
+ "Feature Callout element was removed because CFR pref was disabled"
+ );
+ }
+ );
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+
+ ok(
+ !document.querySelector(calloutSelector),
+ "Feature Callout element was not created because CFR pref was disabled"
+ );
+
+ await toggleCFRFeaturesPref(true);
+ await waitForCalloutScreen(document, "FEATURE_CALLOUT_1");
+ ok(
+ document.querySelector(calloutSelector),
+ "Feature Callout element was created because CFR pref was enabled"
+ );
+ }
+ );
+});
+
+add_task(
+ async function feature_callout_tab_pickup_reminder_primary_click_elm() {
+ Services.prefs.setBoolPref("identity.fxaccounts.enabled", false);
+ const testMessage = getCalloutMessageById(
+ "FIREFOX_VIEW_TAB_PICKUP_REMINDER"
+ );
+ const sandbox = createSandboxWithCalloutTriggerStub(testMessage);
+
+ const expectedUrl = await fxAccounts.constructor.config.promiseConnectAccountURI(
+ "fx-view"
+ );
+ info(`Expected FxA URL: ${expectedUrl}`);
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+ let tabOpened = new Promise(resolve => {
+ gBrowser.tabContainer.addEventListener(
+ "TabOpen",
+ event => {
+ let newTab = event.target;
+ let newBrowser = newTab.linkedBrowser;
+ let result = newTab;
+ BrowserTestUtils.waitForDocLoadAndStopIt(
+ expectedUrl,
+ newBrowser
+ ).then(() => resolve(result));
+ },
+ { once: true }
+ );
+ });
+
+ info("Waiting for callout to render");
+ await waitForCalloutScreen(
+ document,
+ "FIREFOX_VIEW_TAB_PICKUP_REMINDER"
+ );
+
+ info("Clicking primary button");
+ let calloutRemoved = waitForCalloutRemoved(document);
+ await clickPrimaryButton(document);
+ let openedTab = await tabOpened;
+ ok(openedTab, "FxA sign in page opened");
+ // The callout should be removed when primary CTA is clicked
+ await calloutRemoved;
+ BrowserTestUtils.removeTab(openedTab);
+ }
+ );
+ Services.prefs.clearUserPref("identity.fxaccounts.enabled");
+ sandbox.restore();
+ }
+);
+
+add_task(async function feature_callout_dismiss_on_page_click() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[featureTourPref, `{"message":"","screen":"","complete":true}`]],
+ });
+ const screenId = "FIREFOX_VIEW_TAB_PICKUP_REMINDER";
+ const testClickSelector = "#recently-closed-tabs-container";
+ let testMessage = getCalloutMessageById(screenId);
+ // Configure message with a dismiss action on tab container click
+ testMessage.message.content.screens[0].content.page_event_listeners = [
+ {
+ params: {
+ type: "click",
+ selectors: testClickSelector,
+ },
+ action: {
+ dismiss: true,
+ },
+ },
+ ];
+ const sandbox = createSandboxWithCalloutTriggerStub(testMessage);
+ const spy = new TelemetrySpy(sandbox);
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+
+ info("Waiting for callout to render");
+ await waitForCalloutScreen(document, screenId);
+
+ info("Clicking page element");
+ document.querySelector(testClickSelector).click();
+ await waitForCalloutRemoved(document);
+
+ // Test that appropriate telemetry is sent
+ spy.assertCalledWith({
+ event: "PAGE_EVENT",
+ event_context: {
+ action: "DISMISS",
+ reason: "CLICK",
+ source: sinon.match(testClickSelector),
+ page: document.location.href,
+ },
+ message_id: screenId,
+ });
+ spy.assertCalledWith({
+ event: "DISMISS",
+ event_context: {
+ source: sinon
+ .match("PAGE_EVENT:")
+ .and(sinon.match(testClickSelector)),
+ page: document.location.href,
+ },
+ message_id: screenId,
+ });
+
+ browser.tabDialogBox
+ ?.getTabDialogManager()
+ .dialogs.forEach(dialog => dialog.close());
+ }
+ );
+ Services.prefs.clearUserPref("browser.firefox-view.view-count");
+ Services.prefs.clearUserPref("identity.fxaccounts.enabled");
+ sandbox.restore();
+ ASRouter.resetMessageState();
+});
+
+add_task(async function feature_callout_advance_tour_on_page_click() {
+ let sandbox = sinon.createSandbox();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ featureTourPref,
+ JSON.stringify({
+ message: "FIREFOX_VIEW_FEATURE_TOUR",
+ screen: "FEATURE_CALLOUT_1",
+ complete: false,
+ }),
+ ],
+ ],
+ });
+
+ // Add page action listeners to the built-in messages.
+ const TEST_MESSAGES = FeatureCalloutMessages.getMessages().filter(msg =>
+ [
+ "FIREFOX_VIEW_FEATURE_TOUR_1_NO_CWS",
+ "FIREFOX_VIEW_FEATURE_TOUR_2_NO_CWS",
+ ].includes(msg.id)
+ );
+ TEST_MESSAGES.forEach(msg => {
+ let { content } = msg.content.screens[msg.content.startScreen ?? 0];
+ content.page_event_listeners = [
+ {
+ params: {
+ type: "click",
+ selectors: ".brand-logo",
+ },
+ action: JSON.parse(JSON.stringify(content.primary_button.action)),
+ },
+ ];
+ });
+ const getMessagesStub = sandbox.stub(FeatureCalloutMessages, "getMessages");
+ getMessagesStub.returns(TEST_MESSAGES);
+ await ASRouter._updateMessageProviders();
+ await ASRouter.loadMessagesFromAllProviders(
+ ASRouter.state.providers.filter(p => p.id === "onboarding")
+ );
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+
+ await waitForCalloutScreen(document, "FEATURE_CALLOUT_1");
+ info("Clicking page button");
+ document.querySelector(".brand-logo").click();
+
+ await waitForCalloutScreen(document, "FEATURE_CALLOUT_2");
+ info("Clicking page button");
+ document.querySelector(".brand-logo").click();
+
+ await waitForCalloutRemoved(document);
+ let tourComplete = JSON.parse(
+ Services.prefs.getStringPref(featureTourPref)
+ ).complete;
+ ok(
+ tourComplete,
+ `Tour is recorded as complete in ${featureTourPref} preference value`
+ );
+ }
+ );
+
+ sandbox.restore();
+ await ASRouter._updateMessageProviders();
+ await ASRouter.loadMessagesFromAllProviders(
+ ASRouter.state.providers.filter(p => p.id === "onboarding")
+ );
+});
+
+add_task(async function test_firefox_view_spotlight_promo() {
+ // Prevent attempts to fetch CFR messages remotely.
+ const sandbox = sinon.createSandbox();
+ let remoteSettingsStub = sandbox.stub(
+ MessageLoaderUtils,
+ "_remoteSettingsLoader"
+ );
+ remoteSettingsStub.resolves([]);
+
+ await SpecialPowers.pushPrefEnv({
+ clear: [
+ [featureTourPref],
+ ["browser.newtabpage.activity-stream.asrouter.providers.cfr"],
+ ],
+ });
+ ASRouter.resetMessageState();
+
+ let dialogOpenPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ null,
+ "chrome://browser/content/spotlight.html",
+ { isSubDialog: true }
+ );
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ info("Waiting for the Fx View Spotlight promo to open");
+ let dialogBrowser = await dialogOpenPromise;
+ let primaryBtnSelector = ".action-buttons button.primary";
+ await TestUtils.waitForCondition(
+ () => dialogBrowser.document.querySelector("main.DEFAULT_MODAL_UI"),
+ `Should render main.DEFAULT_MODAL_UI`
+ );
+
+ dialogBrowser.document.querySelector(primaryBtnSelector).click();
+ info("Fx View Spotlight promo clicked");
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ browser.contentWindow.performance.navigation.type ==
+ browser.contentWindow.performance.navigation.TYPE_RELOAD
+ );
+ info("Spotlight modal cleared, entering feature tour");
+
+ const { document } = browser.contentWindow;
+ await waitForCalloutScreen(document, "FEATURE_CALLOUT_1");
+ ok(
+ document.querySelector(calloutSelector),
+ "Feature Callout element exists"
+ );
+ info("Feature tour started");
+ await clickPrimaryButton(document);
+ }
+ );
+
+ ok(remoteSettingsStub.called, "Tried to load CFR messages");
+ sandbox.restore();
+ ASRouter.resetMessageState();
+});
+
+add_task(async function feature_callout_returns_default_fxview_focus_to_top() {
+ const testMessage = getCalloutMessageById(
+ "FIREFOX_VIEW_FEATURE_TOUR_1_NO_CWS"
+ );
+ const sandbox = createSandboxWithCalloutTriggerStub(testMessage);
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+ await waitForCalloutScreen(document, "FEATURE_CALLOUT_1");
+
+ ok(
+ document.querySelector(calloutSelector),
+ "Feature Callout element exists"
+ );
+
+ document.querySelector(".dismiss-button").click();
+ await waitForCalloutRemoved(document);
+
+ ok(
+ document.activeElement.localName === "body",
+ "by default focus returns to the document body after callout closes"
+ );
+ }
+ );
+ sandbox.restore();
+});
+
+add_task(
+ async function feature_callout_returns_moved_fxview_focus_to_previous() {
+ const testMessage = getCalloutMessageById(
+ "FIREFOX_VIEW_TAB_PICKUP_REMINDER"
+ );
+ const sandbox = createSandboxWithCalloutTriggerStub(testMessage);
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+ await waitForCalloutScreen(
+ document,
+ "FIREFOX_VIEW_TAB_PICKUP_REMINDER"
+ );
+
+ // change focus to recently-closed-tabs-container
+ let recentlyClosedHeaderSection = document.querySelector(
+ "#recently-closed-tabs-header-section"
+ );
+ recentlyClosedHeaderSection.focus();
+
+ // close the callout dialog
+ document.querySelector(".dismiss-button").click();
+ await waitForCalloutRemoved(document);
+
+ // verify that the focus landed in the right place
+ ok(
+ document.activeElement.id === "recently-closed-tabs-header-section",
+ "when focus changes away from callout it reverts after callout closes"
+ );
+ }
+ );
+ sandbox.restore();
+ }
+);
diff --git a/browser/components/firefoxview/tests/browser/browser_feature_callout_position.js b/browser/components/firefoxview/tests/browser/browser_feature_callout_position.js
new file mode 100644
index 0000000000..8edee7c71b
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_feature_callout_position.js
@@ -0,0 +1,403 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+const featureTourPref = "browser.firefox-view.feature-tour";
+const defaultPrefValue = getPrefValueByScreen(1);
+
+add_task(
+ async function feature_callout_first_screen_positioned_below_element() {
+ const testMessage = getCalloutMessageById(
+ "FIREFOX_VIEW_FEATURE_TOUR_1_NO_CWS"
+ );
+ const sandbox = createSandboxWithCalloutTriggerStub(testMessage);
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+ await waitForCalloutScreen(document, "FEATURE_CALLOUT_1");
+ let parentBottom = document
+ .querySelector("#tab-pickup-container")
+ .getBoundingClientRect().bottom;
+ let containerTop = document
+ .querySelector(calloutSelector)
+ .getBoundingClientRect().top;
+
+ Assert.lessOrEqual(
+ parentBottom,
+ containerTop + 5 + 1, // Add 5px for overlap and 1px for fuzziness to account for possible subpixel rounding
+ "Feature Callout is positioned below parent element with 5px overlap"
+ );
+ }
+ );
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function feature_callout_second_screen_positioned_left_of_element() {
+ const testMessage = getCalloutMessageById(
+ "FIREFOX_VIEW_FEATURE_TOUR_2_NO_CWS"
+ );
+ testMessage.message.content.screens[1].content.arrow_position = "end";
+ const sandbox = createSandboxWithCalloutTriggerStub(testMessage);
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+ const parent = document.querySelector(
+ "#recently-closed-tabs-container"
+ );
+ parent.style.gridArea = "1/2";
+ await waitForCalloutScreen(document, "FEATURE_CALLOUT_2");
+ let parentLeft = parent.getBoundingClientRect().left;
+ let containerRight = document
+ .querySelector(calloutSelector)
+ .getBoundingClientRect().right;
+
+ Assert.greaterOrEqual(
+ parentLeft,
+ containerRight - 5 - 1, // Subtract 5px for overlap and 1px for fuzziness to account for possible subpixel rounding
+ "Feature Callout is positioned left of parent element with 5px overlap"
+ );
+ }
+ );
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function feature_callout_second_screen_positioned_above_element() {
+ const testMessage = getCalloutMessageById(
+ "FIREFOX_VIEW_FEATURE_TOUR_2_NO_CWS"
+ );
+ const sandbox = createSandboxWithCalloutTriggerStub(testMessage);
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+ await waitForCalloutScreen(document, "FEATURE_CALLOUT_2");
+ let parentTop = document
+ .querySelector("#recently-closed-tabs-container")
+ .getBoundingClientRect().top;
+ let containerBottom = document
+ .querySelector(calloutSelector)
+ .getBoundingClientRect().bottom;
+
+ Assert.greaterOrEqual(
+ parentTop,
+ containerBottom - 5 - 1,
+ "Feature Callout is positioned above parent element with 5px overlap"
+ );
+ }
+ );
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function feature_callout_third_screen_position_respects_RTL_layouts() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Set layout direction to right to left
+ ["intl.l10n.pseudo", "bidi"],
+ ],
+ });
+
+ const testMessage = getCalloutMessageById(
+ "FIREFOX_VIEW_FEATURE_TOUR_2_NO_CWS"
+ );
+ const sandbox = createSandboxWithCalloutTriggerStub(testMessage);
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+ const parent = document.querySelector(
+ "#recently-closed-tabs-container"
+ );
+ parent.style.gridArea = "1/2";
+ await waitForCalloutScreen(document, "FEATURE_CALLOUT_2");
+ let parentRight = parent.getBoundingClientRect().right;
+ let containerLeft = document
+ .querySelector(calloutSelector)
+ .getBoundingClientRect().left;
+
+ Assert.lessOrEqual(
+ parentRight,
+ containerLeft + 5 + 1,
+ "Feature Callout is positioned right of parent element when callout is set to 'end' in RTL layouts"
+ );
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+ sandbox.restore();
+ }
+);
+
+add_task(
+ async function feature_callout_is_repositioned_if_parent_container_is_toggled() {
+ const testMessage = getCalloutMessageById(
+ "FIREFOX_VIEW_FEATURE_TOUR_1_NO_CWS"
+ );
+ const sandbox = createSandboxWithCalloutTriggerStub(testMessage);
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+ await waitForCalloutScreen(document, "FEATURE_CALLOUT_1");
+ const parentEl = document.querySelector("#tab-pickup-container");
+ const calloutStartingTopPosition = document.querySelector(
+ calloutSelector
+ ).style.top;
+
+ //container has been toggled/minimized
+ parentEl.removeAttribute("open", "");
+ await BrowserTestUtils.waitForMutationCondition(
+ document.querySelector(calloutSelector),
+ { attributes: true },
+ () =>
+ document.querySelector(calloutSelector).style.top !=
+ calloutStartingTopPosition
+ );
+ isnot(
+ document.querySelector(calloutSelector).style.top,
+ calloutStartingTopPosition,
+ "Feature Callout position is recalculated when parent element is toggled"
+ );
+ await closeCallout(document);
+ }
+ );
+ sandbox.restore();
+ }
+);
+
+// This test should be moved into a surface agnostic test suite with bug 1793656.
+add_task(async function feature_callout_top_end_positioning() {
+ const testMessage = getCalloutMessageById(
+ "FIREFOX_VIEW_FEATURE_TOUR_1_NO_CWS"
+ );
+ testMessage.message.content.screens[0].content.arrow_position = "top-end";
+ const sandbox = createSandboxWithCalloutTriggerStub(testMessage);
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+ await waitForCalloutScreen(document, "FEATURE_CALLOUT_1");
+ let parent = document.querySelector("#tab-pickup-container");
+ let container = document.querySelector(calloutSelector);
+ let parentLeft = parent.getBoundingClientRect().left;
+ let containerLeft = container.getBoundingClientRect().left;
+
+ ok(
+ container.classList.contains("arrow-top-end"),
+ "Feature Callout container has the expected arrow-top-end class"
+ );
+ isfuzzy(
+ containerLeft - parent.clientWidth + container.offsetWidth,
+ parentLeft,
+ 1, // Display scaling can cause up to 1px difference in layout
+ "Feature Callout's right edge is approximately aligned with parent element's right edge"
+ );
+
+ await closeCallout(document);
+ }
+ );
+ sandbox.restore();
+});
+
+// This test should be moved into a surface agnostic test suite with bug 1793656.
+add_task(async function feature_callout_top_start_positioning() {
+ const testMessage = getCalloutMessageById(
+ "FIREFOX_VIEW_FEATURE_TOUR_1_NO_CWS"
+ );
+ testMessage.message.content.screens[0].content.arrow_position = "top-start";
+ const sandbox = createSandboxWithCalloutTriggerStub(testMessage);
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+ await waitForCalloutScreen(document, "FEATURE_CALLOUT_1");
+ let parent = document.querySelector("#tab-pickup-container");
+ let container = document.querySelector(calloutSelector);
+ let parentLeft = parent.getBoundingClientRect().left;
+ let containerLeft = container.getBoundingClientRect().left;
+
+ ok(
+ container.classList.contains("arrow-top-start"),
+ "Feature Callout container has the expected arrow-top-start class"
+ );
+ isfuzzy(
+ containerLeft,
+ parentLeft,
+ 1, // Display scaling can cause up to 1px difference in layout
+ "Feature Callout's left edge is approximately aligned with parent element's left edge"
+ );
+
+ await closeCallout(document);
+ }
+ );
+ sandbox.restore();
+});
+
+// This test should be moved into a surface agnostic test suite with bug 1793656.
+add_task(
+ async function feature_callout_top_end_position_respects_RTL_layouts() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Set layout direction to right to left
+ ["intl.l10n.pseudo", "bidi"],
+ ],
+ });
+
+ const testMessage = getCalloutMessageById(
+ "FIREFOX_VIEW_FEATURE_TOUR_1_NO_CWS"
+ );
+ testMessage.message.content.screens[0].content.arrow_position = "top-end";
+ const sandbox = createSandboxWithCalloutTriggerStub(testMessage);
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+ await waitForCalloutScreen(document, "FEATURE_CALLOUT_1");
+ let parent = document.querySelector("#tab-pickup-container");
+ let container = document.querySelector(calloutSelector);
+ let parentLeft = parent.getBoundingClientRect().left;
+ let containerLeft = container.getBoundingClientRect().left;
+
+ ok(
+ container.classList.contains("arrow-top-start"),
+ "In RTL mode, the feature Callout container has the expected arrow-top-start class"
+ );
+ is(
+ containerLeft,
+ parentLeft,
+ "In RTL mode, the feature Callout's left edge is aligned with parent element's left edge"
+ );
+
+ await closeCallout(document);
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+ sandbox.restore();
+ }
+);
+
+add_task(async function feature_callout_is_larger_than_its_parent() {
+ let testMessage = {
+ message: {
+ id: "FIREFOX_VIEW_FEATURE_TOUR_1_NO_CWS",
+ template: "feature_callout",
+ content: {
+ id: "FIREFOX_VIEW_FEATURE_TOUR",
+ transitions: false,
+ disableHistoryUpdates: true,
+ screens: [
+ {
+ id: "FEATURE_CALLOUT_1",
+ parent_selector: ".brand-icon",
+ content: {
+ position: "callout",
+ arrow_position: "end",
+ title: "callout-firefox-view-tab-pickup-title",
+ subtitle: {
+ string_id: "callout-firefox-view-tab-pickup-subtitle",
+ },
+ logo: {
+ imageURL: "chrome://browser/content/callout-tab-pickup.svg",
+ darkModeImageURL:
+ "chrome://browser/content/callout-tab-pickup-dark.svg",
+ height: "128px", // .brand-icon has a height of 32px
+ },
+ dismiss_button: {
+ action: {
+ navigate: true,
+ },
+ },
+ },
+ },
+ ],
+ },
+ },
+ };
+
+ const sandbox = createSandboxWithCalloutTriggerStub(testMessage);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [[featureTourPref, getPrefValueByScreen(1)]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+ await waitForCalloutScreen(document, "FEATURE_CALLOUT_1");
+ let parent = document.querySelector(".brand-icon");
+ let container = document.querySelector(calloutSelector);
+ let parentHeight = parent.offsetHeight;
+ let containerHeight = container.offsetHeight;
+
+ let parentPositionTop =
+ parent.getBoundingClientRect().top + window.scrollY;
+ let containerPositionTop =
+ container.getBoundingClientRect().top + window.scrollY;
+ Assert.greater(
+ containerHeight,
+ parentHeight,
+ "Feature Callout is height is larger than parent element when callout is configured at end of callout"
+ );
+ Assert.less(
+ containerPositionTop,
+ parentPositionTop,
+ "Feature Callout is positioned higher that parent element when callout is configured at end of callout"
+ );
+ isfuzzy(
+ containerHeight / 2 + containerPositionTop,
+ parentHeight / 2 + parentPositionTop,
+ 1, // Display scaling can cause up to 1px difference in layout
+ "Feature Callout is centered equally to parent element when callout is configured at end of callout"
+ );
+ await ASRouter.resetMessageState();
+ }
+ );
+ sandbox.restore();
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_feature_callout_resize.js b/browser/components/firefoxview/tests/browser/browser_feature_callout_resize.js
new file mode 100644
index 0000000000..1f9d00975a
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_feature_callout_resize.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const featureTourPref = "browser.firefox-view.feature-tour";
+
+add_setup(async function setup() {
+ let originalWidth = window.outerWidth;
+ let originalHeight = window.outerHeight;
+ registerCleanupFunction(async () => {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:firefoxview" },
+ async browser => window.FullZoom.reset(browser)
+ );
+ window.resizeTo(originalWidth, originalHeight);
+ });
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:firefoxview" },
+ async browser => window.FullZoom.setZoom(0.5, browser)
+ );
+});
+
+add_task(async function feature_callout_is_repositioned_if_it_does_not_fit() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.sessionstore.max_tabs_undo", 1]],
+ });
+ const testMessage = getCalloutMessageById(
+ "FIREFOX_VIEW_FEATURE_TOUR_1_NO_CWS"
+ );
+ const sandbox = createSandboxWithCalloutTriggerStub(testMessage);
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:firefoxview" },
+ async browser => {
+ const { document } = browser.contentWindow;
+
+ browser.contentWindow.resizeTo(1550, 1000);
+ await waitForCalloutScreen(document, "FEATURE_CALLOUT_1");
+ ok(
+ document.querySelector(`${calloutSelector}.arrow-top`),
+ "On first screen at 1550x1000, the callout is positioned below the parent element"
+ );
+
+ let startingTop = document.querySelector(calloutSelector).style.top;
+ browser.contentWindow.resizeTo(1600, 400);
+ // Wait for callout to be repositioned
+ await BrowserTestUtils.waitForMutationCondition(
+ document.querySelector(calloutSelector),
+ { attributeFilter: ["style"], attributes: true },
+ () => document.querySelector(calloutSelector).style.top != startingTop
+ );
+ ok(
+ document.querySelector(`${calloutSelector}.arrow-inline-start`),
+ "On first screen at 1600x400, the callout is positioned to the right of the parent element"
+ );
+
+ startingTop = document.querySelector(calloutSelector).style.top;
+ browser.contentWindow.resizeTo(1100, 600);
+ await BrowserTestUtils.waitForMutationCondition(
+ document.querySelector(calloutSelector),
+ { attributeFilter: ["style"], attributes: true },
+ () => document.querySelector(calloutSelector).style.top != startingTop
+ );
+ ok(
+ document.querySelector(`${calloutSelector}.arrow-top`),
+ "On first screen at 1100x600, the callout is positioned below the parent element"
+ );
+ }
+ );
+ sandbox.restore();
+});
+
+add_task(async function feature_callout_is_repositioned_rtl() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Set layout direction to right to left
+ ["intl.l10n.pseudo", "bidi"],
+ ["browser.sessionstore.max_tabs_undo", 1],
+ ],
+ });
+
+ const testMessage = getCalloutMessageById(
+ "FIREFOX_VIEW_FEATURE_TOUR_1_NO_CWS"
+ );
+ const sandbox = createSandboxWithCalloutTriggerStub(testMessage);
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:firefoxview" },
+ async browser => {
+ const { document } = browser.contentWindow;
+
+ browser.contentWindow.resizeTo(1550, 1000);
+ await waitForCalloutScreen(document, "FEATURE_CALLOUT_1");
+ ok(
+ document.querySelector(`${calloutSelector}.arrow-top`),
+ "On first screen at 1550x1000, the callout is positioned below the parent element"
+ );
+
+ let startingTop = document.querySelector(calloutSelector).style.top;
+ browser.contentWindow.resizeTo(1600, 400);
+ // Wait for callout to be repositioned
+ await BrowserTestUtils.waitForMutationCondition(
+ document.querySelector(calloutSelector),
+ { attributeFilter: ["style"], attributes: true },
+ () => document.querySelector(calloutSelector).style.top != startingTop
+ );
+ ok(
+ document.querySelector(`${calloutSelector}.arrow-inline-end`),
+ "On first screen at 1600x400, the callout is positioned to the right of the parent element"
+ );
+
+ startingTop = document.querySelector(calloutSelector).style.top;
+ browser.contentWindow.resizeTo(1100, 600);
+ await BrowserTestUtils.waitForMutationCondition(
+ document.querySelector(calloutSelector),
+ { attributeFilter: ["style"], attributes: true },
+ () => document.querySelector(calloutSelector).style.top != startingTop
+ );
+ ok(
+ document.querySelector(`${calloutSelector}.arrow-top`),
+ "On first screen at 1100x600, the callout is positioned below the parent element"
+ );
+ }
+ );
+ sandbox.restore();
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_feature_callout_targeting.js b/browser/components/firefoxview/tests/browser/browser_feature_callout_targeting.js
new file mode 100644
index 0000000000..ccba3e4560
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_feature_callout_targeting.js
@@ -0,0 +1,175 @@
+"use strict";
+
+const { BuiltInThemes } = ChromeUtils.importESModule(
+ "resource:///modules/BuiltInThemes.sys.mjs"
+);
+
+const { AddonManager } = ChromeUtils.import(
+ "resource://gre/modules/AddonManager.jsm"
+);
+
+add_task(
+ async function test_firefox_view_tab_pick_up_not_signed_in_targeting() {
+ ASRouter.resetMessageState();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.firefox-view.feature-tour", `{"screen":"","complete":true}`],
+ ],
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.firefox-view.view-count", 3]],
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["identity.fxaccounts.enabled", false]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+
+ await waitForCalloutScreen(
+ document,
+ "FIREFOX_VIEW_TAB_PICKUP_REMINDER"
+ );
+ ok(
+ document.querySelector(".featureCallout"),
+ "Firefox:View Tab Pickup should be displayed."
+ );
+
+ SpecialPowers.popPrefEnv();
+ SpecialPowers.popPrefEnv();
+ SpecialPowers.popPrefEnv();
+ }
+ );
+ }
+);
+
+add_task(
+ async function test_firefox_view_tab_pick_up_sync_not_enabled_targeting() {
+ ASRouter.resetMessageState();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.firefox-view.feature-tour", `{"screen":"","complete":true}`],
+ ],
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.firefox-view.view-count", 3]],
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["identity.fxaccounts.enabled", true]],
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["services.sync.engine.tabs", false]],
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["services.sync.username", false]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+
+ await waitForCalloutScreen(
+ document,
+ "FIREFOX_VIEW_TAB_PICKUP_REMINDER"
+ );
+ ok(
+ document.querySelector(".featureCallout"),
+ "Firefox:View Tab Pickup should be displayed."
+ );
+
+ SpecialPowers.popPrefEnv();
+ SpecialPowers.popPrefEnv();
+ SpecialPowers.popPrefEnv();
+ SpecialPowers.popPrefEnv();
+ SpecialPowers.popPrefEnv();
+ }
+ );
+ }
+);
+
+add_task(
+ async function test_firefox_view_tab_pick_up_wait_24_hours_after_spotlight() {
+ const TWENTY_FIVE_HOURS_IN_MS = 25 * 60 * 60 * 1000;
+
+ ASRouter.resetMessageState();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.firefox-view.feature-tour", `{"screen":"","complete":true}`],
+ ],
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.firefox-view.view-count", 3]],
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["identity.fxaccounts.enabled", false]],
+ });
+
+ ASRouter.setState({
+ messageImpressions: { FIREFOX_VIEW_SPOTLIGHT: [Date.now()] },
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+
+ ok(
+ !document.querySelector(".featureCallout"),
+ "Tab Pickup reminder should not be displayed when the Spotlight message introducing the tour was viewed less than 24 hours ago."
+ );
+ }
+ );
+
+ ASRouter.setState({
+ messageImpressions: {
+ FIREFOX_VIEW_SPOTLIGHT: [Date.now() - TWENTY_FIVE_HOURS_IN_MS],
+ },
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+
+ await waitForCalloutScreen(
+ document,
+ "FIREFOX_VIEW_TAB_PICKUP_REMINDER"
+ );
+ ok(
+ document.querySelector(".featureCallout"),
+ "Tab Pickup reminder can be displayed when the Spotlight message introducing the tour was viewed over 24 hours ago."
+ );
+
+ SpecialPowers.popPrefEnv();
+ SpecialPowers.popPrefEnv();
+ SpecialPowers.popPrefEnv();
+ }
+ );
+ }
+);
diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_firefoxview.js
new file mode 100644
index 0000000000..5ac9dd4c7b
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_firefoxview.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function about_firefoxview_smoke_test() {
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+
+ // sanity check the important regions exist on this page
+ ok(
+ document.getElementById("tab-pickup-container"),
+ "tab-pickup-container element exists"
+ );
+ ok(
+ document.getElementById("recently-closed-tabs-container"),
+ "recently-closed-tabs-container element exists"
+ );
+ });
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview_accessibility.js b/browser/components/firefoxview/tests/browser/browser_firefoxview_accessibility.js
new file mode 100644
index 0000000000..e4b2e866a0
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_firefoxview_accessibility.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that are related to the accessibility of the Firefox View
+ * document. These tasks tend to be privileged content, not browser
+ * chrome.
+ */
+
+add_setup(async function setup() {
+ // Make sure the prompt to connect FxA doesn't show
+ // Without resetting the view-count pref it gets surfaced after
+ // the third click on the fx view toolbar button.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.firefox-view.view-count", 0]],
+ });
+});
+
+add_task(async function test_keyboard_focus_after_tab_pickup_opened() {
+ // Reset various things touched by other tests in this file so that
+ // we have a sufficiently clean environment.
+
+ TabsSetupFlowManager.resetInternalState();
+
+ // Ensure that the tab-pickup section doesn't need to be opened.
+ Services.prefs.clearUserPref(
+ "browser.tabs.firefox-view.ui-state.tab-pickup.open"
+ );
+
+ // make sure the feature tour doesn't get in the way
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.firefox-view.feature-tour",
+ JSON.stringify({
+ screen: `FEATURE_CALLOUT_1`,
+ complete: true,
+ }),
+ ],
+ ],
+ });
+
+ // Let's be deterministic about the basic UI state!
+ const sandbox = setupMocks({
+ state: UIState.STATUS_NOT_CONFIGURED,
+ syncEnabled: false,
+ });
+
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ let win = browser.ownerGlobal;
+
+ is(
+ document.activeElement.localName,
+ "body",
+ "document body element is initially focused"
+ );
+
+ const tab = () => {
+ info("Tab keypress synthesized");
+ EventUtils.synthesizeKey("KEY_Tab", {}, win);
+ };
+
+ tab();
+
+ let tabPickupContainer = document.querySelector(
+ "#tab-pickup-container summary.page-section-header"
+ );
+ is(
+ document.activeElement,
+ tabPickupContainer,
+ "tab pickup container header has focus"
+ );
+
+ tab();
+
+ is(
+ document.activeElement.id,
+ "firefoxview-tabpickup-step-signin-primarybutton",
+ "tab pickup primary button has focus"
+ );
+ });
+
+ // cleanup time
+ await tearDown(sandbox);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_keyboard_accessibility_tab_pickup() {
+ await withFirefoxView({}, async browser => {
+ const win = browser.ownerGlobal;
+ const { document } = browser.contentWindow;
+ const enter = async () => {
+ info("Enter");
+ EventUtils.synthesizeKey("KEY_Enter", {}, win);
+ };
+ let details = document.getElementById("tab-pickup-container");
+ let summary = details.querySelector("summary");
+ ok(summary, "summary element should exist");
+ ok(details.open, "Tab pickup container should be initially open on load");
+ summary.focus();
+ await enter();
+ ok(!details.open, "Tab pickup container should be closed");
+ await enter();
+ ok(details.open, "Tab pickup container should be opened");
+ });
+ cleanup_tab_pickup();
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview_feature_callout_a11y.js b/browser/components/firefoxview/tests/browser/browser_firefoxview_feature_callout_a11y.js
new file mode 100644
index 0000000000..17485e54e1
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_firefoxview_feature_callout_a11y.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that are related to the accessibility of the feature callout
+ */
+
+/**
+ * Ensure feature tour is accessible using a screen reader and with
+ * keyboard navigation.
+ */
+add_task(async function feature_callout_is_accessible() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.firefox-view.feature-tour", getPrefValueByScreen(1)]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:firefoxview",
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+ await waitForCalloutScreen(document, "FEATURE_CALLOUT_1");
+
+ await BrowserTestUtils.waitForCondition(
+ () => document.activeElement.id === calloutId,
+ "Feature Callout is focused on page load"
+ );
+ ok(true, "Feature Callout was focused on page load");
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ document.querySelector(
+ `${calloutSelector}[aria-describedby="#${calloutId} .welcome-text"]`
+ ),
+ "The callout container has an aria-describedby value equal to the screen welcome text"
+ );
+ ok(true, "The callout container has the correct aria-describedby value");
+
+ // Advance to second screen
+ clickPrimaryButton(document);
+ await waitForCalloutScreen(document, "FEATURE_CALLOUT_2");
+
+ ok(true, "FEATURE_CALLOUT_2 was successfully displayed");
+ await BrowserTestUtils.waitForCondition(
+ () => document.activeElement.id === calloutId,
+ "Feature Callout is focused after advancing screens"
+ );
+ ok(true, "Feature Callout was successfully focused");
+ }
+ );
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview_tab.js b/browser/components/firefoxview/tests/browser/browser_firefoxview_tab.js
new file mode 100644
index 0000000000..f178ad4f32
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_firefoxview_tab.js
@@ -0,0 +1,249 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+async function expectFocusAfterKey(
+ aKey,
+ aFocus,
+ aAncestorOk = false,
+ aWindow = window
+) {
+ let res = aKey.match(/^(Shift\+)?(?:(.)|(.+))$/);
+ let shift = Boolean(res[1]);
+ let key;
+ if (res[2]) {
+ key = res[2]; // Character.
+ } else {
+ key = "KEY_" + res[3]; // Tab, ArrowRight, etc.
+ }
+ let expected;
+ let friendlyExpected;
+ if (typeof aFocus == "string") {
+ expected = aWindow.document.getElementById(aFocus);
+ friendlyExpected = aFocus;
+ } else {
+ expected = aFocus;
+ if (aFocus == aWindow.gURLBar.inputField) {
+ friendlyExpected = "URL bar input";
+ } else if (aFocus == aWindow.gBrowser.selectedBrowser) {
+ friendlyExpected = "Web document";
+ }
+ }
+ info("Listening on item " + (expected.id || expected.className));
+ let focused = BrowserTestUtils.waitForEvent(expected, "focus", aAncestorOk);
+ EventUtils.synthesizeKey(key, { shiftKey: shift }, aWindow);
+ let receivedEvent = await focused;
+ info(
+ "Got focus on item: " +
+ (receivedEvent.target.id || receivedEvent.target.className)
+ );
+ ok(true, friendlyExpected + " focused after " + aKey + " pressed");
+}
+
+function forceFocus(aElem) {
+ aElem.setAttribute("tabindex", "-1");
+ aElem.focus();
+ aElem.removeAttribute("tabindex");
+}
+
+add_task(async function aria_attributes() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ is(
+ win.FirefoxViewHandler.button.getAttribute("role"),
+ "button",
+ "Firefox View button should have the 'button' ARIA role"
+ );
+ await openFirefoxViewTab(win);
+ isnot(
+ win.FirefoxViewHandler.button.getAttribute("aria-controls"),
+ "",
+ "Firefox View button should have non-empty `aria-controls` attribute"
+ );
+ is(
+ win.FirefoxViewHandler.button.getAttribute("aria-controls"),
+ win.FirefoxViewHandler.tab.linkedPanel,
+ "Firefox View button should refence the hidden tab's linked panel via `aria-controls`"
+ );
+ is(
+ win.FirefoxViewHandler.button.getAttribute("aria-pressed"),
+ "true",
+ 'Firefox View button should have `aria-pressed="true"` upon selecting it'
+ );
+ win.BrowserOpenTab();
+ is(
+ win.FirefoxViewHandler.button.getAttribute("aria-pressed"),
+ "false",
+ 'Firefox View button should have `aria-pressed="false"` upon selecting a different tab'
+ );
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function load_opens_new_tab() {
+ await withFirefoxView({}, async browser => {
+ let win = browser.ownerGlobal;
+ ok(win.FirefoxViewHandler.tab.selected, "Firefox View tab is selected");
+ win.gURLBar.focus();
+ win.gURLBar.value = "https://example.com";
+ let newTabOpened = BrowserTestUtils.waitForEvent(
+ win.gBrowser.tabContainer,
+ "TabOpen"
+ );
+ EventUtils.synthesizeKey("KEY_Enter", {}, win);
+ info(
+ "Waiting for new tab to open from the address bar in the Firefox View tab"
+ );
+ await newTabOpened;
+ assertFirefoxViewTab(win);
+ ok(
+ !win.FirefoxViewHandler.tab.selected,
+ "Firefox View tab is not selected anymore (new tab opened in the foreground)"
+ );
+ });
+});
+
+add_task(async function homepage_new_tab() {
+ await withFirefoxView({}, async browser => {
+ let win = browser.ownerGlobal;
+ ok(win.FirefoxViewHandler.tab.selected, "Firefox View tab is selected");
+ let newTabOpened = BrowserTestUtils.waitForEvent(
+ win.gBrowser.tabContainer,
+ "TabOpen"
+ );
+ win.BrowserHome();
+ info("Waiting for BrowserHome() to open a new tab");
+ await newTabOpened;
+ assertFirefoxViewTab(win);
+ ok(
+ !win.FirefoxViewHandler.tab.selected,
+ "Firefox View tab is not selected anymore (home page opened in the foreground)"
+ );
+ });
+});
+
+add_task(async function number_tab_select_shortcut() {
+ await withFirefoxView({}, async browser => {
+ let win = browser.ownerGlobal;
+ EventUtils.synthesizeKey(
+ "1",
+ AppConstants.MOZ_WIDGET_GTK ? { altKey: true } : { accelKey: true },
+ win
+ );
+ ok(
+ !win.FirefoxViewHandler.tab.selected,
+ "Number shortcut to select the first tab skipped the Firefox View tab"
+ );
+ });
+});
+
+add_task(async function accel_w_behavior() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await openFirefoxViewTab(win);
+ EventUtils.synthesizeKey("w", { accelKey: true }, win);
+ ok(!win.FirefoxViewHandler.tab, "Accel+w closed the Firefox View tab");
+ await openFirefoxViewTab(win);
+ win.gBrowser.selectedTab = win.gBrowser.visibleTabs[0];
+ info(
+ "Waiting for Accel+W in the only visible tab to close the window, ignoring the presence of the hidden Firefox View tab"
+ );
+ let windowClosed = BrowserTestUtils.windowClosed(win);
+ EventUtils.synthesizeKey("w", { accelKey: true }, win);
+ await windowClosed;
+});
+
+add_task(async function undo_close_tab() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ Services.obs.notifyObservers(null, "browser:purge-session-history");
+ is(
+ SessionStore.getClosedTabCount(win),
+ 0,
+ "Closed tab count after purging session history"
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ win.gBrowser,
+ "about:about"
+ );
+ await TestUtils.waitForTick();
+
+ let sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab);
+ win.gBrowser.removeTab(tab);
+ await sessionUpdatePromise;
+ is(
+ SessionStore.getClosedTabCount(win),
+ 1,
+ "Closing about:about added to the closed tab count"
+ );
+
+ let viewTab = await openFirefoxViewTab(win);
+ await TestUtils.waitForTick();
+ sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(viewTab);
+ closeFirefoxViewTab(win);
+ await sessionUpdatePromise;
+ is(
+ SessionStore.getClosedTabCount(win),
+ 1,
+ "Closing the Firefox View tab did not add to the closed tab count"
+ );
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_firefoxview_view_count() {
+ const startViews = 2;
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.firefox-view.view-count", startViews]],
+ });
+
+ let tab = await openFirefoxViewTab(window);
+
+ ok(
+ SpecialPowers.getIntPref("browser.firefox-view.view-count") ===
+ startViews + 1,
+ "View count pref value is incremented when tab is selected"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_add_ons_cant_unhide_fx_view() {
+ // Test that add-ons can't unhide the Firefox View tab by calling
+ // browser.tabs.show(). See bug 1791770 for details.
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ win.gBrowser,
+ "about:about"
+ );
+ let viewTab = await openFirefoxViewTab(win);
+ win.gBrowser.hideTab(tab);
+
+ ok(tab.hidden, "Regular tab is hidden");
+ ok(viewTab.hidden, "Firefox View tab is hidden");
+
+ win.gBrowser.showTab(tab);
+ win.gBrowser.showTab(viewTab);
+
+ ok(!tab.hidden, "Add-on showed regular hidden tab");
+ ok(viewTab.hidden, "Add-on did not show Firefox View tab");
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+// Test navigation to first visible tab when the
+// Firefox View button is present and active.
+add_task(async function testFirstTabFocusableWhenFxViewOpen() {
+ await SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] });
+ await withFirefoxView({}, async browser => {
+ let win = browser.ownerGlobal;
+ ok(win.FirefoxViewHandler.tab.selected, "Firefox View tab is selected");
+ let fxViewBtn = win.document.getElementById("firefox-view-button");
+ forceFocus(fxViewBtn);
+ is(
+ win.document.activeElement,
+ fxViewBtn,
+ "Firefox View button focused for start of test"
+ );
+ let firstVisibleTab = win.gBrowser.visibleTabs[0];
+ await expectFocusAfterKey("Tab", firstVisibleTab, false, win);
+ let activeElement = win.document.activeElement;
+ let expectedElement = firstVisibleTab;
+ is(activeElement, expectedElement, "First visible tab should be focused");
+ });
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_keyboard_focus.js b/browser/components/firefoxview/tests/browser/browser_keyboard_focus.js
new file mode 100644
index 0000000000..aaeb2b7792
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_keyboard_focus.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { TabsSetupFlowManager } = ChromeUtils.importESModule(
+ "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs"
+);
+
+XPCOMUtils.defineLazyModuleGetters(globalThis, {
+ SyncedTabs: "resource://services-sync/SyncedTabs.jsm",
+});
+
+const SYNCED_URI = syncedTabsData1[0].tabs[1].url;
+
+add_task(async function test_keyboard_focus() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["accessibility.tabfocus", 7]],
+ });
+
+ await withFirefoxView({ win: window }, async browser => {
+ const { document } = browser.contentWindow;
+
+ const sandbox = setupRecentDeviceListMocks();
+ const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
+ let mockTabs1 = getMockTabData(syncedTabsData1);
+ syncedTabsMock.returns(mockTabs1);
+
+ await setupListState(browser);
+
+ testVisibility(browser, {
+ expectedVisible: {
+ "ol.synced-tabs-list": true,
+ },
+ });
+
+ let tabPickupEle = document.querySelector(".synced-tab-a");
+ document.querySelector(".page-section-header").focus();
+
+ EventUtils.synthesizeKey("KEY_Tab");
+
+ is(
+ tabPickupEle,
+ document.activeElement,
+ "The first tab pickup link is focused"
+ );
+
+ let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, SYNCED_URI);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await newTabPromise;
+
+ is(
+ SYNCED_URI,
+ gBrowser.selectedBrowser.currentURI.displaySpec,
+ "We opened the tab via keyboard"
+ );
+
+ let sessionStorePromise = BrowserTestUtils.waitForSessionStoreUpdate(
+ gBrowser.selectedTab
+ );
+ gBrowser.removeTab(gBrowser.selectedTab);
+ await sessionStorePromise;
+
+ window.FirefoxViewHandler.openTab();
+
+ let recentlyClosedEle = document.querySelector(".closed-tab-li-main");
+ document.querySelectorAll(".page-section-header")[1].focus();
+
+ EventUtils.synthesizeKey("KEY_Tab");
+
+ is(
+ recentlyClosedEle,
+ document.activeElement,
+ "The recently closed tab is focused"
+ );
+
+ newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, SYNCED_URI);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await newTabPromise;
+ is(
+ SYNCED_URI,
+ gBrowser.selectedBrowser.currentURI.displaySpec,
+ "We opened the tab via keyboard"
+ );
+ gBrowser.removeTab(gBrowser.selectedTab);
+
+ sessionStorePromise = TestUtils.topicObserved(
+ "sessionstore-closed-objects-changed"
+ );
+ SessionStore.forgetClosedTab(window, 0);
+ await sessionStorePromise;
+
+ sandbox.restore();
+ });
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_media_query_dom_sorting.js b/browser/components/firefoxview/tests/browser/browser_media_query_dom_sorting.js
new file mode 100644
index 0000000000..e10158504b
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_media_query_dom_sorting.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const WIDE_WINDOW_WIDTH = 1100;
+const NARROW_WINDOW_WIDTH = 900;
+
+function getTestElements(doc) {
+ return {
+ recentlyClosedTabs: doc.getElementById("recently-closed-tabs-container"),
+ colorways: doc.getElementById("colorways"),
+ };
+}
+
+function iscolorwaysBeforeRecentlyClosedTabs(document) {
+ const recentlyClosedTabs = document.getElementById(
+ "recently-closed-tabs-container"
+ );
+ const colorways = document.getElementById("colorways");
+ return recentlyClosedTabs.previousElementSibling === colorways;
+}
+
+async function resizeWindow(win, width) {
+ const resizePromise = BrowserTestUtils.waitForEvent(win, "resize");
+ win.windowUtils.ensureDirtyRootFrame();
+ info("Resizing window...");
+ win.resizeTo(width, win.outerHeight);
+ await resizePromise;
+}
+
+add_task(async function media_query_less_than_65em() {
+ await withFirefoxView({}, async browser => {
+ let win = browser.contentWindow;
+ const { recentlyClosedTabs, colorways } = getTestElements(win.document);
+ await resizeWindow(win, NARROW_WINDOW_WIDTH);
+ is(
+ recentlyClosedTabs.previousSibling,
+ colorways,
+ "colorway card has been positioned before recently closed tabs"
+ );
+ });
+});
+
+add_task(async function media_query_more_than_65em() {
+ await withFirefoxView({}, async browser => {
+ let win = browser.contentWindow;
+ const { recentlyClosedTabs, colorways } = getTestElements(win.document);
+ await resizeWindow(win, WIDE_WINDOW_WIDTH);
+ is(
+ recentlyClosedTabs.nextSibling,
+ colorways,
+ "colorway card has been positioned after recently closed tabs"
+ );
+ });
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_notification_dot.js b/browser/components/firefoxview/tests/browser/browser_notification_dot.js
new file mode 100644
index 0000000000..729377bf8d
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_notification_dot.js
@@ -0,0 +1,335 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const tabsList1 = syncedTabsData1[0].tabs;
+const tabsList2 = syncedTabsData1[1].tabs;
+const BADGE_TOP_RIGHT = "75% 25%";
+
+const { SyncedTabs } = ChromeUtils.import(
+ "resource://services-sync/SyncedTabs.jsm"
+);
+
+const { FirefoxViewNotificationManager } = ChromeUtils.importESModule(
+ "resource:///modules/firefox-view-notification-manager.sys.mjs"
+);
+
+function setupRecentDeviceListMocks() {
+ const sandbox = sinon.createSandbox();
+ sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => [
+ {
+ id: 1,
+ name: "My desktop",
+ isCurrentDevice: true,
+ type: "desktop",
+ },
+ {
+ id: 2,
+ name: "My iphone",
+ type: "mobile",
+ },
+ ]);
+
+ sandbox.stub(UIState, "get").returns({
+ status: UIState.STATUS_SIGNED_IN,
+ syncEnabled: true,
+ });
+
+ return sandbox;
+}
+
+function waitForWindowActive(win, active) {
+ info("Waiting for window activation");
+ return Promise.all([
+ BrowserTestUtils.waitForEvent(win, active ? "focus" : "blur"),
+ BrowserTestUtils.waitForEvent(win, active ? "activate" : "deactivate"),
+ ]);
+}
+
+async function waitForNotificationBadgeToBeShowing(fxViewButton) {
+ info("Waiting for attention attribute to be set");
+ await BrowserTestUtils.waitForMutationCondition(
+ fxViewButton,
+ { attributes: true },
+ () => fxViewButton.hasAttribute("attention")
+ );
+ return fxViewButton.hasAttribute("attention");
+}
+
+async function waitForNotificationBadgeToBeHidden(fxViewButton) {
+ info("Waiting for attention attribute to be removed");
+ await BrowserTestUtils.waitForMutationCondition(
+ fxViewButton,
+ { attributes: true },
+ () => !fxViewButton.hasAttribute("attention")
+ );
+ return !fxViewButton.hasAttribute("attention");
+}
+
+function getBackgroundPositionForElement(ele) {
+ let style = ele.ownerGlobal.getComputedStyle(ele);
+ return style.getPropertyValue("background-position");
+}
+
+let recentFetchTime = Math.floor(Date.now() / 1000);
+async function initTabSync() {
+ recentFetchTime += 1;
+ info("updating lastFetch:" + recentFetchTime);
+ Services.prefs.setIntPref("services.sync.lastTabFetch", recentFetchTime);
+ await TestUtils.waitForTick();
+}
+
+add_setup(async function() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.firefox-view.notify-for-tabs", true]],
+ });
+
+ // Clear any synced tabs from previous tests
+ FirefoxViewNotificationManager.syncedTabs = null;
+ Services.obs.notifyObservers(
+ null,
+ "firefoxview-notification-dot-update",
+ "false"
+ );
+});
+
+/**
+ * Test that the notification badge will show and hide in the correct cases
+ */
+add_task(async function testNotificationDot() {
+ const sandbox = setupRecentDeviceListMocks();
+ const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
+ sandbox.spy(SyncedTabs, "syncTabs");
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ let fxViewBtn = win.document.getElementById("firefox-view-button");
+ ok(fxViewBtn, "Got the Firefox View button");
+
+ // Initiate a synced tabs update with new tabs
+ syncedTabsMock.returns(tabsList1);
+ await initTabSync();
+
+ ok(
+ BrowserTestUtils.is_visible(fxViewBtn),
+ "The Firefox View button is showing"
+ );
+
+ ok(
+ await waitForNotificationBadgeToBeHidden(fxViewBtn),
+ "The notification badge is not showing initially"
+ );
+
+ // Initiate a synced tabs update with new tabs
+ syncedTabsMock.returns(tabsList2);
+ await initTabSync();
+
+ ok(
+ await waitForNotificationBadgeToBeShowing(fxViewBtn),
+ "The notification badge is showing after first tab sync"
+ );
+
+ // check that switching to the firefoxviewtab removes the badge
+ fxViewBtn.click();
+
+ ok(
+ await waitForNotificationBadgeToBeHidden(fxViewBtn),
+ "The notification badge is not showing after going to Firefox View"
+ );
+
+ await BrowserTestUtils.waitForCondition(() => {
+ return SyncedTabs.syncTabs.calledOnce;
+ });
+
+ ok(SyncedTabs.syncTabs.calledOnce, "SyncedTabs.syncTabs() was called once");
+
+ syncedTabsMock.returns(tabsList1);
+ // Initiate a synced tabs update with new tabs
+ await initTabSync();
+
+ // The noti badge would show but we are on a Firefox View page so no need to show the noti badge
+ ok(
+ await waitForNotificationBadgeToBeHidden(fxViewBtn),
+ "The notification badge is not showing after tab sync while Firefox View is focused"
+ );
+
+ let newTab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser);
+ syncedTabsMock.returns(tabsList2);
+ await initTabSync();
+
+ ok(
+ await waitForNotificationBadgeToBeShowing(fxViewBtn),
+ "The notification badge is showing after navigation to a new tab"
+ );
+
+ // check that switching back to the Firefox View tab removes the badge
+ fxViewBtn.click();
+
+ ok(
+ await waitForNotificationBadgeToBeHidden(fxViewBtn),
+ "The notification badge is not showing after focusing the Firefox View tab"
+ );
+
+ await BrowserTestUtils.switchTab(win.gBrowser, newTab);
+
+ // Initiate a synced tabs update with no new tabs
+ await initTabSync();
+
+ ok(
+ await waitForNotificationBadgeToBeHidden(fxViewBtn),
+ "The notification badge is not showing after a tab sync with the same tabs"
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+
+ sandbox.restore();
+});
+
+/**
+ * Tests the notification badge with multiple windows
+ */
+add_task(async function testNotificationDotOnMultipleWindows() {
+ const sandbox = setupRecentDeviceListMocks();
+ const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
+
+ // Create a new window
+ let win1 = await BrowserTestUtils.openNewBrowserWindow();
+ await win1.delayedStartupPromise;
+ let fxViewBtn = win1.document.getElementById("firefox-view-button");
+ ok(fxViewBtn, "Got the Firefox View button");
+
+ syncedTabsMock.returns(tabsList1);
+ // Initiate a synced tabs update
+ await initTabSync();
+
+ // Create another window
+ let win2 = await BrowserTestUtils.openNewBrowserWindow();
+ await win2.delayedStartupPromise;
+ let fxViewBtn2 = win2.document.getElementById("firefox-view-button");
+
+ fxViewBtn2.click();
+
+ // Make sure the badge doesn't show on any window
+ ok(
+ await waitForNotificationBadgeToBeHidden(fxViewBtn),
+ "The notification badge is not showing in the inital window"
+ );
+ ok(
+ await waitForNotificationBadgeToBeHidden(fxViewBtn2),
+ "The notification badge is not showing in the second window"
+ );
+
+ // Minimize the window.
+ win2.minimize();
+
+ await TestUtils.waitForCondition(
+ () => !win2.gBrowser.selectedBrowser.docShellIsActive,
+ "Waiting for docshell to be marked as inactive after minimizing the window"
+ );
+
+ syncedTabsMock.returns(tabsList2);
+ info("Initiate a synced tabs update with new tabs");
+ await initTabSync();
+
+ // The badge will show because the View tab is minimized
+ // Make sure the badge shows on all windows
+ ok(
+ await waitForNotificationBadgeToBeShowing(fxViewBtn),
+ "The notification badge is showing in the initial window"
+ );
+ ok(
+ await waitForNotificationBadgeToBeShowing(fxViewBtn2),
+ "The notification badge is showing in the second window"
+ );
+
+ win2.restore();
+ await TestUtils.waitForCondition(
+ () => win2.gBrowser.selectedBrowser.docShellIsActive,
+ "Waiting for docshell to be marked as active after restoring the window"
+ );
+
+ await BrowserTestUtils.closeWindow(win1);
+ await BrowserTestUtils.closeWindow(win2);
+
+ sandbox.restore();
+});
+
+/**
+ * Tests the notification badge is in the correct spot and that the badge shows when opening a new window
+ * if another window is showing the badge
+ */
+add_task(async function testNotificationDotLocation() {
+ const sandbox = setupRecentDeviceListMocks();
+ const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
+
+ syncedTabsMock.returns(tabsList1);
+
+ let win1 = await BrowserTestUtils.openNewBrowserWindow();
+ let fxViewBtn = win1.document.getElementById("firefox-view-button");
+ ok(fxViewBtn, "Got the Firefox View button");
+
+ // Initiate a synced tabs update
+ await initTabSync();
+ syncedTabsMock.returns(tabsList2);
+ // Initiate another synced tabs update
+ await initTabSync();
+
+ ok(
+ await waitForNotificationBadgeToBeShowing(fxViewBtn),
+ "The notification badge is showing initially"
+ );
+
+ // Create a new window
+ let win2 = await BrowserTestUtils.openNewBrowserWindow();
+ await win2.delayedStartupPromise;
+
+ // Make sure the badge doesn't showing on the new window
+ let fxViewBtn2 = win2.document.getElementById("firefox-view-button");
+ ok(
+ await waitForNotificationBadgeToBeShowing(fxViewBtn2),
+ "The notification badge is showing in the second window after opening"
+ );
+
+ // Make sure the badge is below and center now
+ isnot(
+ getBackgroundPositionForElement(fxViewBtn),
+ BADGE_TOP_RIGHT,
+ "The notification badge is not showing in the top right in the initial window"
+ );
+ isnot(
+ getBackgroundPositionForElement(fxViewBtn2),
+ BADGE_TOP_RIGHT,
+ "The notification badge is not showing in the top right in the second window"
+ );
+
+ CustomizableUI.addWidgetToArea(
+ "firefox-view-button",
+ CustomizableUI.AREA_NAVBAR
+ );
+
+ // Make sure both windows still have the notification badge
+ ok(
+ await waitForNotificationBadgeToBeShowing(fxViewBtn),
+ "The notification badge is showing in the initial window"
+ );
+ ok(
+ await waitForNotificationBadgeToBeShowing(fxViewBtn2),
+ "The notification badge is showing in the second window"
+ );
+
+ // Make sure the badge is in the top right now
+ is(
+ getBackgroundPositionForElement(fxViewBtn),
+ BADGE_TOP_RIGHT,
+ "The notification badge is showing in the top right in the initial window"
+ );
+ is(
+ getBackgroundPositionForElement(fxViewBtn2),
+ BADGE_TOP_RIGHT,
+ "The notification badge is showing in the top right in the second window"
+ );
+
+ CustomizableUI.reset();
+ await BrowserTestUtils.closeWindow(win1);
+ await BrowserTestUtils.closeWindow(win2);
+
+ sandbox.restore();
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_recently_closed_tabs.js b/browser/components/firefoxview/tests/browser/browser_recently_closed_tabs.js
new file mode 100644
index 0000000000..988a576327
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_recently_closed_tabs.js
@@ -0,0 +1,798 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * The recently closed tab list is populated on a per-window basis.
+ *
+ * By default, the withFirefoxView helper opens a new window.
+ * When using this helper for the tests in this file, we pass a
+ * { win: window } option to skip that step and open fx view in
+ * the current window. This ensures that the add_new_tab, close_tab,
+ * and open_then_close functions are creating sessionstore entries
+ * associated with the correct window where the tests are run.
+ */
+
+ChromeUtils.defineESModuleGetters(globalThis, {
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+});
+
+const { TabsSetupFlowManager } = ChromeUtils.importESModule(
+ "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs"
+);
+
+const RECENTLY_CLOSED_EVENT = [
+ ["firefoxview", "entered", "firefoxview", undefined],
+ ["firefoxview", "recently_closed", "tabs", undefined],
+];
+
+const CLOSED_TABS_OPEN_EVENT = [
+ ["firefoxview", "closed_tabs_open", "tabs", "false"],
+];
+
+const RECENTLY_CLOSED_DISMISS_EVENT = [
+ ["firefoxview", "dismiss_closed_tab", "tabs", undefined],
+];
+
+async function add_new_tab(URL) {
+ let tab = BrowserTestUtils.addTab(gBrowser, URL);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ return tab;
+}
+
+async function close_tab(tab) {
+ const sessionStorePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab);
+ BrowserTestUtils.removeTab(tab);
+ await sessionStorePromise;
+}
+
+async function dismiss_tab(tab, content) {
+ info(`Dismissing tab ${tab.dataset.targetURI}`);
+ const closedObjectsChanged = () =>
+ TestUtils.topicObserved("sessionstore-closed-objects-changed");
+ let dismissButton = tab.querySelector(".closed-tab-li-dismiss");
+ EventUtils.synthesizeMouseAtCenter(dismissButton, {}, content);
+ await closedObjectsChanged();
+}
+
+add_task(async function test_empty_list() {
+ clearHistory();
+
+ await withFirefoxView({ win: window }, async browser => {
+ const { document } = browser.contentWindow;
+ let container = document.querySelector("#collapsible-tabs-container");
+ ok(
+ container.classList.contains("empty-container"),
+ "collapsible container should have correct styling when the list is empty"
+ );
+
+ testVisibility(browser, {
+ expectedVisible: {
+ "#recently-closed-tabs-placeholder": true,
+ "ol.closed-tabs-list": false,
+ },
+ });
+
+ const tab1 = await add_new_tab(URLs[0]);
+
+ await close_tab(tab1);
+
+ // The UI update happens asynchronously as we learn of the new closed tab.
+ await BrowserTestUtils.waitForMutationCondition(
+ container,
+ { attributeFilter: ["class"] },
+ () => !container.classList.contains("empty-container")
+ );
+ ok(
+ !container.classList.contains("empty-container"),
+ "collapsible container should have correct styling when the list is not empty"
+ );
+
+ testVisibility(browser, {
+ expectedVisible: {
+ "#recently-closed-tabs-placeholder": false,
+ "ol.closed-tabs-list": true,
+ },
+ });
+
+ is(
+ document.querySelector("ol.closed-tabs-list").children.length,
+ 1,
+ "recently-closed-tabs-list should have one list item"
+ );
+ });
+});
+
+add_task(async function test_list_ordering() {
+ Services.obs.notifyObservers(null, "browser:purge-session-history");
+ is(
+ SessionStore.getClosedTabCount(window),
+ 0,
+ "Closed tab count after purging session history"
+ );
+ await clearAllParentTelemetryEvents();
+
+ const closedObjectsChanged = () =>
+ TestUtils.topicObserved("sessionstore-closed-objects-changed");
+
+ const tab1 = await add_new_tab(URLs[0]);
+ const tab2 = await add_new_tab(URLs[1]);
+ const tab3 = await add_new_tab(URLs[2]);
+
+ gBrowser.selectedTab = tab3;
+
+ await close_tab(tab3);
+ await closedObjectsChanged();
+
+ await close_tab(tab2);
+ await closedObjectsChanged();
+
+ await close_tab(tab1);
+ await closedObjectsChanged();
+
+ await withFirefoxView({ win: window }, async browser => {
+ const { document } = browser.contentWindow;
+ const tabsList = document.querySelector("ol.closed-tabs-list");
+ await BrowserTestUtils.waitForMutationCondition(
+ tabsList,
+ { childList: true },
+ () => tabsList.children.length > 1
+ );
+
+ is(
+ document.querySelector("ol.closed-tabs-list").children.length,
+ 3,
+ "recently-closed-tabs-list should have three list items"
+ );
+
+ // check that the ordering is correct when user navigates to another tab, and then closes multiple tabs.
+ ok(
+ document
+ .querySelector("ol.closed-tabs-list")
+ .firstChild.textContent.includes("mochi.test"),
+ "first list item in recently-closed-tabs-list is in the correct order"
+ );
+
+ ok(
+ document
+ .querySelector("ol.closed-tabs-list")
+ .children[2].textContent.includes("example.net"),
+ "last list item in recently-closed-tabs-list is in the correct order"
+ );
+
+ let ele = document.querySelector("ol.closed-tabs-list").firstElementChild;
+ let uri = ele.getAttribute("data-target-u-r-i");
+ let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, uri);
+ ele.click();
+ await newTabPromise;
+
+ await TestUtils.waitForCondition(
+ () => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ ).parent;
+ return events && events.length >= 2;
+ },
+ "Waiting for entered and recently_closed firefoxview telemetry events.",
+ 200,
+ 100
+ );
+
+ TelemetryTestUtils.assertEvents(
+ RECENTLY_CLOSED_EVENT,
+ { category: "firefoxview" },
+ { clear: true, process: "parent" }
+ );
+
+ gBrowser.removeTab(gBrowser.selectedTab);
+
+ await clearAllParentTelemetryEvents();
+
+ await waitForElementVisible(
+ browser,
+ "#recently-closed-tabs-container > summary"
+ );
+ document.querySelector("#recently-closed-tabs-container > summary").click();
+
+ await TestUtils.waitForCondition(
+ () => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ ).parent;
+ return events && events.length >= 1;
+ },
+ "Waiting for closed_tabs_open firefoxview telemetry event.",
+ 200,
+ 100
+ );
+
+ TelemetryTestUtils.assertEvents(
+ CLOSED_TABS_OPEN_EVENT,
+ { category: "firefoxview" },
+ { clear: true, process: "parent" }
+ );
+ });
+});
+
+add_task(async function test_max_list_items() {
+ Services.obs.notifyObservers(null, "browser:purge-session-history");
+ is(
+ SessionStore.getClosedTabCount(window),
+ 0,
+ "Closed tab count after purging session history"
+ );
+
+ await open_then_close(URLs[0]);
+ await open_then_close(URLs[1]);
+ await open_then_close(URLs[2]);
+
+ // Seed the closed tabs count. We've assured that we've opened and
+ // closed at least three tabs because of the calls to open_then_close
+ // above.
+ let mockMaxTabsLength = 3;
+
+ await withFirefoxView({ win: window }, async browser => {
+ const { document } = browser.contentWindow;
+
+ // override this value for testing purposes
+ document.querySelector(
+ "recently-closed-tabs-list"
+ ).maxTabsLength = mockMaxTabsLength;
+
+ ok(
+ !document
+ .querySelector("#collapsible-tabs-container")
+ .classList.contains("empty-container"),
+ "collapsible container should have correct styling when the list is not empty"
+ );
+
+ testVisibility(browser, {
+ expectedVisible: {
+ "#recently-closed-tabs-placeholder": false,
+ "ol.closed-tabs-list": true,
+ },
+ });
+
+ is(
+ document.querySelector("ol.closed-tabs-list").childNodes.length,
+ mockMaxTabsLength,
+ `recently-closed-tabs-list should have ${mockMaxTabsLength} list items`
+ );
+
+ const closedObjectsChanged = TestUtils.topicObserved(
+ "sessionstore-closed-objects-changed"
+ );
+ // add another tab
+ const tab = await add_new_tab(URLs[3]);
+ await close_tab(tab);
+ await closedObjectsChanged;
+
+ let firstListItem = document.querySelector("ol.closed-tabs-list")
+ .firstChild;
+ await BrowserTestUtils.waitForMutationCondition(
+ firstListItem,
+ { characterData: true, childList: true, subtree: true },
+ () => firstListItem.textContent.includes(".org")
+ );
+ ok(
+ firstListItem.textContent.includes("example.org"),
+ "first list item in recently-closed-tabs-list should have been updated"
+ );
+
+ is(
+ document.querySelector("ol.closed-tabs-list").childNodes.length,
+ mockMaxTabsLength,
+ `recently-closed-tabs-list should still have ${mockMaxTabsLength} list items`
+ );
+ });
+});
+
+add_task(async function test_time_updates_correctly() {
+ clearHistory();
+ is(
+ SessionStore.getClosedTabCount(window),
+ 0,
+ "Closed tab count after purging session history"
+ );
+
+ // Set the closed tabs state to include one tab that was closed 2 seconds ago.
+ // This is well below the initial threshold for displaying the 'Just now' timestamp.
+ // It is also much greater than the 5ms threshold we use for the updated pref value,
+ // which results in the timestamp text changing after the pref value is changed.
+ const TAB_CLOSED_AGO_MS = 2000;
+ const TAB_UPDATE_TIME_MS = 5;
+ const TAB_CLOSED_STATE = {
+ windows: [
+ {
+ tabs: [{ entries: [] }],
+ _closedTabs: [
+ {
+ state: { entries: [{ url: "https://www.example.com/" }] },
+ closedId: 0,
+ closedAt: Date.now() - TAB_CLOSED_AGO_MS,
+ image: null,
+ },
+ ],
+ },
+ ],
+ };
+ await SessionStore.setBrowserState(JSON.stringify(TAB_CLOSED_STATE));
+
+ is(
+ SessionStore.getClosedTabCount(window),
+ 1,
+ "Closed tab count after setting browser state"
+ );
+
+ await withFirefoxView(
+ {
+ win: window,
+ },
+ async browser => {
+ const { document } = browser.contentWindow;
+
+ const lastListItem = document.querySelector("ol.closed-tabs-list")
+ .lastChild;
+ const timeLabel = lastListItem.querySelector("span.closed-tab-li-time");
+ let initialTimeText = timeLabel.textContent;
+ Assert.stringContains(
+ initialTimeText,
+ "Just now",
+ "recently-closed-tabs list item time is 'Just now'"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.firefox-view.updateTimeMs", TAB_UPDATE_TIME_MS]],
+ });
+
+ await BrowserTestUtils.waitForMutationCondition(
+ timeLabel,
+ { childList: true },
+ () => !timeLabel.textContent.includes("now")
+ );
+
+ isnot(
+ timeLabel.textContent,
+ initialTimeText,
+ "recently-closed-tabs list item time has updated"
+ );
+
+ await SpecialPowers.popPrefEnv();
+ }
+ );
+ // Cleanup recently closed tab data.
+ clearHistory();
+});
+
+add_task(async function test_list_maintains_focus_when_restoring_tab() {
+ await SpecialPowers.clearUserPref(RECENTLY_CLOSED_STATE_PREF);
+ Services.obs.notifyObservers(null, "browser:purge-session-history");
+ is(
+ SessionStore.getClosedTabCount(window),
+ 0,
+ "Closed tab count after purging session history"
+ );
+
+ const sandbox = sinon.createSandbox();
+ let setupCompleteStub = sandbox.stub(
+ TabsSetupFlowManager,
+ "isTabSyncSetupComplete"
+ );
+ setupCompleteStub.returns(true);
+
+ await open_then_close(URLs[0]);
+ await open_then_close(URLs[1]);
+ await open_then_close(URLs[2]);
+
+ await withFirefoxView({ win: window }, async browser => {
+ let gBrowser = browser.getTabBrowser();
+ const { document } = browser.contentWindow;
+ const list = document.querySelectorAll(".closed-tab-li");
+ let expectedFocusedElement = list[1].querySelector(".closed-tab-li-main");
+ list[0].querySelector(".closed-tab-li-main").focus();
+ EventUtils.synthesizeKey("KEY_Enter");
+ let firefoxViewTab = gBrowser.tabs.find(tab => tab.label == "Firefox View");
+ await BrowserTestUtils.switchTab(gBrowser, firefoxViewTab);
+ is(
+ document.activeElement,
+ expectedFocusedElement,
+ "Focus should be on the first item in the recently closed list"
+ );
+ });
+
+ // clean up extra tabs
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.tabs.at(-1));
+ }
+
+ clearHistory();
+ await open_then_close(URLs[2]);
+ await withFirefoxView({ win: window }, async browser => {
+ let gBrowser = browser.getTabBrowser();
+ const { document } = browser.contentWindow;
+ let expectedFocusedElement = document.getElementById(
+ "recently-closed-tabs-header-section"
+ );
+ const list = document.querySelectorAll(".closed-tab-li");
+ list[0].querySelector(".closed-tab-li-main").focus();
+
+ EventUtils.synthesizeKey("KEY_Enter");
+ let firefoxViewTab = gBrowser.tabs.find(tab => tab.label == "Firefox View");
+ await BrowserTestUtils.switchTab(gBrowser, firefoxViewTab);
+ is(
+ document.activeElement,
+ expectedFocusedElement,
+ "Focus should be on the section header"
+ );
+ });
+
+ // clean up extra tabs
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.tabs.at(-1));
+ }
+});
+
+add_task(async function test_switch_before_closing() {
+ clearHistory();
+
+ const INITIAL_URL = "https://example.org/iwilldisappear";
+ const FINAL_URL = "https://example.com/ishouldappear";
+ await withFirefoxView({ win: window }, async function(browser) {
+ let gBrowser = browser.getTabBrowser();
+ let newTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ INITIAL_URL
+ );
+ // Switch back to FxView:
+ await BrowserTestUtils.switchTab(
+ gBrowser,
+ gBrowser.getTabForBrowser(browser)
+ );
+ // Update the tab we opened to a different site:
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ newTab.linkedBrowser,
+ null,
+ FINAL_URL
+ );
+ BrowserTestUtils.loadURI(newTab.linkedBrowser, FINAL_URL);
+ await loadPromise;
+
+ // Close the added tab
+ BrowserTestUtils.removeTab(newTab);
+
+ const { document } = browser.contentWindow;
+ const tabsList = document.querySelector("ol.closed-tabs-list");
+ await BrowserTestUtils.waitForMutationCondition(
+ tabsList,
+ { childList: true },
+ () => !!tabsList.children.length
+ );
+ info("A tab appeared in the list, ensure it has the right URL.");
+ let urlBit = tabsList.firstElementChild.querySelector(".closed-tab-li-url");
+ await BrowserTestUtils.waitForMutationCondition(
+ urlBit,
+ { characterData: true, attributeFilter: ["title"] },
+ () => urlBit.textContent.includes(".com")
+ );
+ is(
+ urlBit.textContent,
+ "example.com",
+ "Item should end up with the correct URL."
+ );
+ });
+});
+
+add_task(async function test_alt_click_no_launch() {
+ Services.obs.notifyObservers(null, "browser:purge-session-history");
+ is(
+ SessionStore.getClosedTabCount(window),
+ 0,
+ "Closed tab count after purging session history"
+ );
+
+ await open_then_close(URLs[0]);
+
+ await withFirefoxView({ win: window }, async browser => {
+ let gBrowser = browser.getTabBrowser();
+ let originalTabsLength = gBrowser.tabs.length;
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ ".closed-tab-li",
+ { altKey: true },
+ browser
+ );
+
+ is(
+ gBrowser.tabs.length,
+ originalTabsLength,
+ `Opened tabs length should still be ${originalTabsLength}`
+ );
+ });
+});
+
+/**
+ * Asserts that tabs that have been recently closed can be
+ * restored by clicking on them, using the Enter key,
+ * and using the Space bar.
+ */
+add_task(async function test_restore_recently_closed_tabs() {
+ clearHistory();
+
+ await open_then_close(URLs[0]);
+ await open_then_close(URLs[1]);
+ await open_then_close(URLs[2]);
+
+ await EventUtils.synthesizeMouseAtCenter(
+ gBrowser.ownerDocument.getElementById("firefox-view-button"),
+ { type: "mousedown" },
+ window
+ );
+ // Wait for Firefox View to be loaded before interacting
+ // with the page.
+ await BrowserTestUtils.browserLoaded(
+ window.FirefoxViewHandler.tab.linkedBrowser
+ );
+ let { document } = gBrowser.contentWindow;
+ let tabRestored = BrowserTestUtils.waitForNewTab(gBrowser, URLs[2]);
+ EventUtils.synthesizeMouseAtCenter(
+ document.querySelector(".closed-tab-li"),
+ {},
+ gBrowser.contentWindow
+ );
+
+ await tabRestored;
+ ok(true, "Tab was restored by mouse click");
+
+ await EventUtils.synthesizeMouseAtCenter(
+ gBrowser.ownerDocument.getElementById("firefox-view-button"),
+ { type: "mousedown" },
+ window
+ );
+
+ tabRestored = BrowserTestUtils.waitForNewTab(gBrowser, URLs[1]);
+ document.querySelector(".closed-tab-li .closed-tab-li-main").focus();
+ EventUtils.synthesizeKey("KEY_Enter", {}, gBrowser.contentWindow);
+
+ await tabRestored;
+ ok(true, "Tab was restored by using the Enter key");
+
+ await EventUtils.synthesizeMouseAtCenter(
+ gBrowser.ownerDocument.getElementById("firefox-view-button"),
+ { type: "mousedown" },
+ window
+ );
+
+ tabRestored = BrowserTestUtils.waitForNewTab(gBrowser, URLs[0]);
+ document.querySelector(".closed-tab-li .closed-tab-li-main").focus();
+ EventUtils.synthesizeKey(" ", {}, gBrowser.contentWindow);
+
+ await tabRestored;
+ ok(true, "Tab was restored by using the Space bar");
+
+ // clean up extra tabs
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.tabs.at(-1));
+ }
+});
+
+/**
+ * Asserts that tabs are removed from Recently Closed tabs in
+ * Fx View when tabs are removed from latest closed tab data.
+ * Ex: Selecting "Reopen Closed Tab" from the tabs toolbar
+ * context menu
+ */
+add_task(async function test_reopen_recently_closed_tabs() {
+ clearHistory();
+
+ await open_then_close(URLs[0]);
+ await open_then_close(URLs[1]);
+ await open_then_close(URLs[2]);
+
+ await EventUtils.synthesizeMouseAtCenter(
+ gBrowser.ownerDocument.getElementById("firefox-view-button"),
+ { type: "mousedown" },
+ window
+ );
+ // Wait for Firefox View to be loaded before interacting
+ // with the page.
+ await BrowserTestUtils.browserLoaded(
+ window.FirefoxViewHandler.tab.linkedBrowser
+ );
+
+ let { document } = gBrowser.contentWindow;
+
+ let tabReopened = BrowserTestUtils.waitForNewTab(gBrowser, URLs[2]);
+ SessionStore.undoCloseTab(window);
+ await tabReopened;
+
+ const tabsList = document.querySelector("ol.closed-tabs-list");
+
+ await EventUtils.synthesizeMouseAtCenter(
+ gBrowser.ownerDocument.getElementById("firefox-view-button"),
+ { type: "mousedown" },
+ window
+ );
+
+ await BrowserTestUtils.waitForMutationCondition(
+ tabsList,
+ { childList: true },
+ () => tabsList.children.length === 2
+ );
+
+ Assert.equal(
+ tabsList.children[0].dataset.targetURI,
+ URLs[1],
+ `First recently closed item should be ${URLs[1]}`
+ );
+
+ await close_tab(gBrowser.visibleTabs[gBrowser.visibleTabs.length - 1]);
+
+ await BrowserTestUtils.waitForMutationCondition(
+ tabsList,
+ { childList: true },
+ () => tabsList.children.length === 3
+ );
+
+ Assert.equal(
+ tabsList.children[0].dataset.targetURI,
+ URLs[2],
+ `First recently closed item should be ${URLs[2]}`
+ );
+
+ await dismiss_tab(tabsList.children[0], content);
+
+ await BrowserTestUtils.waitForMutationCondition(
+ tabsList,
+ { childList: true },
+ () => tabsList.children.length === 2
+ );
+
+ Assert.equal(
+ tabsList.children[0].dataset.targetURI,
+ URLs[1],
+ `First recently closed item should be ${URLs[1]}`
+ );
+
+ // clean up extra tabs
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.tabs.at(-1));
+ }
+});
+
+/**
+ * Asserts that tabs that have been recently closed can be
+ * dismissed by clicking on their respective dismiss buttons.
+ */
+add_task(async function test_dismiss_tab() {
+ Services.obs.notifyObservers(null, "browser:purge-session-history");
+ Assert.equal(
+ SessionStore.getClosedTabCount(window),
+ 0,
+ "Closed tab count after purging session history"
+ );
+ await clearAllParentTelemetryEvents();
+
+ await withFirefoxView({ win: window }, async browser => {
+ const { document } = browser.contentWindow;
+
+ const closedObjectsChanged = () =>
+ TestUtils.topicObserved("sessionstore-closed-objects-changed");
+
+ const tab1 = await add_new_tab(URLs[0]);
+ const tab2 = await add_new_tab(URLs[1]);
+ const tab3 = await add_new_tab(URLs[2]);
+
+ await close_tab(tab3);
+ await closedObjectsChanged();
+
+ await close_tab(tab2);
+ await closedObjectsChanged();
+
+ await close_tab(tab1);
+ await closedObjectsChanged();
+
+ const tabsList = document.querySelector("ol.closed-tabs-list");
+
+ await clearAllParentTelemetryEvents();
+
+ await dismiss_tab(tabsList.children[0], content);
+
+ await TestUtils.waitForCondition(
+ () => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ ).parent;
+ return events && events.length >= 1;
+ },
+ "Waiting for dismiss_closed_tab firefoxview telemetry event.",
+ 200,
+ 100
+ );
+
+ TelemetryTestUtils.assertEvents(
+ RECENTLY_CLOSED_DISMISS_EVENT,
+ { category: "firefoxview" },
+ { clear: true, process: "parent" }
+ );
+
+ Assert.equal(
+ tabsList.children[0].dataset.targetURI,
+ URLs[1],
+ `First recently closed item should be ${URLs[1]}`
+ );
+
+ Assert.equal(
+ tabsList.children.length,
+ 2,
+ "recently-closed-tabs-list should have two list items"
+ );
+
+ await clearAllParentTelemetryEvents();
+
+ await dismiss_tab(tabsList.children[0], content);
+
+ await TestUtils.waitForCondition(
+ () => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ ).parent;
+ return events && events.length >= 1;
+ },
+ "Waiting for dismiss_closed_tab firefoxview telemetry event.",
+ 200,
+ 100
+ );
+
+ TelemetryTestUtils.assertEvents(
+ RECENTLY_CLOSED_DISMISS_EVENT,
+ { category: "firefoxview" },
+ { clear: true, process: "parent" }
+ );
+
+ Assert.equal(
+ tabsList.children[0].dataset.targetURI,
+ URLs[2],
+ `First recently closed item should be ${URLs[2]}`
+ );
+
+ Assert.equal(
+ tabsList.children.length,
+ 1,
+ "recently-closed-tabs-list should have one list item"
+ );
+
+ await clearAllParentTelemetryEvents();
+
+ await dismiss_tab(tabsList.children[0], content);
+
+ await TestUtils.waitForCondition(
+ () => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ ).parent;
+ return events && events.length >= 1;
+ },
+ "Waiting for dismiss_closed_tab firefoxview telemetry event.",
+ 200,
+ 100
+ );
+
+ TelemetryTestUtils.assertEvents(
+ RECENTLY_CLOSED_DISMISS_EVENT,
+ { category: "firefoxview" },
+ { clear: true, process: "parent" }
+ );
+
+ testVisibility(browser, {
+ expectedVisible: {
+ "#recently-closed-tabs-placeholder": true,
+ "ol.closed-tabs-list": false,
+ },
+ });
+ });
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_recently_closed_tabs_keyboard.js b/browser/components/firefoxview/tests/browser/browser_recently_closed_tabs_keyboard.js
new file mode 100644
index 0000000000..2d495934df
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_recently_closed_tabs_keyboard.js
@@ -0,0 +1,255 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function dismiss_tab_keyboard(closedTab, document) {
+ const enter = () => {
+ info("Enter");
+ EventUtils.synthesizeKey("KEY_Enter");
+ };
+ const tab = (shiftKey = false) => {
+ info(`${shiftKey ? "Shift + Tab" : "Tab"}`);
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey });
+ };
+ const closedObjectsChanged = () =>
+ TestUtils.topicObserved("sessionstore-closed-objects-changed");
+ let firstTabMainContent = closedTab.querySelector(".closed-tab-li-main");
+ let dismissButton = closedTab.querySelector(".closed-tab-li-dismiss");
+ firstTabMainContent.focus();
+ tab();
+ Assert.equal(
+ document.activeElement,
+ dismissButton,
+ "Focus should be on the dismiss button for the first item in the recently closed list"
+ );
+ enter();
+ await closedObjectsChanged();
+}
+
+/**
+ * Tests keyboard navigation of the recently closed tabs component
+ */
+add_task(async function test_keyboard_navigation() {
+ const enter = () => {
+ info("Enter");
+ EventUtils.synthesizeKey("KEY_Enter");
+ };
+ const tab = (shiftKey = false) => {
+ info(`${shiftKey ? "Shift + Tab" : "Tab"}`);
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey });
+ };
+ /**
+ * Focus the summary element and asserts that:
+ * - The recently closed details should be initially opened
+ * - The recently closed details can be opened and closed via the Enter key
+ *
+ * @param {Document} document The currently used browser's content window document
+ * @param {HTMLElement} summary The header section element for recently closed tabs
+ */
+ const assertPreconditions = (document, summary) => {
+ let details = document.getElementById("recently-closed-tabs-container");
+ ok(
+ details.open,
+ "Recently closed details should be initially open on load"
+ );
+ summary.focus();
+ enter();
+ ok(!details.open, "Recently closed details should be closed");
+ enter();
+ ok(details.open, "Recently closed details should be opened");
+ };
+ await SpecialPowers.clearUserPref(RECENTLY_CLOSED_STATE_PREF);
+ Services.obs.notifyObservers(null, "browser:purge-session-history");
+ is(
+ SessionStore.getClosedTabCount(window),
+ 0,
+ "Closed tab count after purging session history"
+ );
+
+ const sandbox = sinon.createSandbox();
+ let setupCompleteStub = sandbox.stub(
+ TabsSetupFlowManager,
+ "isTabSyncSetupComplete"
+ );
+ setupCompleteStub.returns(true);
+
+ await open_then_close(URLs[0]);
+
+ await withFirefoxView({ win: window }, async browser => {
+ const { document } = browser.contentWindow;
+ const list = document.querySelectorAll(".closed-tab-li");
+ let summary = document.getElementById(
+ "recently-closed-tabs-header-section"
+ );
+
+ assertPreconditions(document, summary);
+ tab();
+
+ ok(
+ list[0].querySelector(".closed-tab-li-main").matches(":focus"),
+ "The first link is focused"
+ );
+
+ tab(true);
+ ok(
+ summary.matches(":focus"),
+ "The container is focused when using shift+tab in the list"
+ );
+ });
+ // clean up extra tabs
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.tabs.at(-1));
+ }
+
+ clearHistory();
+
+ await open_then_close(URLs[0]);
+ await open_then_close(URLs[1]);
+
+ await withFirefoxView({ win: window }, async browser => {
+ const { document } = browser.contentWindow;
+ const list = document.querySelectorAll(".closed-tab-li");
+ let summary = document.getElementById(
+ "recently-closed-tabs-header-section"
+ );
+ assertPreconditions(document, summary);
+
+ tab();
+
+ ok(
+ list[0].querySelector(".closed-tab-li-main").matches(":focus"),
+ "The first link is focused"
+ );
+ tab();
+ tab();
+ ok(
+ list[1].querySelector(".closed-tab-li-main").matches(":focus"),
+ "The second link is focused"
+ );
+ tab(true);
+ tab(true);
+ ok(
+ list[0].querySelector(".closed-tab-li-main").matches(":focus"),
+ "The first link is focused again"
+ );
+
+ tab(true);
+ ok(
+ summary.matches(":focus"),
+ "The container is focused when using shift+tab in the list"
+ );
+ });
+
+ // clean up extra tabs
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.tabs.at(-1));
+ }
+
+ clearHistory();
+
+ await open_then_close(URLs[0]);
+ await open_then_close(URLs[1]);
+ await open_then_close(URLs[2]);
+
+ await withFirefoxView({ win: window }, async browser => {
+ const { document } = browser.contentWindow;
+ const list = document.querySelectorAll(".closed-tab-li");
+ let summary = document.getElementById(
+ "recently-closed-tabs-header-section"
+ );
+ assertPreconditions(document, summary);
+
+ tab();
+
+ ok(
+ list[0].querySelector(".closed-tab-li-main").matches(":focus"),
+ "The first link is focused"
+ );
+ tab();
+ tab();
+ ok(
+ list[1].querySelector(".closed-tab-li-main").matches(":focus"),
+ "The second link is focused"
+ );
+ tab();
+ tab();
+ ok(
+ list[2].querySelector(".closed-tab-li-main").matches(":focus"),
+ "The third link is focused"
+ );
+ tab(true);
+ tab(true);
+ ok(
+ list[1].querySelector(".closed-tab-li-main").matches(":focus"),
+ "The second link is focused"
+ );
+ tab(true);
+ tab(true);
+ ok(
+ list[0].querySelector(".closed-tab-li-main").matches(":focus"),
+ "The first link is focused"
+ );
+ });
+});
+
+add_task(async function test_dismiss_tab_keyboard() {
+ Services.obs.notifyObservers(null, "browser:purge-session-history");
+ Assert.equal(
+ SessionStore.getClosedTabCount(window),
+ 0,
+ "Closed tab count after purging session history"
+ );
+ await withFirefoxView({ win: window }, async browser => {
+ const { document } = browser.contentWindow;
+
+ await open_then_close(URLs[0]);
+ await open_then_close(URLs[1]);
+ await open_then_close(URLs[2]);
+
+ await EventUtils.synthesizeMouseAtCenter(
+ gBrowser.ownerDocument.getElementById("firefox-view-button"),
+ { type: "mousedown" },
+ window
+ );
+
+ const tabsList = document.querySelector("ol.closed-tabs-list");
+
+ await dismiss_tab_keyboard(tabsList.children[0], document);
+
+ Assert.equal(
+ tabsList.children[0].dataset.targetURI,
+ URLs[1],
+ `First recently closed item should be ${URLs[1]}`
+ );
+
+ Assert.equal(
+ tabsList.children.length,
+ 2,
+ "recently-closed-tabs-list should have two list items"
+ );
+
+ await dismiss_tab_keyboard(tabsList.children[0], document);
+
+ Assert.equal(
+ tabsList.children[0].dataset.targetURI,
+ URLs[0],
+ `First recently closed item should be ${URLs[0]}`
+ );
+
+ Assert.equal(
+ tabsList.children.length,
+ 1,
+ "recently-closed-tabs-list should have one list item"
+ );
+
+ await dismiss_tab_keyboard(tabsList.children[0], document);
+
+ testVisibility(browser, {
+ expectedVisible: {
+ "#recently-closed-tabs-placeholder": true,
+ "ol.closed-tabs-list": false,
+ },
+ });
+ });
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_reload_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_reload_firefoxview.js
new file mode 100644
index 0000000000..f9a226bbf2
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_reload_firefoxview.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ Ensures that the Firefox View tab can be reloaded via:
+ - Clicking the Refresh button in the toolbar
+ - Using the various keyboard shortcuts
+*/
+add_task(async function test_reload_firefoxview() {
+ await withFirefoxView({}, async browser => {
+ let reloadButton = document.getElementById("reload-button");
+ let tabLoaded = BrowserTestUtils.browserLoaded(browser);
+ EventUtils.synthesizeMouseAtCenter(reloadButton, {}, browser.ownerGlobal);
+ await tabLoaded;
+ ok(true, "Firefox View loaded after clicking the Reload button");
+
+ let keys = [
+ ["R", { accelKey: true }],
+ ["R", { accelKey: true, shift: true }],
+ ["VK_F5", {}],
+ ];
+
+ if (AppConstants.platform != "macosx") {
+ keys.push(["VK_F5", { accelKey: true }]);
+ }
+
+ for (let key of keys) {
+ tabLoaded = BrowserTestUtils.browserLoaded(browser);
+ EventUtils.synthesizeKey(key[0], key[1], browser.ownerGlobal);
+ await tabLoaded;
+ ok(true, `Firefox view loaded after using ${key}`);
+ }
+ });
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_setup_errors.js b/browser/components/firefoxview/tests/browser/browser_setup_errors.js
new file mode 100644
index 0000000000..4929e93600
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_setup_errors.js
@@ -0,0 +1,374 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { TabsSetupFlowManager } = ChromeUtils.importESModule(
+ "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs"
+);
+
+const { LoginTestUtils } = ChromeUtils.import(
+ "resource://testing-common/LoginTestUtils.jsm"
+);
+
+async function setupWithDesktopDevices(state = UIState.STATUS_SIGNED_IN) {
+ const sandbox = setupSyncFxAMocks({
+ state,
+ fxaDevices: [
+ {
+ id: 1,
+ name: "This Device",
+ isCurrentDevice: true,
+ type: "desktop",
+ },
+ {
+ id: 2,
+ name: "Other Device",
+ type: "desktop",
+ },
+ ],
+ });
+ return sandbox;
+}
+
+async function tearDown(sandbox) {
+ sandbox?.restore();
+ Services.prefs.clearUserPref("services.sync.lastTabFetch");
+ Services.prefs.clearUserPref("identity.fxaccounts.enabled");
+}
+
+add_setup(async function() {
+ // gSync.init() is called in a requestIdleCallback. Force its initialization.
+ gSync.init();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["services.sync.engine.tabs", true],
+ ["identity.fxaccounts.enabled", true],
+ ],
+ });
+
+ registerCleanupFunction(async function() {
+ // reset internal state so it doesn't affect the next tests
+ TabsSetupFlowManager.resetInternalState();
+ await tearDown(gSandbox);
+ });
+});
+
+add_task(async function test_network_offline() {
+ const sandbox = await setupWithDesktopDevices();
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+ Services.obs.notifyObservers(
+ null,
+ "network:offline-status-changed",
+ "offline"
+ );
+ await waitForElementVisible(browser, "#tabpickup-steps", true);
+ await waitForVisibleSetupStep(browser, {
+ expectedVisible: "#tabpickup-steps-view0",
+ });
+
+ const errorStateHeader = document.querySelector(
+ "#tabpickup-steps-view0-header"
+ );
+
+ await BrowserTestUtils.waitForMutationCondition(
+ errorStateHeader,
+ { childList: true },
+ () => errorStateHeader.textContent.includes("connection")
+ );
+
+ ok(
+ errorStateHeader.getAttribute("data-l10n-id").includes("network-offline"),
+ "Correct message should show when network connection is lost"
+ );
+
+ Services.obs.notifyObservers(
+ null,
+ "network:offline-status-changed",
+ "online"
+ );
+
+ await waitForElementVisible(browser, "#tabpickup-tabs-container", true);
+ });
+ await tearDown(sandbox);
+});
+
+add_task(async function test_sync_error() {
+ const sandbox = await setupWithDesktopDevices();
+ sandbox.spy(TabsSetupFlowManager, "tryToClearError");
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+ Services.obs.notifyObservers(null, "weave:service:sync:error");
+
+ await waitForElementVisible(browser, "#tabpickup-steps", true);
+ await waitForVisibleSetupStep(browser, {
+ expectedVisible: "#tabpickup-steps-view0",
+ });
+
+ const errorStateHeader = document.querySelector(
+ "#tabpickup-steps-view0-header"
+ );
+
+ await BrowserTestUtils.waitForMutationCondition(
+ errorStateHeader,
+ { childList: true },
+ () => errorStateHeader.textContent.includes("trouble syncing")
+ );
+
+ ok(
+ errorStateHeader.getAttribute("data-l10n-id").includes("sync-error"),
+ "Correct message should show when there's a sync service error"
+ );
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#error-state-button",
+ {},
+ browser
+ );
+
+ await BrowserTestUtils.waitForCondition(() => {
+ return TabsSetupFlowManager.tryToClearError.calledOnce;
+ });
+
+ ok(
+ TabsSetupFlowManager.tryToClearError.calledOnce,
+ "TabsSetupFlowManager.tryToClearError() was called once"
+ );
+
+ // Clear the error.
+ Services.obs.notifyObservers(null, "weave:service:sync:finish");
+ });
+
+ // Now reopen the tab and check that sending an error state does not
+ // start showing the error:
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+ const recentFetchTime = Math.floor(Date.now() / 1000);
+ info("updating lastFetch:" + recentFetchTime);
+ Services.prefs.setIntPref("services.sync.lastTabFetch", recentFetchTime);
+ await withFirefoxView({ resetFlowManager: false }, async browser => {
+ const { document } = browser.contentWindow;
+
+ await waitForElementVisible(browser, "#synced-tabs-placeholder", true);
+
+ Services.obs.notifyObservers(null, "weave:service:sync:error");
+ await TestUtils.waitForTick();
+ ok(
+ BrowserTestUtils.is_visible(
+ document.getElementById("synced-tabs-placeholder")
+ ),
+ "Should still be showing the placeholder content."
+ );
+ let stepHeader = document.getElementById("tabpickup-steps-view0-header");
+ ok(
+ !stepHeader || BrowserTestUtils.is_hidden(stepHeader),
+ "Should not be showing an error state if we had previously synced successfully."
+ );
+
+ // Now drop a device:
+ let someDevice = gMockFxaDevices.pop();
+ Services.obs.notifyObservers(null, "fxaccounts:devicelist_updated");
+ // This will trip a UI update where we decide we can't rely on
+ // previously synced tabs anymore (they may be from the device
+ // that was removed!), so we still show an error:
+
+ await waitForElementVisible(browser, "#tabpickup-steps", true);
+ await waitForVisibleSetupStep(browser, {
+ expectedVisible: "#tabpickup-steps-view0",
+ });
+
+ const errorStateHeader = document.querySelector(
+ "#tabpickup-steps-view0-header"
+ );
+
+ await BrowserTestUtils.waitForMutationCondition(
+ errorStateHeader,
+ { childList: true },
+ () => errorStateHeader.textContent.includes("trouble syncing")
+ );
+
+ ok(
+ errorStateHeader.getAttribute("data-l10n-id").includes("sync-error"),
+ "Correct message should show when there's an error and tab information is outdated."
+ );
+
+ // Sneak device back in so as not to break other tests:
+ gMockFxaDevices.push(someDevice);
+ // Clear the error.
+ Services.obs.notifyObservers(null, "weave:service:sync:finish");
+ });
+ Services.prefs.clearUserPref("services.sync.lastTabFetch");
+
+ await tearDown(sandbox);
+});
+
+add_task(async function test_sync_error_signed_out() {
+ // sync error should not show if user is not signed in
+ let sandbox = await setupWithDesktopDevices(UIState.STATUS_NOT_CONFIGURED);
+ await withFirefoxView({}, async browser => {
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+ Services.obs.notifyObservers(null, "weave:service:sync:error");
+
+ await waitForElementVisible(browser, "#tabpickup-steps", true);
+ await waitForVisibleSetupStep(browser, {
+ expectedVisible: "#tabpickup-steps-view1",
+ });
+ });
+ await tearDown(sandbox);
+});
+
+add_task(async function test_sync_disconnected_error() {
+ // it's possible for fxa to be enabled but sync not enabled.
+ const sandbox = setupSyncFxAMocks({
+ state: UIState.STATUS_SIGNED_IN,
+ syncEnabled: false,
+ });
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+
+ // triggered when user disconnects sync in about:preferences
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ await waitForElementVisible(browser, "#tabpickup-steps", true);
+ info("Waiting for the tabpickup error step to be visible");
+ await waitForVisibleSetupStep(browser, {
+ expectedVisible: "#tabpickup-steps-view0",
+ });
+
+ const errorStateHeader = document.querySelector(
+ "#tabpickup-steps-view0-header"
+ );
+
+ info(
+ "Waiting for a mutation condition to ensure the right syncing error message"
+ );
+ await BrowserTestUtils.waitForMutationCondition(
+ errorStateHeader,
+ { childList: true },
+ () => errorStateHeader.textContent.includes("Turn on syncing to continue")
+ );
+
+ ok(
+ errorStateHeader
+ .getAttribute("data-l10n-id")
+ .includes("sync-disconnected"),
+ "Correct message should show when sync's been disconnected error"
+ );
+
+ let preferencesTabPromise = BrowserTestUtils.waitForNewTab(
+ browser.getTabBrowser(),
+ "about:preferences?action=choose-what-to-sync#sync",
+ true
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#error-state-button",
+ {},
+ browser
+ );
+ let preferencesTab = await preferencesTabPromise;
+ await BrowserTestUtils.removeTab(preferencesTab);
+ });
+ await tearDown(sandbox);
+});
+
+add_task(async function test_password_change_disconnect_error() {
+ // When the user changes their password on another device, we get into a state
+ // where the user is signed out but sync is still enabled.
+ const sandbox = setupSyncFxAMocks({
+ state: UIState.STATUS_LOGIN_FAILED,
+ syncEnabled: true,
+ });
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+
+ // triggered by the user changing fxa password on another device
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ await waitForElementVisible(browser, "#tabpickup-steps", true);
+ await waitForVisibleSetupStep(browser, {
+ expectedVisible: "#tabpickup-steps-view0",
+ });
+
+ const errorStateHeader = document.querySelector(
+ "#tabpickup-steps-view0-header"
+ );
+
+ await BrowserTestUtils.waitForMutationCondition(
+ errorStateHeader,
+ { childList: true },
+ () => errorStateHeader.textContent.includes("Sign in to reconnect")
+ );
+
+ ok(
+ errorStateHeader.getAttribute("data-l10n-id").includes("signed-out"),
+ "Correct message should show when user has been logged out due to external password change."
+ );
+ });
+ await tearDown(sandbox);
+});
+
+add_task(async function test_multiple_errors() {
+ let sandbox = await setupWithDesktopDevices();
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ // Simulate conditions in which both the locked password and sync error
+ // messages could be shown
+ LoginTestUtils.primaryPassword.enable();
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+ Services.obs.notifyObservers(null, "weave:service:sync:error");
+
+ info("Waiting for the primary password error message to be shown");
+ await waitForElementVisible(browser, "#tabpickup-steps", true);
+ await waitForVisibleSetupStep(browser, {
+ expectedVisible: "#tabpickup-steps-view0",
+ });
+
+ const errorStateHeader = document.querySelector(
+ "#tabpickup-steps-view0-header"
+ );
+ await BrowserTestUtils.waitForMutationCondition(
+ errorStateHeader,
+ { childList: true },
+ () => errorStateHeader.textContent.includes("Enter your Primary Password")
+ );
+
+ ok(
+ errorStateHeader.getAttribute("data-l10n-id").includes("password-locked"),
+ "Password locked error message is shown"
+ );
+
+ const errorLink = document.querySelector("#error-state-link");
+ ok(
+ errorLink && BrowserTestUtils.is_visible(errorLink),
+ "Error link is visible"
+ );
+
+ // Clear the primary password error message
+ LoginTestUtils.primaryPassword.disable();
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ info("Waiting for the sync error message to be shown");
+ await BrowserTestUtils.waitForMutationCondition(
+ errorStateHeader,
+ { childList: true },
+ () => errorStateHeader.textContent.includes("trouble syncing")
+ );
+
+ ok(
+ errorStateHeader.getAttribute("data-l10n-id").includes("sync-error"),
+ "Sync error message is now shown"
+ );
+
+ ok(
+ errorLink && BrowserTestUtils.is_hidden(errorLink),
+ "Error link is now hidden"
+ );
+
+ // Clear the sync error
+ Services.obs.notifyObservers(null, "weave:service:sync:finish");
+ });
+ await tearDown(sandbox);
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_setup_primary_password.js b/browser/components/firefoxview/tests/browser/browser_setup_primary_password.js
new file mode 100644
index 0000000000..655ecf1e6f
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_setup_primary_password.js
@@ -0,0 +1,150 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { TabsSetupFlowManager } = ChromeUtils.importESModule(
+ "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs"
+);
+
+const { LoginTestUtils } = ChromeUtils.import(
+ "resource://testing-common/LoginTestUtils.jsm"
+);
+
+async function tearDown(sandbox) {
+ sandbox?.restore();
+ Services.prefs.clearUserPref("services.sync.lastTabFetch");
+}
+
+function setupMocks() {
+ const sandbox = (gSandbox = setupRecentDeviceListMocks());
+ return sandbox;
+}
+
+add_setup(async function() {
+ registerCleanupFunction(async () => {
+ // reset internal state so it doesn't affect the next tests
+ TabsSetupFlowManager.resetInternalState();
+ LoginTestUtils.primaryPassword.disable();
+ await tearDown(gSandbox);
+ });
+ await SpecialPowers.pushPrefEnv({
+ set: [["services.sync.username", "username@example.com"]],
+ });
+});
+
+add_task(async function test_primary_password_locked() {
+ LoginTestUtils.primaryPassword.enable();
+ const sandbox = setupMocks();
+
+ await withFirefoxView({}, async browser => {
+ sandbox
+ .stub(TabsSetupFlowManager, "syncTabs")
+ .returns(Promise.resolve(null));
+ sandbox.stub(TabsSetupFlowManager, "startFullTabsSync").returns(undefined);
+
+ const { document } = browser.contentWindow;
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ info("waiting for the error setup step to be visible");
+ await waitForVisibleSetupStep(browser, {
+ expectedVisible: "#tabpickup-steps-view0",
+ });
+
+ const errorStateHeader = document.querySelector(
+ "#tabpickup-steps-view0-header"
+ );
+ await BrowserTestUtils.waitForMutationCondition(
+ errorStateHeader,
+ { childList: true },
+ () => errorStateHeader.textContent.includes("Enter your Primary Password")
+ );
+
+ ok(
+ errorStateHeader.getAttribute("data-l10n-id").includes("password-locked"),
+ "Correct error message is shown"
+ );
+
+ const errorLink = document.querySelector("#error-state-link");
+ ok(
+ errorLink && BrowserTestUtils.is_visible(errorLink),
+ "Error link is visible"
+ );
+ ok(
+ errorLink.getAttribute("data-l10n-id").includes("password-locked-link"),
+ "Correct link text is shown"
+ );
+
+ const primaryButton = document.querySelector("#error-state-button");
+ ok(
+ primaryButton && BrowserTestUtils.is_visible(primaryButton),
+ "Error primary button is visible"
+ );
+
+ const clearErrorStub = sandbox.stub(
+ TabsSetupFlowManager,
+ "tryToClearError"
+ );
+ info("Setup state:" + TabsSetupFlowManager.currentSetupState.name);
+
+ info("clicking the error panel button");
+ primaryButton.click();
+ ok(
+ clearErrorStub.called,
+ "tryToClearError was called when the try-again button was clicked"
+ );
+ TabsSetupFlowManager.tryToClearError.restore();
+
+ info("Clearing the primary password");
+ LoginTestUtils.primaryPassword.disable();
+ ok(
+ !TabsSetupFlowManager.isPrimaryPasswordLocked,
+ "primary password is unlocked"
+ );
+
+ info("notifying of the primary-password unlock");
+ const clearErrorSpy = sandbox.spy(TabsSetupFlowManager, "tryToClearError");
+ // we stubbed out sync, so pretend it ran.
+ info("notifying of sync:finish");
+ Services.obs.notifyObservers(null, "weave:service:sync:finish");
+
+ const setupContainer = document.querySelector(".sync-setup-container");
+ // wait until the setup container gets hidden before checking if the tabs container is visible
+ // as it may not exist until then
+ let setupHiddenPromise = BrowserTestUtils.waitForMutationCondition(
+ setupContainer,
+ {
+ attributeFilter: ["hidden"],
+ },
+ () => {
+ return BrowserTestUtils.is_hidden(setupContainer);
+ }
+ );
+
+ Services.obs.notifyObservers(null, "passwordmgr-crypto-login");
+ await setupHiddenPromise;
+ ok(
+ clearErrorSpy.called,
+ "tryToClearError was called when the primary-password unlock notification was received"
+ );
+ // We expect the waiting state until we get a sync update/finished
+ info("Setup state:" + TabsSetupFlowManager.currentSetupState.name);
+
+ ok(TabsSetupFlowManager.waitingForTabs, "Now waiting for tabs");
+ ok(
+ document
+ .querySelector("#tabpickup-tabs-container")
+ .classList.contains("loading"),
+ "Synced tabs container has loading class"
+ );
+
+ info("notifying of sync:finish");
+ Services.obs.notifyObservers(null, "weave:service:sync:finish");
+ await TestUtils.waitForTick();
+ ok(
+ !document
+ .querySelector("#tabpickup-tabs-container")
+ .classList.contains("loading"),
+ "Synced tabs isn't loading any more"
+ );
+ });
+ await tearDown(sandbox);
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_setup_state.js b/browser/components/firefoxview/tests/browser/browser_setup_state.js
new file mode 100644
index 0000000000..2e4921b4bf
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_setup_state.js
@@ -0,0 +1,769 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { TabsSetupFlowManager } = ChromeUtils.importESModule(
+ "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs"
+);
+
+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({}, 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({}, 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({ win: window }, 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.popPrefEnv();
+ 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")
+ );
+
+ let win2 = await BrowserTestUtils.openNewBrowserWindow();
+ info("Got window, now opening Firefox View in it");
+ await withFirefoxView(
+ { resetFlowManager: false, win: win2 },
+ 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, win2);
+ BrowserTestUtils.is_hidden(confirmBox);
+
+ for (let fxviewBrowser of [browser, win2Browser]) {
+ checkMobilePromo(fxviewBrowser, {
+ mobilePromo: false,
+ mobileConfirmation: false,
+ });
+ }
+ }
+ );
+ await BrowserTestUtils.closeWindow(win2);
+ });
+ 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.loadURI(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 SpecialPowers.popPrefEnv();
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_setup_synced_tabs_loading.js b/browser/components/firefoxview/tests/browser/browser_setup_synced_tabs_loading.js
new file mode 100644
index 0000000000..63518e79c0
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_setup_synced_tabs_loading.js
@@ -0,0 +1,180 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { TabsSetupFlowManager } = ChromeUtils.importESModule(
+ "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs"
+);
+
+XPCOMUtils.defineLazyModuleGetters(globalThis, {
+ SyncedTabs: "resource://services-sync/SyncedTabs.jsm",
+});
+
+async function tearDown(sandbox) {
+ sandbox?.restore();
+ Services.prefs.clearUserPref("services.sync.lastTabFetch");
+}
+
+function checkLoadingState(browser, isLoading = false) {
+ const { document } = browser.contentWindow;
+ const tabsContainer = document.querySelector("#tabpickup-tabs-container");
+ const tabsList = document.querySelector(
+ "#tabpickup-tabs-container tab-pickup-list"
+ );
+ const loadingElem = document.querySelector(
+ "#tabpickup-tabs-container .loading-content"
+ );
+ const setupElem = document.querySelector("#tabpickup-steps");
+
+ if (isLoading) {
+ ok(
+ tabsContainer.classList.contains("loading"),
+ "Tabs container has loading class"
+ );
+ BrowserTestUtils.is_visible(
+ loadingElem,
+ "Loading content is visible when loading"
+ );
+ !tabsList ||
+ BrowserTestUtils.is_hidden(
+ tabsList,
+ "Synced tabs list is not visible when loading"
+ );
+ !setupElem ||
+ BrowserTestUtils.is_hidden(
+ setupElem,
+ "Setup content is not visible when loading"
+ );
+ } else {
+ ok(
+ !tabsContainer.classList.contains("loading"),
+ "Tabs container has no loading class"
+ );
+ !loadingElem ||
+ BrowserTestUtils.is_hidden(
+ loadingElem,
+ "Loading content is not visible when tabs are loaded"
+ );
+ BrowserTestUtils.is_visible(
+ tabsList,
+ "Synced tabs list is visible when loaded"
+ );
+ !setupElem ||
+ BrowserTestUtils.is_hidden(
+ setupElem,
+ "Setup content is not visible when tabs are loaded"
+ );
+ }
+}
+
+function setupMocks(recentTabs, syncEnabled = true) {
+ const sandbox = (gSandbox = setupRecentDeviceListMocks());
+ sandbox.stub(SyncedTabs, "getRecentTabs").callsFake(() => {
+ info(
+ `SyncedTabs.getRecentTabs will return a promise resolving to ${recentTabs.length} tabs`
+ );
+ return Promise.resolve(recentTabs);
+ });
+ 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);
+ });
+});
+
+add_task(async function test_tab_sync_loading() {
+ // empty synced tabs, so we're relying on tabs.changed or sync:finish notifications to clear the waiting state
+ const recentTabsData = [];
+ const sandbox = setupMocks(recentTabsData);
+ // stub syncTabs so it resolves to true - meaning yes it will trigger a sync, which is the case
+ // we want to cover in this test.
+ sandbox.stub(SyncedTabs._internal, "syncTabs").resolves(true);
+
+ await withFirefoxView({}, async browser => {
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ const { document } = browser.contentWindow;
+ const tabsContainer = document.querySelector("#tabpickup-tabs-container");
+
+ await waitForElementVisible(browser, "#tabpickup-steps", false);
+ await waitForElementVisible(browser, "#tabpickup-tabs-container", true);
+ checkMobilePromo(browser, {
+ mobilePromo: false,
+ mobileConfirmation: false,
+ });
+
+ ok(TabsSetupFlowManager.waitingForTabs, "waitingForTabs is true");
+ checkLoadingState(browser, true);
+
+ Services.obs.notifyObservers(null, "services.sync.tabs.changed");
+
+ await BrowserTestUtils.waitForMutationCondition(
+ tabsContainer,
+ { attributeFilter: ["class"], attributes: true },
+ () => {
+ return !tabsContainer.classList.contains("loading");
+ }
+ );
+ checkLoadingState(browser, false);
+ checkMobilePromo(browser, {
+ mobilePromo: false,
+ mobileConfirmation: false,
+ });
+ });
+ await tearDown(sandbox);
+});
+
+add_task(async function test_tab_no_sync() {
+ // Ensure we take down the waiting message if SyncedTabs determines it doesnt need to sync
+ const recentTabsData = [];
+ const sandbox = setupMocks(recentTabsData);
+ // stub syncTabs so it resolves to false - meaning it will not trigger a sync, which is the case
+ // we want to cover in this test.
+ sandbox.stub(SyncedTabs._internal, "syncTabs").resolves(false);
+
+ await withFirefoxView({}, async browser => {
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ await waitForElementVisible(browser, "#tabpickup-steps", false);
+ await waitForElementVisible(browser, "#tabpickup-tabs-container", true);
+
+ ok(!TabsSetupFlowManager.waitingForTabs, "waitingForTabs is false");
+ checkLoadingState(browser, false);
+ });
+ await tearDown(sandbox);
+});
+
+add_task(async function test_recent_tabs_loading() {
+ // Simulate stale data by setting lastTabFetch to 10mins ago
+ const TEN_MINUTES_MS = 1000 * 60 * 10;
+ const staleFetchSeconds = Math.floor((Date.now() - TEN_MINUTES_MS) / 1000);
+ info("updating lastFetch:" + staleFetchSeconds);
+ Services.prefs.setIntPref("services.sync.lastTabFetch", staleFetchSeconds);
+
+ // cached tabs data is available, so we shouldn't wait on lastTabFetch pref value
+ const recentTabsData = structuredClone(syncedTabsData1[0].tabs);
+ const sandbox = setupMocks(recentTabsData);
+
+ await withFirefoxView({}, async browser => {
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ await waitForElementVisible(browser, "#tabpickup-steps", false);
+ await waitForElementVisible(browser, "#tabpickup-tabs-container", true);
+ checkMobilePromo(browser, {
+ mobilePromo: false,
+ mobileConfirmation: false,
+ });
+ checkLoadingState(browser, false);
+ });
+ await tearDown(sandbox);
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_sync_admin_disabled.js b/browser/components/firefoxview/tests/browser/browser_sync_admin_disabled.js
new file mode 100644
index 0000000000..43c0663d76
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_sync_admin_disabled.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { TabsSetupFlowManager } = ChromeUtils.importESModule(
+ "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs"
+);
+
+var gSandbox;
+
+add_setup(async function() {
+ Services.prefs.lockPref("identity.fxaccounts.enabled");
+
+ registerCleanupFunction(() => {
+ gSandbox?.restore();
+ Services.prefs.clearUserPref("services.sync.lastTabFetch");
+ Services.prefs.unlockPref("identity.fxaccounts.enabled");
+ Services.prefs.clearUserPref("identity.fxaccounts.enabled");
+ // 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();
+});
+
+add_task(async function test_sync_admin_disabled() {
+ const sandbox = (gSandbox = sinon.createSandbox());
+ sandbox.stub(UIState, "get").callsFake(() => {
+ return {
+ status: UIState.STATUS_NOT_CONFIGURED,
+ syncEnabled: false,
+ };
+ });
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+ is(
+ Services.prefs.getBoolPref("identity.fxaccounts.enabled"),
+ true,
+ "Expected identity.fxaccounts.enabled pref to be false"
+ );
+
+ is(
+ Services.prefs.prefIsLocked("identity.fxaccounts.enabled"),
+ true,
+ "Expected identity.fxaccounts.enabled pref to be locked"
+ );
+
+ await waitForVisibleSetupStep(browser, {
+ expectedVisible: "#tabpickup-steps-view0",
+ });
+
+ const errorStateHeader = document.querySelector(
+ "#tabpickup-steps-view0-header"
+ );
+
+ await BrowserTestUtils.waitForMutationCondition(
+ errorStateHeader,
+ { childList: true },
+ () => errorStateHeader.textContent.includes("disabled")
+ );
+
+ ok(
+ errorStateHeader
+ .getAttribute("data-l10n-id")
+ .includes("fxa-admin-disabled"),
+ "Correct message should show when fxa is disabled by an admin"
+ );
+ });
+ Services.prefs.unlockPref("identity.fxaccounts.enabled");
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_tab_close_last_tab.js b/browser/components/firefoxview/tests/browser/browser_tab_close_last_tab.js
new file mode 100644
index 0000000000..4af862d40c
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_tab_close_last_tab.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL = "https://example.com/";
+
+add_task(async function closing_last_tab_should_not_switch_to_fx_view() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.closeWindowWithLastTab", false]],
+ });
+ info("Opening window...");
+ const win = await BrowserTestUtils.openNewBrowserWindow({
+ waitForTabURL: "about:newtab",
+ });
+ const firstTab = win.gBrowser.selectedTab;
+ info("Opening Firefox View tab...");
+ await openFirefoxViewTab(win);
+ info("Switch back to new tab...");
+ await BrowserTestUtils.switchTab(win.gBrowser, firstTab);
+ info("Load web page in new tab...");
+ const loaded = BrowserTestUtils.browserLoaded(
+ win.gBrowser.selectedBrowser,
+ false,
+ URL
+ );
+ BrowserTestUtils.loadURI(win.gBrowser.selectedBrowser, URL);
+ await loaded;
+ info("Opening new browser tab...");
+ const secondTab = await BrowserTestUtils.openNewForegroundTab(
+ win.gBrowser,
+ URL
+ );
+ info("Close all broswer tabs...");
+ await BrowserTestUtils.removeTab(firstTab);
+ await BrowserTestUtils.removeTab(secondTab);
+ isnot(
+ win.gBrowser.selectedTab,
+ win.FirefoxViewHandler.tab,
+ "The selected tab should not be the Firefox View tab"
+ );
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_tab_on_close_warning.js b/browser/components/firefoxview/tests/browser/browser_tab_on_close_warning.js
new file mode 100644
index 0000000000..cd6d30f3d1
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_tab_on_close_warning.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+class DialogObserver {
+ constructor() {
+ this.wasOpened = false;
+ Services.obs.addObserver(this, "common-dialog-loaded");
+ }
+ cleanup() {
+ Services.obs.removeObserver(this, "common-dialog-loaded");
+ }
+ observe(win, topic) {
+ if (topic == "common-dialog-loaded") {
+ this.wasOpened = true;
+ // Close dialog.
+ win.document
+ .querySelector("dialog")
+ .getButton("cancel")
+ .click();
+ }
+ }
+}
+
+add_task(
+ async function on_close_warning_should_not_show_for_firefox_view_tab() {
+ const dialogObserver = new DialogObserver();
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.warnOnClose", true]],
+ });
+ info("Opening window...");
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+ info("Opening Firefox View tab...");
+ await openFirefoxViewTab(win);
+ info("Trigger warnAboutClosingWindow()");
+ win.BrowserTryToCloseWindow();
+ await BrowserTestUtils.closeWindow(win);
+ ok(!dialogObserver.wasOpened, "Dialog was not opened");
+ dialogObserver.cleanup();
+ }
+);
+
+add_task(
+ async function on_close_warning_should_not_show_for_firefox_view_tab_non_macos() {
+ let initialTab = gBrowser.selectedTab;
+ const dialogObserver = new DialogObserver();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.tabs.warnOnClose", true],
+ ["browser.warnOnQuit", true],
+ ],
+ });
+ info("Opening Firefox View tab...");
+ await openFirefoxViewTab(window);
+ info('Trigger "quit-application-requested"');
+ canQuitApplication("lastwindow", "close-button");
+ ok(!dialogObserver.wasOpened, "Dialog was not opened");
+ await BrowserTestUtils.switchTab(gBrowser, initialTab);
+ closeFirefoxViewTab(window);
+ dialogObserver.cleanup();
+ }
+);
diff --git a/browser/components/firefoxview/tests/browser/browser_tab_pickup_list.js b/browser/components/firefoxview/tests/browser/browser_tab_pickup_list.js
new file mode 100644
index 0000000000..b7fa3a2e5a
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_tab_pickup_list.js
@@ -0,0 +1,607 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { TabsSetupFlowManager } = ChromeUtils.importESModule(
+ "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs"
+);
+
+XPCOMUtils.defineLazyModuleGetters(globalThis, {
+ SyncedTabs: "resource://services-sync/SyncedTabs.jsm",
+});
+
+const twoTabs = [
+ {
+ type: "tab",
+ title: "Phabricator Home",
+ url: "https://phabricator.services.mozilla.com/",
+ icon: "https://phabricator.services.mozilla.com/favicon.d25d81d39065.ico",
+ lastUsed: 1655745700, // Mon, 20 Jun 2022 17:21:40 GMT
+ },
+ {
+ type: "tab",
+ title: "Firefox Privacy Notice",
+ url: "https://www.mozilla.org/en-US/privacy/firefox/",
+ icon:
+ "https://www.mozilla.org/media/img/favicons/mozilla/favicon.d25d81d39065.ico",
+ lastUsed: 1655745700, // Mon, 20 Jun 2022 17:21:40 GMT
+ },
+];
+const syncedTabsData2 = structuredClone(syncedTabsData1);
+syncedTabsData2[1].tabs = [...syncedTabsData2[1].tabs, ...twoTabs];
+
+const syncedTabsData3 = [
+ {
+ id: 1,
+ type: "client",
+ name: "My desktop",
+ clientType: "desktop",
+ lastModified: 1655730486760,
+ tabs: [
+ {
+ 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 syncedTabsData4 = structuredClone(syncedTabsData3);
+syncedTabsData4[0].tabs = [...syncedTabsData4[0].tabs, ...twoTabs];
+
+const syncedTabsData5 = [
+ {
+ id: 1,
+ type: "client",
+ name: "My desktop",
+ clientType: "desktop",
+ lastModified: Date.now(),
+ tabs: [
+ {
+ 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 NO_TABS_EVENTS = [
+ ["firefoxview", "entered", "firefoxview", undefined],
+ ["firefoxview", "synced_tabs", "tabs", undefined, { count: "0" }],
+];
+
+const TAB_PICKUP_EVENT = [
+ ["firefoxview", "entered", "firefoxview", undefined],
+ ["firefoxview", "synced_tabs", "tabs", undefined, { count: "1" }],
+ [
+ "firefoxview",
+ "tab_pickup",
+ "tabs",
+ undefined,
+ { position: "1", deviceType: "desktop" },
+ ],
+];
+
+const TAB_PICKUP_OPEN_EVENT = [
+ ["firefoxview", "tab_pickup_open", "tabs", "false"],
+];
+
+registerCleanupFunction(async function() {
+ cleanup_tab_pickup();
+});
+
+add_task(async function test_tab_list_ordering() {
+ const sandbox = setupRecentDeviceListMocks();
+ const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
+ let mockTabs1 = getMockTabData(syncedTabsData1);
+ let mockTabs2 = getMockTabData(syncedTabsData2);
+ syncedTabsMock.callsFake(() => {
+ info(
+ `Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${mockTabs1.length} tabs\n`
+ );
+ return Promise.resolve(mockTabs1);
+ });
+
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+
+ await setupListState(browser);
+
+ testVisibility(browser, {
+ expectedVisible: {
+ "ol.synced-tabs-list": true,
+ },
+ });
+
+ ok(
+ document.querySelector("ol.synced-tabs-list").children.length === 3,
+ "synced-tabs-list should have three list items"
+ );
+
+ ok(
+ document
+ .querySelector("ol.synced-tabs-list")
+ .firstChild.textContent.includes("Internet for people, not profits"),
+ "First list item in synced-tabs-list is in the correct order"
+ );
+
+ ok(
+ document
+ .querySelector("ol.synced-tabs-list")
+ .children[2].textContent.includes("Sandboxes - Sinon.JS"),
+ "Last list item in synced-tabs-list is in the correct order"
+ );
+
+ syncedTabsMock.returns(mockTabs2);
+ // Initiate a synced tabs update
+ Services.obs.notifyObservers(null, "services.sync.tabs.changed");
+
+ const syncedTabsList = document.querySelector("ol.synced-tabs-list");
+ // first list item has been updated
+ await BrowserTestUtils.waitForMutationCondition(
+ syncedTabsList,
+ { childList: true },
+ () => syncedTabsList.firstChild.textContent.includes("Firefox")
+ );
+
+ ok(
+ document.querySelector("ol.synced-tabs-list").children.length === 3,
+ "Synced-tabs-list should still have three list items"
+ );
+
+ ok(
+ document
+ .querySelector("ol.synced-tabs-list")
+ .children[1].textContent.includes("Phabricator"),
+ "Second list item in synced-tabs-list has been updated"
+ );
+
+ ok(
+ document
+ .querySelector("ol.synced-tabs-list")
+ .children[2].textContent.includes("Internet for people, not profits"),
+ "Last list item in synced-tabs-list has been updated"
+ );
+
+ sandbox.restore();
+ cleanup_tab_pickup();
+ });
+});
+
+add_task(async function test_empty_list_items() {
+ const sandbox = setupRecentDeviceListMocks();
+ const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
+ let mockTabs1 = getMockTabData(syncedTabsData3);
+ let mockTabs2 = getMockTabData(syncedTabsData4);
+ syncedTabsMock.callsFake(() => {
+ info(
+ `Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${mockTabs1.length} tabs\n`
+ );
+ return Promise.resolve(mockTabs1);
+ });
+
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+
+ await setupListState(browser);
+
+ testVisibility(browser, {
+ expectedVisible: {
+ "ol.synced-tabs-list": true,
+ },
+ });
+
+ ok(
+ document.querySelector("ol.synced-tabs-list").children.length === 3,
+ "synced-tabs-list should have three list items"
+ );
+
+ ok(
+ document
+ .querySelector("ol.synced-tabs-list")
+ .firstChild.textContent.includes("Sandboxes - Sinon.JS"),
+ "First list item in synced-tabs-list is in the correct order"
+ );
+
+ ok(
+ document
+ .querySelector("ol.synced-tabs-list")
+ .children[1].classList.contains("synced-tab-li-placeholder"),
+ "Second list item in synced-tabs-list should be a placeholder"
+ );
+
+ ok(
+ document
+ .querySelector("ol.synced-tabs-list")
+ .lastChild.classList.contains("synced-tab-li-placeholder"),
+ "Last list item in synced-tabs-list should be a placeholder"
+ );
+
+ syncedTabsMock.returns(mockTabs2);
+ // Initiate a synced tabs update
+ Services.obs.notifyObservers(null, "services.sync.tabs.changed");
+
+ const syncedTabsList = document.querySelector("ol.synced-tabs-list");
+ // first list item has been updated
+ await BrowserTestUtils.waitForMutationCondition(
+ syncedTabsList,
+ { childList: true },
+ () =>
+ syncedTabsList.firstChild.textContent.includes("Firefox Privacy Notice")
+ );
+
+ ok(
+ document
+ .querySelector("ol.synced-tabs-list")
+ .children[1].textContent.includes("Phabricator"),
+ "Second list item in synced-tabs-list has been updated"
+ );
+
+ ok(
+ document
+ .querySelector("ol.synced-tabs-list")
+ .lastChild.textContent.includes("Sandboxes - Sinon.JS"),
+ "Last list item in synced-tabs-list has been updated"
+ );
+
+ sandbox.restore();
+ cleanup_tab_pickup();
+ });
+});
+
+add_task(async function test_empty_list() {
+ await clearAllParentTelemetryEvents();
+ const sandbox = setupRecentDeviceListMocks();
+ const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
+ let mockTabs1 = getMockTabData([]);
+ let mockTabs2 = getMockTabData(syncedTabsData4);
+ syncedTabsMock.callsFake(() => {
+ info(
+ `Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${mockTabs1.length} tabs\n`
+ );
+ return Promise.resolve(mockTabs1);
+ });
+
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+
+ await setupListState(browser);
+ info("setupListState complete, checking placeholder and list visibility");
+ testVisibility(browser, {
+ expectedVisible: {
+ "#synced-tabs-placeholder": true,
+ "ol.synced-tabs-list": false,
+ },
+ });
+
+ ok(
+ document
+ .querySelector("#synced-tabs-placeholder")
+ .classList.contains("empty-container"),
+ "collapsible container should have correct styling when the list is empty"
+ );
+
+ await TestUtils.waitForCondition(
+ () => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ ).parent;
+ return events && events.length >= 2;
+ },
+ "Waiting for entered and synced_tabs firefoxview telemetry events.",
+ 200,
+ 100
+ );
+
+ TelemetryTestUtils.assertEvents(
+ NO_TABS_EVENTS,
+ { category: "firefoxview" },
+ { clear: true, process: "parent" }
+ );
+
+ syncedTabsMock.callsFake(() => {
+ info(
+ `Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${mockTabs2.length} tabs\n`
+ );
+ return Promise.resolve(mockTabs2);
+ });
+ // Initiate a synced tabs update
+ Services.obs.notifyObservers(null, "services.sync.tabs.changed");
+
+ const syncedTabsList = document.querySelector("ol.synced-tabs-list");
+ await BrowserTestUtils.waitForMutationCondition(
+ syncedTabsList,
+ { childList: true },
+ () => syncedTabsList.children.length
+ );
+
+ testVisibility(browser, {
+ expectedVisible: {
+ "#synced-tabs-placeholder": false,
+ "ol.synced-tabs-list": true,
+ },
+ });
+
+ sandbox.restore();
+ cleanup_tab_pickup();
+ });
+});
+
+add_task(async function test_time_updates_correctly() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.firefox-view.updateTimeMs", 100]],
+ });
+ await clearAllParentTelemetryEvents();
+
+ const sandbox = setupRecentDeviceListMocks();
+ const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
+ let mockTabs1 = getMockTabData(syncedTabsData5);
+ syncedTabsMock.callsFake(() => {
+ info(
+ `Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${mockTabs1.length} tabs\n`
+ );
+ return Promise.resolve(mockTabs1);
+ });
+
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+
+ await setupListState(browser);
+
+ let initialTimeText = document.querySelector("span.synced-tab-li-time")
+ .textContent;
+ Assert.stringContains(
+ initialTimeText,
+ "Just now",
+ "synced-tab-li-time text is 'Just now'"
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.firefox-view.updateTimeMs", 100]],
+ });
+
+ const timeLabel = document.querySelector("span.synced-tab-li-time");
+ await BrowserTestUtils.waitForMutationCondition(
+ timeLabel,
+ { childList: true },
+ () => !timeLabel.textContent.includes("now")
+ );
+
+ isnot(
+ timeLabel.textContent,
+ initialTimeText,
+ "synced-tab-li-time text has updated"
+ );
+
+ document.querySelector(".synced-tab-a").click();
+
+ await TestUtils.waitForCondition(
+ () => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ ).parent;
+ return events && events.length >= 3;
+ },
+ "Waiting for entered, synced_tabs, and tab_pickup firefoxview telemetry events.",
+ 200,
+ 100
+ );
+
+ TelemetryTestUtils.assertEvents(
+ TAB_PICKUP_EVENT,
+ { category: "firefoxview" },
+ { clear: true, process: "parent" }
+ );
+
+ let gBrowser = browser.getTabBrowser();
+ is(
+ gBrowser.visibleTabs.indexOf(gBrowser.selectedTab),
+ 0,
+ "Tab opened at the beginning of the tab strip"
+ );
+ gBrowser.removeTab(gBrowser.selectedTab);
+ // make sure we're back on fx-view
+ browser.ownerGlobal.FirefoxViewHandler.openTab();
+
+ info("Waiting for the tab pickup summary to be visible");
+ await waitForElementVisible(browser, "#tab-pickup-container > summary");
+ // click on the details summary and verify telemetry gets logged for this event
+ await clearAllParentTelemetryEvents();
+ info("clicking the summary to collapse it");
+ document.querySelector("#tab-pickup-container > summary").click();
+
+ await TestUtils.waitForCondition(
+ () => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ ).parent;
+ return events && events.length >= 1;
+ },
+ "Waiting for tab_pickup_open firefoxview telemetry event.",
+ 200,
+ 100
+ );
+ TelemetryTestUtils.assertEvents(
+ TAB_PICKUP_OPEN_EVENT,
+ { category: "firefoxview" },
+ { clear: true, process: "parent" }
+ );
+
+ sandbox.restore();
+ cleanup_tab_pickup();
+ await SpecialPowers.popPrefEnv();
+ });
+});
+
+/**
+ * Ensure that tabs sync when a user reloads Firefox View.
+ * This is accomplished by asserting that a new set of tabs are loaded
+ * on page reload.
+ */
+add_task(async function test_tabs_sync_on_user_page_reload() {
+ const sandbox = setupRecentDeviceListMocks();
+ sandbox.stub(SyncedTabs._internal, "syncTabs").resolves(true);
+ const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
+ let mockTabs1 = getMockTabData(syncedTabsData1);
+ let expectedTabsAfterReload = getMockTabData(syncedTabsData3);
+ syncedTabsMock.callsFake(() => {
+ info(
+ `Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${mockTabs1.length} tabs\n`
+ );
+ return Promise.resolve(mockTabs1);
+ });
+
+ await withFirefoxView({}, async browser => {
+ let reloadButton = browser.ownerDocument.getElementById("reload-button");
+
+ await setupListState(browser);
+
+ let tabLoaded = BrowserTestUtils.browserLoaded(browser);
+ EventUtils.synthesizeMouseAtCenter(reloadButton, {}, browser.ownerGlobal);
+ await tabLoaded;
+ // Wait until the window is reloaded, then get the current instance
+ // of the contentWindow
+ const { document } = browser.contentWindow;
+ ok(true, "Firefox View has been reloaded");
+ ok(TabsSetupFlowManager.waitingForTabs, "waitingForTabs is true");
+
+ syncedTabsMock.returns(expectedTabsAfterReload);
+ Services.obs.notifyObservers(null, "services.sync.tabs.changed");
+ ok(!TabsSetupFlowManager.waitingForTabs, "waitingForTabs is false");
+
+ const syncedTabsList = document.querySelector("ol.synced-tabs-list");
+ // The tab pickup list has been updated
+ await BrowserTestUtils.waitForMutationCondition(
+ syncedTabsList,
+ { childList: true },
+ () =>
+ syncedTabsList.firstChild.textContent.includes("Sandboxes - Sinon.JS")
+ );
+
+ sandbox.restore();
+ cleanup_tab_pickup();
+ });
+});
+
+add_task(async function test_keyboard_navigation() {
+ // Setting this pref allows the test to run as expected on MacOS
+ await SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] });
+ TabsSetupFlowManager.resetInternalState();
+
+ const sandbox = setupRecentDeviceListMocks();
+ const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
+ let mockTabs1 = getMockTabData(syncedTabsData1);
+ syncedTabsMock.callsFake(() => {
+ info(
+ `Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${mockTabs1.length} tabs\n`
+ );
+ return Promise.resolve(mockTabs1);
+ });
+
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ let win = browser.ownerGlobal;
+
+ await setupListState(browser);
+ const tab = (shiftKey = false) => {
+ info(`${shiftKey ? "Shift + Tab" : "Tab"}`);
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey }, win);
+ };
+ const arrowDown = () => {
+ info("Arrow Down");
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ };
+ const arrowUp = () => {
+ info("Arrow Up");
+ EventUtils.synthesizeKey("KEY_ArrowUp", {}, win);
+ };
+ const arrowLeft = () => {
+ info("Arrow Left");
+ EventUtils.synthesizeKey("KEY_ArrowLeft", {}, win);
+ };
+ const arrowRight = () => {
+ info("Arrow Right");
+ EventUtils.synthesizeKey("KEY_ArrowRight", {}, win);
+ };
+
+ let syncedTabsLinks = document
+ .querySelector("ol.synced-tabs-list")
+ .querySelectorAll("a");
+ let summary = document
+ .getElementById("tab-pickup-container")
+ .querySelector("summary");
+ summary.focus();
+ tab();
+ is(
+ syncedTabsLinks[0],
+ document.activeElement,
+ "First synced tab should be focused"
+ );
+ arrowDown();
+ is(
+ syncedTabsLinks[1],
+ document.activeElement,
+ "Second synced tab should be focused"
+ );
+ arrowDown();
+ is(
+ syncedTabsLinks[2],
+ document.activeElement,
+ "Third synced tab should be focused"
+ );
+ arrowDown();
+ is(
+ syncedTabsLinks[2],
+ document.activeElement,
+ "Third synced tab should still be focused"
+ );
+ arrowUp();
+ is(
+ syncedTabsLinks[1],
+ document.activeElement,
+ "Second synced tab should be focused"
+ );
+ arrowLeft();
+ is(
+ syncedTabsLinks[0],
+ document.activeElement,
+ "First synced tab should be focused"
+ );
+ arrowRight();
+ is(
+ syncedTabsLinks[1],
+ document.activeElement,
+ "Second synced tab should be focused"
+ );
+ arrowDown();
+ is(
+ syncedTabsLinks[2],
+ document.activeElement,
+ "Third synced tab should be focused"
+ );
+ arrowLeft();
+ is(
+ syncedTabsLinks[0],
+ document.activeElement,
+ "First synced tab should be focused"
+ );
+
+ tab(true);
+ is(
+ summary,
+ document.activeElement,
+ "Summary element should be focused when shift tabbing away from list"
+ );
+
+ sandbox.restore();
+ cleanup_tab_pickup();
+ });
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_ui_state.js b/browser/components/firefoxview/tests/browser/browser_ui_state.js
new file mode 100644
index 0000000000..cc19b75023
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_ui_state.js
@@ -0,0 +1,145 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { TabsSetupFlowManager } = ChromeUtils.importESModule(
+ "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs"
+);
+
+add_task(async function test_state_prefs_unset() {
+ await SpecialPowers.clearUserPref(TAB_PICKUP_STATE_PREF);
+ await SpecialPowers.clearUserPref(RECENTLY_CLOSED_STATE_PREF);
+
+ const sandbox = sinon.createSandbox();
+ let setupCompleteStub = sandbox.stub(
+ TabsSetupFlowManager,
+ "isTabSyncSetupComplete"
+ );
+ setupCompleteStub.returns(true);
+
+ await withFirefoxView({}, async function(browser) {
+ const { document } = browser.contentWindow;
+ let recentlyClosedTabsContainer = document.querySelector(
+ "#recently-closed-tabs-container"
+ );
+ ok(
+ recentlyClosedTabsContainer.open,
+ "Recently Closed Tabs should be open if the pref is unset and sync setup is complete"
+ );
+
+ let tabPickupContainer = document.querySelector("#tab-pickup-container");
+ ok(
+ tabPickupContainer.open,
+ "Tab Pickup container should be open if the pref is unset and sync setup is complete"
+ );
+
+ sandbox.restore();
+ });
+});
+
+add_task(async function test_state_prefs_defined() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [TAB_PICKUP_STATE_PREF, false],
+ [RECENTLY_CLOSED_STATE_PREF, false],
+ ],
+ });
+
+ const sandbox = sinon.createSandbox();
+ let setupCompleteStub = sandbox.stub(
+ TabsSetupFlowManager,
+ "isTabSyncSetupComplete"
+ );
+ setupCompleteStub.returns(true);
+
+ await withFirefoxView({}, async function(browser) {
+ const { document } = browser.contentWindow;
+ let recentlyClosedTabsContainer = document.querySelector(
+ "#recently-closed-tabs-container"
+ );
+ ok(
+ !recentlyClosedTabsContainer.getAttribute("open"),
+ "Recently Closed Tabs should not be open if the pref is set to false"
+ );
+
+ let tabPickupContainer = document.querySelector("#tab-pickup-container");
+ ok(
+ !tabPickupContainer.getAttribute("open"),
+ "Tab Pickup container should not be open if the pref is set to false and sync setup is complete"
+ );
+
+ sandbox.restore();
+ });
+});
+
+add_task(async function test_state_pref_set_on_toggle() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [TAB_PICKUP_STATE_PREF, true],
+ [RECENTLY_CLOSED_STATE_PREF, true],
+ ],
+ });
+
+ const sandbox = sinon.createSandbox();
+ let setupCompleteStub = sandbox.stub(
+ TabsSetupFlowManager,
+ "isTabSyncSetupComplete"
+ );
+ setupCompleteStub.returns(true);
+
+ await withFirefoxView({}, async function(browser) {
+ const { document } = browser.contentWindow;
+
+ await waitForElementVisible(browser, "#tab-pickup-container > summary");
+
+ document.querySelector("#tab-pickup-container > summary").click();
+
+ document.querySelector("#recently-closed-tabs-container > summary").click();
+
+ // Wait a turn for the click to propagate to the pref.
+ await TestUtils.waitForTick();
+
+ ok(
+ !Services.prefs.getBoolPref(RECENTLY_CLOSED_STATE_PREF),
+ "Hiding the recently closed container should have flipped the UI state pref value"
+ );
+ ok(
+ !Services.prefs.getBoolPref(TAB_PICKUP_STATE_PREF),
+ "Hiding the tab pickup container should have flipped the UI state pref value"
+ );
+
+ sandbox.restore();
+ });
+});
+
+add_task(async function test_state_prefs_ignored_during_sync_setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [TAB_PICKUP_STATE_PREF, false],
+ [RECENTLY_CLOSED_STATE_PREF, false],
+ ],
+ });
+ const sandbox = sinon.createSandbox();
+ let setupCompleteStub = sandbox.stub(
+ TabsSetupFlowManager,
+ "isTabSyncSetupComplete"
+ );
+ setupCompleteStub.returns(false);
+ await withFirefoxView({}, async function(browser) {
+ const { document } = browser.contentWindow;
+ let recentlyClosedTabsContainer = document.querySelector(
+ "#recently-closed-tabs-container"
+ );
+ ok(
+ !recentlyClosedTabsContainer.open,
+ "Recently Closed Tabs should not be open if the pref is set to false"
+ );
+
+ let tabPickupContainer = document.querySelector("#tab-pickup-container");
+ ok(
+ tabPickupContainer.open,
+ "Tab Pickup container should be open if the pref is set to false but sync setup is not complete"
+ );
+
+ sandbox.restore();
+ });
+});
diff --git a/browser/components/firefoxview/tests/browser/head.js b/browser/components/firefoxview/tests/browser/head.js
new file mode 100644
index 0000000000..68bf9b8316
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/head.js
@@ -0,0 +1,599 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* exported testVisibility */
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { ASRouter } = ChromeUtils.import(
+ "resource://activity-stream/lib/ASRouter.jsm"
+);
+const { UIState } = ChromeUtils.import("resource://services-sync/UIState.jsm");
+const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
+const { FeatureCalloutMessages } = ChromeUtils.import(
+ "resource://activity-stream/lib/FeatureCalloutMessages.jsm"
+);
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AboutWelcomeParent: "resource:///actors/AboutWelcomeParent.jsm",
+});
+
+const MOBILE_PROMO_DISMISSED_PREF =
+ "browser.tabs.firefox-view.mobilePromo.dismissed";
+const RECENTLY_CLOSED_STATE_PREF =
+ "browser.tabs.firefox-view.ui-state.recently-closed-tabs.open";
+const TAB_PICKUP_STATE_PREF =
+ "browser.tabs.firefox-view.ui-state.tab-pickup.open";
+
+const calloutId = "root";
+const calloutSelector = `#${calloutId}.featureCallout`;
+const primaryButtonSelector = `#${calloutId} .primary`;
+
+/**
+ * URLs used for browser_recently_closed_tabs_keyboard and
+ * browser_firefoxview_accessibility
+ */
+const URLs = [
+ "http://mochi.test:8888/browser/",
+ "https://www.example.com/",
+ "https://example.net/",
+ "https://example.org/",
+];
+
+const syncedTabsData1 = [
+ {
+ id: 1,
+ type: "client",
+ name: "My desktop",
+ clientType: "desktop",
+ lastModified: 1655730486760,
+ tabs: [
+ {
+ 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
+ },
+ {
+ type: "tab",
+ title: "Internet for people, not profits - Mozilla",
+ url: "https://www.mozilla.org/",
+ icon:
+ "https://www.mozilla.org/media/img/favicons/mozilla/favicon.d25d81d39065.ico",
+ lastUsed: 1655730486, // Mon Jun 20 2022 13:08:06 GMT+0000
+ },
+ ],
+ },
+ {
+ id: 2,
+ type: "client",
+ name: "My iphone",
+ clientType: "phone",
+ lastModified: 1655727832930,
+ tabs: [
+ {
+ type: "tab",
+ title: "The Guardian",
+ url: "https://www.theguardian.com/",
+ icon: "page-icon:https://www.theguardian.com/",
+ lastUsed: 1655291890, // Wed Jun 15 2022 11:18:10 GMT+0000
+ },
+ {
+ type: "tab",
+ title: "The Times",
+ url: "https://www.thetimes.co.uk/",
+ icon: "page-icon:https://www.thetimes.co.uk/",
+ lastUsed: 1655727485, // Mon Jun 20 2022 12:18:05 GMT+0000
+ },
+ ],
+ },
+];
+
+async function clearAllParentTelemetryEvents() {
+ // Clear everything.
+ await TestUtils.waitForCondition(() => {
+ Services.telemetry.clearEvents();
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).parent;
+ return !events || !events.length;
+ });
+}
+
+function testVisibility(browser, expected) {
+ const { document } = browser.contentWindow;
+ for (let [selector, shouldBeVisible] of Object.entries(
+ expected.expectedVisible
+ )) {
+ const elem = document.querySelector(selector);
+ if (shouldBeVisible) {
+ ok(
+ BrowserTestUtils.is_visible(elem),
+ `Expected ${selector} to be visible`
+ );
+ } else {
+ ok(BrowserTestUtils.is_hidden(elem), `Expected ${selector} to be hidden`);
+ }
+ }
+}
+
+async function waitForElementVisible(browser, selector, isVisible = true) {
+ const { document } = browser.contentWindow;
+ const elem = document.querySelector(selector);
+ if (!isVisible && !elem) {
+ return;
+ }
+ ok(elem, `Got element with selector: ${selector}`);
+
+ await BrowserTestUtils.waitForMutationCondition(
+ elem,
+ {
+ attributeFilter: ["hidden"],
+ },
+ () => {
+ return isVisible
+ ? BrowserTestUtils.is_visible(elem)
+ : BrowserTestUtils.is_hidden(elem);
+ }
+ );
+}
+
+async function waitForVisibleSetupStep(browser, expected) {
+ const { document } = browser.contentWindow;
+
+ const deck = document.querySelector(".sync-setup-container");
+ const nextStepElem = deck.querySelector(expected.expectedVisible);
+ const stepElems = deck.querySelectorAll(".setup-step");
+
+ await BrowserTestUtils.waitForMutationCondition(
+ deck,
+ {
+ attributeFilter: ["selected-view"],
+ },
+ () => {
+ return BrowserTestUtils.is_visible(nextStepElem);
+ }
+ );
+
+ for (let elem of stepElems) {
+ if (elem == nextStepElem) {
+ ok(
+ BrowserTestUtils.is_visible(elem),
+ `Expected ${elem.id || elem.className} to be visible`
+ );
+ } else {
+ ok(
+ BrowserTestUtils.is_hidden(elem),
+ `Expected ${elem.id || elem.className} to be hidden`
+ );
+ }
+ }
+}
+
+function assertFirefoxViewTab(w) {
+ ok(w.FirefoxViewHandler.tab, "Firefox View tab exists");
+ ok(w.FirefoxViewHandler.tab?.hidden, "Firefox View tab is hidden");
+ is(
+ w.gBrowser.visibleTabs.indexOf(w.FirefoxViewHandler.tab),
+ -1,
+ "Firefox View tab is not in the list of visible tabs"
+ );
+}
+
+async function openFirefoxViewTab(w) {
+ ok(
+ !w.FirefoxViewHandler.tab,
+ "Firefox View tab doesn't exist prior to clicking the button"
+ );
+ info("Clicking the Firefox View button");
+ await EventUtils.synthesizeMouseAtCenter(
+ w.document.getElementById("firefox-view-button"),
+ { type: "mousedown" },
+ w
+ );
+ assertFirefoxViewTab(w);
+ ok(w.FirefoxViewHandler.tab.selected, "Firefox View tab is selected");
+ await BrowserTestUtils.browserLoaded(w.FirefoxViewHandler.tab.linkedBrowser);
+ return w.FirefoxViewHandler.tab;
+}
+
+function closeFirefoxViewTab(w) {
+ w.gBrowser.removeTab(w.FirefoxViewHandler.tab);
+ ok(
+ !w.FirefoxViewHandler.tab,
+ "Reference to Firefox View tab got removed when closing the tab"
+ );
+}
+
+async function withFirefoxView(
+ { resetFlowManager = true, win = null },
+ taskFn
+) {
+ let shouldCloseWin = false;
+ if (!win) {
+ win = await BrowserTestUtils.openNewBrowserWindow();
+ shouldCloseWin = true;
+ }
+ if (resetFlowManager) {
+ const { TabsSetupFlowManager } = ChromeUtils.importESModule(
+ "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs"
+ );
+ // reset internal state so we aren't reacting to whatever state the last invocation left behind
+ TabsSetupFlowManager.resetInternalState();
+ }
+ let tab = await openFirefoxViewTab(win);
+ let originalWindow = tab.ownerGlobal;
+ let result = await taskFn(tab.linkedBrowser);
+ let finalWindow = tab.ownerGlobal;
+ if (originalWindow == finalWindow && !tab.closing && tab.linkedBrowser) {
+ // taskFn may resolve within a tick after opening a new tab.
+ // We shouldn't remove the newly opened tab in the same tick.
+ // Wait for the next tick here.
+ await TestUtils.waitForTick();
+ BrowserTestUtils.removeTab(tab);
+ } else {
+ Services.console.logStringMessage(
+ "withFirefoxView: Tab was already closed before " +
+ "removeTab would have been called"
+ );
+ }
+ if (shouldCloseWin) {
+ await BrowserTestUtils.closeWindow(win);
+ }
+ return result;
+}
+
+var gMockFxaDevices = null;
+var gUIStateStatus;
+var gSandbox;
+function setupSyncFxAMocks({ fxaDevices = null, state, syncEnabled = true }) {
+ gUIStateStatus = state || UIState.STATUS_SIGNED_IN;
+ if (gSandbox) {
+ gSandbox.restore();
+ }
+ const sandbox = (gSandbox = sinon.createSandbox());
+ gMockFxaDevices = fxaDevices;
+ sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => fxaDevices);
+ sandbox.stub(UIState, "get").callsFake(() => {
+ return {
+ status: gUIStateStatus,
+ syncEnabled,
+ email:
+ gUIStateStatus === UIState.STATUS_NOT_CONFIGURED
+ ? undefined
+ : "email@example.com",
+ };
+ });
+
+ return sandbox;
+}
+
+function setupRecentDeviceListMocks() {
+ const sandbox = sinon.createSandbox();
+ sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => [
+ {
+ id: 1,
+ name: "My desktop",
+ isCurrentDevice: true,
+ type: "desktop",
+ },
+ {
+ id: 2,
+ name: "My iphone",
+ type: "mobile",
+ },
+ ]);
+
+ sandbox.stub(UIState, "get").returns({
+ status: UIState.STATUS_SIGNED_IN,
+ syncEnabled: true,
+ email: "email@example.com",
+ });
+
+ return sandbox;
+}
+
+function getMockTabData(clients) {
+ let tabs = [];
+
+ for (let client of clients) {
+ for (let tab of client.tabs) {
+ tab.device = client.name;
+ tab.deviceType = client.clientType;
+ }
+ tabs = [...tabs, ...client.tabs.reverse()];
+ }
+ tabs = tabs.sort((a, b) => b.lastUsed - a.lastUsed).slice(0, 3);
+
+ return tabs;
+}
+
+async function setupListState(browser) {
+ // Skip the synced tabs sign up flow to get to a loaded list state
+ await SpecialPowers.pushPrefEnv({
+ set: [["services.sync.engine.tabs", true]],
+ });
+
+ UIState.refresh();
+ const recentFetchTime = Math.floor(Date.now() / 1000);
+ info("updating lastFetch:" + recentFetchTime);
+ Services.prefs.setIntPref("services.sync.lastTabFetch", recentFetchTime);
+
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ await waitForElementVisible(browser, "#tabpickup-steps", false);
+ await waitForElementVisible(browser, "#tabpickup-tabs-container", true);
+
+ const tabsContainer = browser.contentWindow.document.querySelector(
+ "#tabpickup-tabs-container"
+ );
+ await tabsContainer.tabListAdded;
+ await BrowserTestUtils.waitForMutationCondition(
+ tabsContainer,
+ { attributeFilter: ["class"], attributes: true },
+ () => {
+ return !tabsContainer.classList.contains("loading");
+ }
+ );
+ info("tabsContainer isn't loading anymore, returning");
+}
+
+function checkMobilePromo(browser, expected = {}) {
+ const { document } = browser.contentWindow;
+ const promoElem = document.querySelector(
+ "#tab-pickup-container > .promo-box"
+ );
+ const successElem = document.querySelector(
+ "#tab-pickup-container > .confirmation-message-box"
+ );
+
+ info("checkMobilePromo: " + JSON.stringify(expected));
+ if (expected.mobilePromo) {
+ ok(BrowserTestUtils.is_visible(promoElem), "Mobile promo is visible");
+ } else {
+ ok(
+ !promoElem || BrowserTestUtils.is_hidden(promoElem),
+ "Mobile promo is hidden"
+ );
+ }
+ if (expected.mobileConfirmation) {
+ ok(
+ BrowserTestUtils.is_visible(successElem),
+ "Success confirmation is visible"
+ );
+ } else {
+ ok(
+ !successElem || BrowserTestUtils.is_hidden(successElem),
+ "Success confirmation is hidden"
+ );
+ }
+}
+
+async function touchLastTabFetch() {
+ // lastTabFetch stores a timestamp in *seconds*.
+ const nowSeconds = Math.floor(Date.now() / 1000);
+ info("updating lastFetch:" + nowSeconds);
+ Services.prefs.setIntPref("services.sync.lastTabFetch", nowSeconds);
+ // wait so all pref observers can complete
+ await TestUtils.waitForTick();
+}
+
+let gUIStateSyncEnabled;
+function setupMocks({ fxaDevices = null, state, syncEnabled = true }) {
+ gUIStateStatus = state || UIState.STATUS_SIGNED_IN;
+ gUIStateSyncEnabled = syncEnabled;
+ if (gSandbox) {
+ gSandbox.restore();
+ }
+ const sandbox = (gSandbox = sinon.createSandbox());
+ gMockFxaDevices = fxaDevices;
+ sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => fxaDevices);
+ sandbox.stub(UIState, "get").callsFake(() => {
+ return {
+ status: gUIStateStatus,
+ // Sometimes syncEnabled is not present on UIState, for example when the user signs
+ // out the state is just { status: "not_configured" }
+ ...(gUIStateSyncEnabled != undefined && {
+ syncEnabled: gUIStateSyncEnabled,
+ }),
+ };
+ });
+ return sandbox;
+}
+
+async function tearDown(sandbox) {
+ sandbox?.restore();
+ Services.prefs.clearUserPref("services.sync.lastTabFetch");
+ Services.prefs.clearUserPref(MOBILE_PROMO_DISMISSED_PREF);
+}
+
+/**
+ * Returns a value that can be used to set
+ * `browser.firefox-view.feature-tour` to change the feature tour's
+ * UI state.
+ *
+ * @see FeatureCalloutMessages.jsm for valid values of "screen"
+ *
+ * @param {number} screen The full ID of the feature callout screen
+ * @return {string} JSON string used to set
+ * `browser.firefox-view.feature-tour`
+ */
+const getPrefValueByScreen = screen => {
+ return JSON.stringify({
+ screen: `FEATURE_CALLOUT_${screen}`,
+ complete: false,
+ });
+};
+
+/**
+ * Wait for a feature callout screen of given parameters to be shown
+ * @param {Document} doc the document where the callout appears.
+ * @param {String} screenPostfix The full ID of the feature callout screen.
+ */
+const waitForCalloutScreen = async (doc, screenPostfix) => {
+ await BrowserTestUtils.waitForCondition(() =>
+ doc.querySelector(`${calloutSelector}:not(.hidden) .${screenPostfix}`)
+ );
+};
+
+/**
+ * Waits for the feature callout screen to be removed.
+ *
+ * @param {Document} doc The document where the callout appears.
+ */
+const waitForCalloutRemoved = async doc => {
+ await BrowserTestUtils.waitForCondition(() => {
+ return !doc.body.querySelector(calloutSelector);
+ });
+};
+
+/**
+ * NOTE: Should be replaced with synthesizeMouseAtCenter for
+ * simulating user input. See Bug 1798322
+ *
+ * Clicks the primary button in the feature callout dialog
+ *
+ * @param {document} doc Firefox View document
+ */
+const clickPrimaryButton = async doc => {
+ doc.querySelector(primaryButtonSelector).click();
+};
+
+/**
+ * Closes a feature callout via a click to the dismiss button.
+ *
+ * @param {Document} doc The document where the callout appears.
+ */
+const closeCallout = async doc => {
+ // close the callout dialog
+ const dismissBtn = doc.querySelector(`${calloutSelector} .dismiss-button`);
+ if (!dismissBtn) {
+ return;
+ }
+ doc.querySelector(`${calloutSelector} .dismiss-button`).click();
+ await BrowserTestUtils.waitForCondition(() => {
+ return !document.querySelector(calloutSelector);
+ });
+};
+
+/**
+ * Get a Feature Callout message by id.
+ *
+ * @param {string} Message id
+ */
+const getCalloutMessageById = id => {
+ return {
+ message: FeatureCalloutMessages.getMessages().find(m => m.id === id),
+ };
+};
+
+/**
+ * Create a sinon sandbox with `sendTriggerMessage` stubbed
+ * to return a specified test message for featureCalloutCheck.
+ *
+ * @param {object} Test message
+ */
+const createSandboxWithCalloutTriggerStub = testMessage => {
+ const firefoxViewMatch = sinon.match({
+ id: "featureCalloutCheck",
+ context: { source: "firefoxview" },
+ });
+ const sandbox = sinon.createSandbox();
+ const sendTriggerStub = sandbox.stub(ASRouter, "sendTriggerMessage");
+ sendTriggerStub.withArgs(firefoxViewMatch).resolves(testMessage);
+ sendTriggerStub.callThrough();
+ return sandbox;
+};
+
+/**
+ * A helper to check that correct telemetry was sent by AWSendEventTelemetry.
+ * This is a wrapper around sinon's spy functionality.
+ *
+ * @example
+ * let spy = new TelemetrySpy();
+ * element.click();
+ * spy.assertCalledWith({ event: "CLICK" });
+ * spy.restore();
+ */
+class TelemetrySpy {
+ /**
+ * @param {object} [sandbox] A pre-existing sinon sandbox to build the spy in.
+ * If not provided, a new sandbox will be created.
+ */
+ constructor(sandbox = sinon.createSandbox()) {
+ this.sandbox = sandbox;
+ this.spy = this.sandbox
+ .spy(AboutWelcomeParent.prototype, "onContentMessage")
+ .withArgs("AWPage:TELEMETRY_EVENT");
+ registerCleanupFunction(() => this.restore());
+ }
+ /**
+ * Assert that AWSendEventTelemetry sent the expected telemetry object.
+ * @param {Object} expectedData
+ */
+ assertCalledWith(expectedData) {
+ let match = this.spy.calledWith("AWPage:TELEMETRY_EVENT", expectedData);
+ if (match) {
+ ok(true, "Expected telemetry sent");
+ } else if (this.spy.called) {
+ ok(
+ false,
+ "Wrong telemetry sent: " + JSON.stringify(this.spy.lastCall.args)
+ );
+ } else {
+ ok(false, "No telemetry sent");
+ }
+ }
+ reset() {
+ this.spy.resetHistory();
+ }
+ restore() {
+ this.sandbox.restore();
+ }
+}
+
+/**
+ * Helper function to open and close a tab so the recently
+ * closed tabs list can have data.
+ *
+ * @param {string} url
+ * @return {Promise} Promise that resolves when the session store
+ * has been updated after closing the tab.
+ */
+async function open_then_close(url) {
+ let { updatePromise } = await BrowserTestUtils.withNewTab(
+ url,
+ async browser => {
+ return {
+ updatePromise: BrowserTestUtils.waitForSessionStoreUpdate({
+ linkedBrowser: browser,
+ }),
+ };
+ }
+ );
+ await updatePromise;
+ return TestUtils.topicObserved("sessionstore-closed-objects-changed");
+}
+
+/**
+ * Clears session history. Used to clear out the recently closed tabs list.
+ *
+ */
+function clearHistory() {
+ Services.obs.notifyObservers(null, "browser:purge-session-history");
+}
+
+/**
+ * Cleanup function for tab pickup tests.
+ *
+ */
+function cleanup_tab_pickup() {
+ Services.prefs.clearUserPref("services.sync.engine.tabs");
+ Services.prefs.clearUserPref("services.sync.lastTabFetch");
+ Services.prefs.clearUserPref(TAB_PICKUP_STATE_PREF);
+}