summaryrefslogtreecommitdiffstats
path: root/browser/components/uitour/test
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/uitour/test')
-rw-r--r--browser/components/uitour/test/browser.ini49
-rw-r--r--browser/components/uitour/test/browser_UITour.js751
-rw-r--r--browser/components/uitour/test/browser_UITour2.js150
-rw-r--r--browser/components/uitour/test/browser_UITour3.js317
-rw-r--r--browser/components/uitour/test/browser_UITour4.js235
-rw-r--r--browser/components/uitour/test/browser_UITour5.js60
-rw-r--r--browser/components/uitour/test/browser_UITour_annotation_size_attributes.js65
-rw-r--r--browser/components/uitour/test/browser_UITour_availableTargets.js129
-rw-r--r--browser/components/uitour/test/browser_UITour_colorway.js74
-rw-r--r--browser/components/uitour/test/browser_UITour_defaultBrowser.js66
-rw-r--r--browser/components/uitour/test/browser_UITour_detach_tab.js113
-rw-r--r--browser/components/uitour/test/browser_UITour_forceReaderMode.js24
-rw-r--r--browser/components/uitour/test/browser_UITour_modalDialog.js116
-rw-r--r--browser/components/uitour/test/browser_UITour_observe.js99
-rw-r--r--browser/components/uitour/test/browser_UITour_panel_close_annotation.js227
-rw-r--r--browser/components/uitour/test/browser_UITour_pocket.js38
-rw-r--r--browser/components/uitour/test/browser_UITour_resetProfile.js46
-rw-r--r--browser/components/uitour/test/browser_UITour_showNewTab.js25
-rw-r--r--browser/components/uitour/test/browser_UITour_showProtectionReport.js47
-rw-r--r--browser/components/uitour/test/browser_UITour_sync.js231
-rw-r--r--browser/components/uitour/test/browser_UITour_toggleReaderMode.js21
-rw-r--r--browser/components/uitour/test/browser_backgroundTab.js57
-rw-r--r--browser/components/uitour/test/browser_closeTab.js23
-rw-r--r--browser/components/uitour/test/browser_fxa.js61
-rw-r--r--browser/components/uitour/test/browser_fxa_config.js379
-rw-r--r--browser/components/uitour/test/browser_openPreferences.js73
-rw-r--r--browser/components/uitour/test/browser_openSearchPanel.js34
-rw-r--r--browser/components/uitour/test/head.js539
-rw-r--r--browser/components/uitour/test/image.pngbin0 -> 56060 bytes
-rw-r--r--browser/components/uitour/test/uitour.html42
30 files changed, 4091 insertions, 0 deletions
diff --git a/browser/components/uitour/test/browser.ini b/browser/components/uitour/test/browser.ini
new file mode 100644
index 0000000000..2fb34ea080
--- /dev/null
+++ b/browser/components/uitour/test/browser.ini
@@ -0,0 +1,49 @@
+[DEFAULT]
+support-files =
+ head.js
+ image.png
+ uitour.html
+ ../UITour-lib.js
+
+
+[browser_UITour.js]
+skip-if =
+ os == "linux" || verify # Intermittent failures, bug 951965
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_UITour2.js]
+[browser_UITour3.js]
+[browser_UITour4.js]
+[browser_UITour5.js]
+[browser_UITour_annotation_size_attributes.js]
+[browser_UITour_availableTargets.js]
+[browser_UITour_colorway.js]
+[browser_UITour_defaultBrowser.js]
+[browser_UITour_detach_tab.js]
+[browser_UITour_forceReaderMode.js]
+[browser_UITour_modalDialog.js]
+skip-if = os != "mac" # modal dialog disabling only working on OS X.
+[browser_UITour_observe.js]
+[browser_UITour_panel_close_annotation.js]
+skip-if = true # Bug 1026310
+[browser_UITour_pocket.js]
+skip-if = true # Disabled pending removal of pocket UI Tour
+[browser_UITour_resetProfile.js]
+skip-if = (verify && !debug && (os == 'linux'))
+[browser_UITour_showNewTab.js]
+skip-if = (verify && !debug && (os == 'linux'))
+[browser_UITour_showProtectionReport.js]
+skip-if = os == "linux" && (asan || debug || tsan) # Bug 1697217
+[browser_UITour_sync.js]
+skip-if =
+ os == "linux" # Bug 1678417
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_UITour_toggleReaderMode.js]
+skip-if = (verify && !debug && (os == 'linux'))
+[browser_backgroundTab.js]
+[browser_closeTab.js]
+skip-if = (verify && !debug && (os == 'linux'))
+[browser_fxa.js]
+[browser_fxa_config.js]
+[browser_openPreferences.js]
+[browser_openSearchPanel.js]
+skip-if = true # Bug 1113038 - Intermittent "Popup was opened"
diff --git a/browser/components/uitour/test/browser_UITour.js b/browser/components/uitour/test/browser_UITour.js
new file mode 100644
index 0000000000..fc2353117a
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour.js
@@ -0,0 +1,751 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var gTestTab;
+var gContentAPI;
+
+ChromeUtils.defineESModuleGetters(this, {
+ ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs",
+ TelemetryArchiveTesting:
+ "resource://testing-common/TelemetryArchiveTesting.sys.mjs",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+ UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
+});
+
+function test() {
+ UITourTest();
+}
+
+var tests = [
+ function test_untrusted_host(done) {
+ loadUITourTestPage(function () {
+ CustomizableUI.addWidgetToArea(
+ "bookmarks-menu-button",
+ CustomizableUI.AREA_NAVBAR,
+ 0
+ );
+ registerCleanupFunction(() =>
+ CustomizableUI.removeWidgetFromArea("bookmarks-menu-button")
+ );
+ let bookmarksMenu = document.getElementById("bookmarks-menu-button");
+ is(bookmarksMenu.open, false, "Bookmark menu should initially be closed");
+
+ gContentAPI.showMenu("bookmarks");
+ is(
+ bookmarksMenu.open,
+ false,
+ "Bookmark menu should not open on a untrusted host"
+ );
+
+ done();
+ }, "http://mochi.test:8888/");
+ },
+ function test_testing_host(done) {
+ // Add two testing origins intentionally surrounded by whitespace to be ignored.
+ Services.prefs.setCharPref(
+ "browser.uitour.testingOrigins",
+ "https://test1.example.org, https://test2.example.org:443 "
+ );
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.uitour.testingOrigins");
+ });
+ function callback(result) {
+ ok(result, "Callback should be called on a testing origin");
+ done();
+ }
+
+ loadUITourTestPage(function () {
+ gContentAPI.getConfiguration("appinfo", callback);
+ }, "https://test2.example.org/");
+ },
+ function test_unsecure_host(done) {
+ loadUITourTestPage(function () {
+ let bookmarksMenu = document.getElementById("bookmarks-menu-button");
+ is(bookmarksMenu.open, false, "Bookmark menu should initially be closed");
+
+ gContentAPI.showMenu("bookmarks");
+ is(
+ bookmarksMenu.open,
+ false,
+ "Bookmark menu should not open on a unsecure host"
+ );
+
+ done();
+ }, "http://example.org/");
+ },
+ function test_unsecure_host_override(done) {
+ Services.prefs.setBoolPref("browser.uitour.requireSecure", false);
+ loadUITourTestPage(function () {
+ let highlight = document.getElementById("UITourHighlight");
+ is_element_hidden(highlight, "Highlight should initially be hidden");
+
+ gContentAPI.showHighlight("urlbar").then(() => {
+ waitForElementToBeVisible(
+ highlight,
+ done,
+ "Highlight should be shown on a unsecure host when override pref is set"
+ );
+
+ Services.prefs.setBoolPref("browser.uitour.requireSecure", true);
+ });
+ }, "http://example.org/");
+ },
+ function test_disabled(done) {
+ Services.prefs.setBoolPref("browser.uitour.enabled", false);
+
+ let bookmarksMenu = document.getElementById("bookmarks-menu-button");
+ is(bookmarksMenu.open, false, "Bookmark menu should initially be closed");
+
+ gContentAPI.showMenu("bookmarks").then(() => {
+ is(
+ bookmarksMenu.open,
+ false,
+ "Bookmark menu should not open when feature is disabled"
+ );
+
+ Services.prefs.setBoolPref("browser.uitour.enabled", true);
+ });
+ done();
+ },
+ function test_highlight(done) {
+ function test_highlight_2() {
+ let highlight = document.getElementById("UITourHighlight");
+ gContentAPI.hideHighlight();
+
+ waitForElementToBeHidden(
+ highlight,
+ test_highlight_3,
+ "Highlight should be hidden after hideHighlight()"
+ );
+ }
+ function test_highlight_3() {
+ is_element_hidden(
+ highlight,
+ "Highlight should be hidden after hideHighlight()"
+ );
+
+ gContentAPI.showHighlight("urlbar");
+ waitForElementToBeVisible(
+ highlight,
+ test_highlight_4,
+ "Highlight should be shown after showHighlight()"
+ );
+ }
+ function test_highlight_4() {
+ let highlight = document.getElementById("UITourHighlight");
+ gContentAPI.showHighlight("backForward");
+ waitForElementToBeVisible(
+ highlight,
+ done,
+ "Highlight should be shown after showHighlight()"
+ );
+ }
+
+ let highlight = document.getElementById("UITourHighlight");
+ is_element_hidden(highlight, "Highlight should initially be hidden");
+
+ gContentAPI.showHighlight("urlbar");
+ waitForElementToBeVisible(
+ highlight,
+ test_highlight_2,
+ "Highlight should be shown after showHighlight()"
+ );
+ },
+ function test_highlight_toolbar_button(done) {
+ function check_highlight_size() {
+ let panel = highlight.parentElement;
+ let anchor = panel.anchorNode;
+ let anchorRect = anchor.getBoundingClientRect();
+ info(
+ "addons target: width: " +
+ anchorRect.width +
+ " height: " +
+ anchorRect.height
+ );
+ let dimension = anchorRect.width;
+ let highlightRect = highlight.getBoundingClientRect();
+ info(
+ "highlight: width: " +
+ highlightRect.width +
+ " height: " +
+ highlightRect.height
+ );
+ is(
+ Math.round(highlightRect.width),
+ dimension,
+ "The width of the highlight should be equal to the width of the target"
+ );
+ is(
+ Math.round(highlightRect.height),
+ dimension,
+ "The height of the highlight should be equal to the width of the target"
+ );
+ is(
+ highlight.classList.contains("rounded-highlight"),
+ true,
+ "Highlight should be rounded-rectangle styled"
+ );
+ CustomizableUI.removeWidgetFromArea("home-button");
+ done();
+ }
+ info("Adding home button.");
+ CustomizableUI.addWidgetToArea("home-button", "nav-bar");
+ // Force the button to get layout so we can show the highlight.
+ document.getElementById("home-button").clientHeight;
+ let highlight = document.getElementById("UITourHighlight");
+ is_element_hidden(highlight, "Highlight should initially be hidden");
+
+ gContentAPI.showHighlight("home");
+ waitForElementToBeVisible(
+ highlight,
+ check_highlight_size,
+ "Highlight should be shown after showHighlight()"
+ );
+ },
+ function test_highlight_addons_auto_open_close(done) {
+ let highlight = document.getElementById("UITourHighlight");
+ gContentAPI.showHighlight("addons");
+ waitForElementToBeVisible(
+ highlight,
+ function checkPanelIsOpen() {
+ isnot(PanelUI.panel.state, "closed", "Panel should have opened");
+ isnot(
+ highlight.classList.contains("rounded-highlight"),
+ true,
+ "Highlight should not be round-rectangle styled."
+ );
+
+ let hiddenPromise = promisePanelElementHidden(window, PanelUI.panel);
+ // Move the highlight outside which should close the app menu.
+ gContentAPI.showHighlight("appMenu");
+ hiddenPromise.then(() => {
+ waitForElementToBeVisible(
+ highlight,
+ function checkPanelIsClosed() {
+ isnot(
+ PanelUI.panel.state,
+ "open",
+ "Panel should have closed after the highlight moved elsewhere."
+ );
+ done();
+ },
+ "Highlight should move to the appMenu button"
+ );
+ });
+ },
+ "Highlight should be shown after showHighlight() for fixed panel items"
+ );
+ },
+ function test_highlight_addons_manual_open_close(done) {
+ let highlight = document.getElementById("UITourHighlight");
+ // Manually open the app menu then show a highlight there. The menu should remain open.
+ let shownPromise = promisePanelShown(window);
+ gContentAPI.showMenu("appMenu");
+ shownPromise
+ .then(() => {
+ isnot(PanelUI.panel.state, "closed", "Panel should have opened");
+ gContentAPI.showHighlight("addons");
+
+ waitForElementToBeVisible(
+ highlight,
+ function checkPanelIsStillOpen() {
+ isnot(PanelUI.panel.state, "closed", "Panel should still be open");
+
+ // Move the highlight outside which shouldn't close the app menu since it was manually opened.
+ gContentAPI.showHighlight("appMenu");
+ waitForElementToBeVisible(
+ highlight,
+ function () {
+ isnot(
+ PanelUI.panel.state,
+ "closed",
+ "Panel should remain open since UITour didn't open it in the first place"
+ );
+ gContentAPI.hideMenu("appMenu");
+ done();
+ },
+ "Highlight should move to the appMenu button"
+ );
+ },
+ "Highlight should be shown after showHighlight() for fixed panel items"
+ );
+ })
+ .catch(console.error);
+ },
+ function test_highlight_effect(done) {
+ function waitForHighlightWithEffect(highlightEl, effect, next, error) {
+ return waitForCondition(
+ () => highlightEl.getAttribute("active") == effect,
+ next,
+ error
+ );
+ }
+ function checkDefaultEffect() {
+ is(
+ highlight.getAttribute("active"),
+ "none",
+ "The default should be no effect"
+ );
+
+ gContentAPI.showHighlight("urlbar", "none");
+ waitForHighlightWithEffect(
+ highlight,
+ "none",
+ checkZoomEffect,
+ "There should be no effect"
+ );
+ }
+ function checkZoomEffect() {
+ gContentAPI.showHighlight("urlbar", "zoom");
+ waitForHighlightWithEffect(
+ highlight,
+ "zoom",
+ () => {
+ let style = window.getComputedStyle(highlight);
+ is(
+ style.animationName,
+ "uitour-zoom",
+ "The animation-name should be uitour-zoom"
+ );
+ checkSameEffectOnDifferentTarget();
+ },
+ "There should be a zoom effect"
+ );
+ }
+ function checkSameEffectOnDifferentTarget() {
+ gContentAPI.showHighlight("appMenu", "wobble");
+ waitForHighlightWithEffect(
+ highlight,
+ "wobble",
+ () => {
+ highlight.addEventListener(
+ "animationstart",
+ function (aEvent) {
+ ok(
+ true,
+ "Animation occurred again even though the effect was the same"
+ );
+ checkRandomEffect();
+ },
+ { once: true }
+ );
+ gContentAPI.showHighlight("backForward", "wobble");
+ },
+ "There should be a wobble effect"
+ );
+ }
+ function checkRandomEffect() {
+ function waitForActiveHighlight(highlightEl, next, error) {
+ return waitForCondition(
+ () => highlightEl.hasAttribute("active"),
+ next,
+ error
+ );
+ }
+
+ gContentAPI.hideHighlight();
+ gContentAPI.showHighlight("urlbar", "random");
+ waitForActiveHighlight(
+ highlight,
+ () => {
+ ok(
+ highlight.hasAttribute("active"),
+ "The highlight should be active"
+ );
+ isnot(
+ highlight.getAttribute("active"),
+ "none",
+ "A random effect other than none should have been chosen"
+ );
+ isnot(
+ highlight.getAttribute("active"),
+ "random",
+ "The random effect shouldn't be 'random'"
+ );
+ isnot(
+ UITour.highlightEffects.indexOf(highlight.getAttribute("active")),
+ -1,
+ "Check that a supported effect was randomly chosen"
+ );
+ done();
+ },
+ "There should be an active highlight with a random effect"
+ );
+ }
+
+ let highlight = document.getElementById("UITourHighlight");
+ is_element_hidden(highlight, "Highlight should initially be hidden");
+
+ gContentAPI.showHighlight("urlbar");
+ waitForElementToBeVisible(
+ highlight,
+ checkDefaultEffect,
+ "Highlight should be shown after showHighlight()"
+ );
+ },
+ function test_highlight_effect_unsupported(done) {
+ function checkUnsupportedEffect() {
+ is(
+ highlight.getAttribute("active"),
+ "none",
+ "No effect should be used when an unsupported effect is requested"
+ );
+ done();
+ }
+
+ let highlight = document.getElementById("UITourHighlight");
+ is_element_hidden(highlight, "Highlight should initially be hidden");
+
+ gContentAPI.showHighlight("urlbar", "__UNSUPPORTED__");
+ waitForElementToBeVisible(
+ highlight,
+ checkUnsupportedEffect,
+ "Highlight should be shown after showHighlight()"
+ );
+ },
+ function test_info_1(done) {
+ let popup = document.getElementById("UITourTooltip");
+ let title = document.getElementById("UITourTooltipTitle");
+ let desc = document.getElementById("UITourTooltipDescription");
+ let icon = document.getElementById("UITourTooltipIcon");
+ let buttons = document.getElementById("UITourTooltipButtons");
+
+ popup.addEventListener(
+ "popupshown",
+ function () {
+ is(
+ popup.anchorNode,
+ document.getElementById("urlbar"),
+ "Popup should be anchored to the urlbar"
+ );
+ is(title.textContent, "test title", "Popup should have correct title");
+ is(
+ desc.textContent,
+ "test text",
+ "Popup should have correct description text"
+ );
+ is(icon.src, "", "Popup should have no icon");
+ is(buttons.hasChildNodes(), false, "Popup should have no buttons");
+
+ popup.addEventListener(
+ "popuphidden",
+ function () {
+ popup.addEventListener(
+ "popupshown",
+ function () {
+ done();
+ },
+ { once: true }
+ );
+
+ gContentAPI.showInfo("urlbar", "test title", "test text");
+ },
+ { once: true }
+ );
+ gContentAPI.hideInfo();
+ },
+ { once: true }
+ );
+
+ gContentAPI.showInfo("urlbar", "test title", "test text");
+ },
+ taskify(async function test_info_2() {
+ let popup = document.getElementById("UITourTooltip");
+ let title = document.getElementById("UITourTooltipTitle");
+ let desc = document.getElementById("UITourTooltipDescription");
+ let icon = document.getElementById("UITourTooltipIcon");
+ let buttons = document.getElementById("UITourTooltipButtons");
+
+ await showInfoPromise("urlbar", "urlbar title", "urlbar text");
+
+ is(
+ popup.anchorNode,
+ document.getElementById("urlbar"),
+ "Popup should be anchored to the urlbar"
+ );
+ is(title.textContent, "urlbar title", "Popup should have correct title");
+ is(
+ desc.textContent,
+ "urlbar text",
+ "Popup should have correct description text"
+ );
+ is(icon.src, "", "Popup should have no icon");
+ is(buttons.hasChildNodes(), false, "Popup should have no buttons");
+
+ // Place the search bar in the navigation toolbar temporarily.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.widget.inNavBar", true]],
+ });
+
+ await showInfoPromise("search", "search title", "search text");
+
+ is(
+ popup.anchorNode,
+ document.getElementById("searchbar"),
+ "Popup should be anchored to the searchbar"
+ );
+ is(title.textContent, "search title", "Popup should have correct title");
+ is(
+ desc.textContent,
+ "search text",
+ "Popup should have correct description text"
+ );
+
+ await SpecialPowers.popPrefEnv();
+ }),
+ function test_getConfigurationVersion(done) {
+ function callback(result) {
+ ok(
+ typeof result.version !== "undefined",
+ "Check version isn't undefined."
+ );
+ is(
+ result.version,
+ Services.appinfo.version,
+ "Should have the same version property."
+ );
+ is(
+ result.defaultUpdateChannel,
+ UpdateUtils.getUpdateChannel(false),
+ "Should have the correct update channel."
+ );
+ done();
+ }
+
+ gContentAPI.getConfiguration("appinfo", callback);
+ },
+ function test_getConfigurationDistribution(done) {
+ gContentAPI.getConfiguration("appinfo", result => {
+ ok(
+ typeof result.distribution !== "undefined",
+ "Check distribution isn't undefined."
+ );
+ // distribution id defaults to "default" for most builds, and
+ // "mozilla-MSIX" for MSIX builds.
+ is(
+ result.distribution,
+ AppConstants.platform === "win" &&
+ Services.sysinfo.getProperty("hasWinPackageId")
+ ? "mozilla-MSIX"
+ : "default",
+ 'Should be "default" without preference set.'
+ );
+
+ let defaults = Services.prefs.getDefaultBranch("distribution.");
+ let testDistributionID = "TestDistribution";
+ defaults.setCharPref("id", testDistributionID);
+ gContentAPI.getConfiguration("appinfo", result2 => {
+ ok(
+ typeof result2.distribution !== "undefined",
+ "Check distribution isn't undefined."
+ );
+ is(
+ result2.distribution,
+ testDistributionID,
+ "Should have the distribution as set in preference."
+ );
+
+ done();
+ });
+ });
+ },
+ function test_getConfigurationProfileAge(done) {
+ gContentAPI.getConfiguration("appinfo", result => {
+ ok(
+ typeof result.profileCreatedWeeksAgo === "number",
+ "profileCreatedWeeksAgo should be number."
+ );
+ ok(
+ result.profileResetWeeksAgo === null,
+ "profileResetWeeksAgo should be null."
+ );
+
+ // Set profile reset date to 15 days ago.
+ ProfileAge().then(profileAccessor => {
+ profileAccessor.recordProfileReset(
+ Date.now() - 15 * 24 * 60 * 60 * 1000
+ );
+ gContentAPI.getConfiguration("appinfo", result2 => {
+ ok(
+ typeof result2.profileResetWeeksAgo === "number",
+ "profileResetWeeksAgo should be number."
+ );
+ is(
+ result2.profileResetWeeksAgo,
+ 2,
+ "profileResetWeeksAgo should be 2."
+ );
+ done();
+ });
+ });
+ });
+ },
+ function test_addToolbarButton(done) {
+ let placement = CustomizableUI.getPlacementOfWidget("panic-button");
+ is(placement, null, "default UI has panic button in the palette");
+
+ gContentAPI.getConfiguration("availableTargets", data => {
+ let available = data.targets.includes("forget");
+ ok(!available, "Forget button should not be available by default");
+
+ gContentAPI.addNavBarWidget("forget", () => {
+ info("addNavBarWidget callback successfully called");
+
+ let updatedPlacement =
+ CustomizableUI.getPlacementOfWidget("panic-button");
+ is(updatedPlacement.area, CustomizableUI.AREA_NAVBAR);
+
+ gContentAPI.getConfiguration("availableTargets", data2 => {
+ let updatedAvailable = data2.targets.includes("forget");
+ ok(updatedAvailable, "Forget button should now be available");
+
+ // Cleanup
+ CustomizableUI.removeWidgetFromArea("panic-button");
+ done();
+ });
+ });
+ });
+ },
+ taskify(async function test_search() {
+ let defaultEngine = await Services.search.getDefault();
+ let visibleEngines = await Services.search.getVisibleEngines();
+ let expectedEngines = visibleEngines
+ .filter(engine => engine.identifier)
+ .map(engine => "searchEngine-" + engine.identifier);
+
+ let data = await new Promise(resolve =>
+ gContentAPI.getConfiguration("search", resolve)
+ );
+ let engines = data.engines;
+ ok(Array.isArray(engines), "data.engines should be an array");
+ is(
+ engines.sort().toString(),
+ expectedEngines.sort().toString(),
+ "Engines should be as expected"
+ );
+
+ is(
+ data.searchEngineIdentifier,
+ defaultEngine.identifier,
+ "the searchEngineIdentifier property should contain the defaultEngine's identifier"
+ );
+
+ let someOtherEngineID = data.engines.filter(
+ t => t != "searchEngine-" + defaultEngine.identifier
+ )[0];
+ someOtherEngineID = someOtherEngineID.replace(/^searchEngine-/, "");
+
+ Services.telemetry.clearEvents();
+ Services.fog.testResetFOG();
+
+ await new Promise(resolve => {
+ let observe = function (subject, topic, verb) {
+ Services.obs.removeObserver(observe, "browser-search-engine-modified");
+ info("browser-search-engine-modified: " + verb);
+ if (verb == "engine-default") {
+ is(
+ Services.search.defaultEngine.identifier,
+ someOtherEngineID,
+ "correct engine was switched to"
+ );
+ resolve();
+ }
+ };
+ Services.obs.addObserver(observe, "browser-search-engine-modified");
+ registerCleanupFunction(async () => {
+ await Services.search.setDefault(
+ defaultEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ });
+
+ gContentAPI.setDefaultSearchEngine(someOtherEngineID);
+ });
+
+ let engine = (await Services.search.getVisibleEngines()).filter(
+ e => e.identifier == someOtherEngineID
+ )[0];
+
+ let submissionUrl = engine
+ .getSubmission("dummy")
+ .uri.spec.replace("dummy", "");
+
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "change_default",
+ value: "uitour",
+ extra: {
+ prev_id: defaultEngine.telemetryId,
+ new_id: engine.telemetryId,
+ new_name: engine.name,
+ new_load_path: engine.wrappedJSObject._loadPath,
+ // Telemetry has a limit of 80 characters.
+ new_sub_url: submissionUrl.slice(0, 80),
+ },
+ },
+ ],
+ { category: "search", method: "engine" }
+ );
+
+ let snapshot = await Glean.searchEngineDefault.changed.testGetValue();
+ delete snapshot[0].timestamp;
+ Assert.deepEqual(
+ snapshot[0],
+ {
+ category: "search.engine.default",
+ name: "changed",
+ extra: {
+ change_source: "uitour",
+ previous_engine_id: defaultEngine.telemetryId,
+ new_engine_id: engine.telemetryId,
+ new_display_name: engine.name,
+ new_load_path: engine.wrappedJSObject._loadPath,
+ // Glean has a limit of 100 characters.
+ new_submission_url: submissionUrl.slice(0, 100),
+ },
+ },
+ "Should have received the correct event details"
+ );
+ }),
+ taskify(async function test_treatment_tag() {
+ let ac = new TelemetryArchiveTesting.Checker();
+ await ac.promiseInit();
+ await gContentAPI.setTreatmentTag("foobar", "baz");
+ // Wait until the treatment telemetry is sent before looking in the archive.
+ await BrowserTestUtils.waitForContentEvent(
+ gTestTab.linkedBrowser,
+ "mozUITourNotification",
+ false,
+ event => event.detail.event === "TreatmentTag:TelemetrySent"
+ );
+ await new Promise(resolve => {
+ gContentAPI.getTreatmentTag("foobar", data => {
+ is(data.value, "baz", "set and retrieved treatmentTag");
+ ac.promiseFindPing("uitour-tag", [
+ [["payload", "tagName"], "foobar"],
+ [["payload", "tagValue"], "baz"],
+ ]).then(
+ found => {
+ ok(found, "Telemetry ping submitted for setTreatmentTag");
+ resolve();
+ },
+ err => {
+ ok(false, "Exception finding uitour telemetry ping: " + err);
+ resolve();
+ }
+ );
+ });
+ });
+ }),
+
+ // Make sure this test is last in the file so the appMenu gets left open and done will confirm it got tore down.
+ taskify(async function cleanupMenus() {
+ let shownPromise = promisePanelShown(window);
+ gContentAPI.showMenu("appMenu");
+ await shownPromise;
+ }),
+];
diff --git a/browser/components/uitour/test/browser_UITour2.js b/browser/components/uitour/test/browser_UITour2.js
new file mode 100644
index 0000000000..d911a6142d
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour2.js
@@ -0,0 +1,150 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var gTestTab;
+var gContentAPI;
+
+function test() {
+ UITourTest();
+}
+
+var tests = [
+ function test_info_addons_auto_open_close(done) {
+ let popup = document.getElementById("UITourTooltip");
+ gContentAPI.showInfo("addons", "Addons", "Let's get addons!");
+
+ let shownPromise = promisePanelShown(window);
+ shownPromise.then(() => {
+ UITour.getTarget(window, "addons").then(addonsTarget => {
+ waitForPopupAtAnchor(
+ popup,
+ addonsTarget.node,
+ function checkPanelIsOpen() {
+ isnot(
+ PanelUI.panel.state,
+ "closed",
+ "Panel should have opened before the popup anchored"
+ );
+ ok(
+ PanelUI.panel.hasAttribute("noautohide"),
+ "@noautohide on the menu panel should have been set"
+ );
+
+ // Move the info outside which should close the app menu.
+ gContentAPI.showInfo("appMenu", "Open Me", "You know you want to");
+ UITour.getTarget(window, "appMenu").then(target => {
+ waitForPopupAtAnchor(
+ popup,
+ target.node,
+ function checkPanelIsClosed() {
+ isnot(
+ PanelUI.panel.state,
+ "open",
+ "Panel should have closed after the info moved elsewhere."
+ );
+ ok(
+ !PanelUI.panel.hasAttribute("noautohide"),
+ "@noautohide on the menu panel should have been cleaned up on close"
+ );
+ done();
+ },
+ "Info should move to the appMenu button"
+ );
+ });
+ },
+ "Info panel should be anchored to the addons button"
+ );
+ });
+ });
+ },
+ function test_info_addons_manual_open_close(done) {
+ let popup = document.getElementById("UITourTooltip");
+ // Manually open the app menu then show an info panel there. The menu should remain open.
+ let shownPromise = promisePanelShown(window);
+ gContentAPI.showMenu("appMenu");
+ shownPromise
+ .then(() => {
+ isnot(PanelUI.panel.state, "closed", "Panel should have opened");
+ ok(
+ PanelUI.panel.hasAttribute("noautohide"),
+ "@noautohide on the menu panel should have been set"
+ );
+ gContentAPI.showInfo("addons", "Addons", "Let's get addons!");
+
+ UITour.getTarget(window, "addons").then(customizeTarget => {
+ waitForPopupAtAnchor(
+ popup,
+ customizeTarget.node,
+ function () {
+ isnot(
+ PanelUI.panel.state,
+ "closed",
+ "Panel should still be open"
+ );
+ ok(
+ PanelUI.panel.hasAttribute("noautohide"),
+ "@noautohide on the menu panel should still be set"
+ );
+
+ // Move the info outside which shouldn't close the app menu since it was manually opened.
+ gContentAPI.showInfo(
+ "appMenu",
+ "Open Me",
+ "You know you want to"
+ );
+ UITour.getTarget(window, "appMenu").then(target => {
+ waitForPopupAtAnchor(
+ popup,
+ target.node,
+ function () {
+ isnot(
+ PanelUI.panel.state,
+ "closed",
+ "Menu should remain open since UITour didn't open it in the first place"
+ );
+ waitForElementToBeHidden(window.PanelUI.panel, () => {
+ ok(
+ !PanelUI.panel.hasAttribute("noautohide"),
+ "@noautohide on the menu panel should have been cleaned up on close"
+ );
+ done();
+ });
+ gContentAPI.hideMenu("appMenu");
+ },
+ "Info should move to the appMenu button"
+ );
+ });
+ },
+ "Info should be shown after showInfo() for fixed menu panel items"
+ );
+ });
+ })
+ .catch(console.error);
+ },
+ taskify(async function test_bookmarks_menu() {
+ CustomizableUI.addWidgetToArea(
+ "bookmarks-menu-button",
+ CustomizableUI.AREA_NAVBAR,
+ 0
+ );
+ registerCleanupFunction(() =>
+ CustomizableUI.removeWidgetFromArea("bookmarks-menu-button")
+ );
+
+ let bookmarksMenuButton = document.getElementById("bookmarks-menu-button");
+
+ is(bookmarksMenuButton.open, false, "Menu should initially be closed");
+ gContentAPI.showMenu("bookmarks");
+
+ await waitForConditionPromise(() => {
+ return bookmarksMenuButton.open;
+ }, "Menu should be visible after showMenu()");
+
+ gContentAPI.hideMenu("bookmarks");
+ await waitForConditionPromise(() => {
+ return !bookmarksMenuButton.open;
+ }, "Menu should be hidden after hideMenu()");
+ }),
+];
diff --git a/browser/components/uitour/test/browser_UITour3.js b/browser/components/uitour/test/browser_UITour3.js
new file mode 100644
index 0000000000..526994f420
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour3.js
@@ -0,0 +1,317 @@
+"use strict";
+
+const { CustomizableUITestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CustomizableUITestUtils.sys.mjs"
+);
+let gCUITestUtils = new CustomizableUITestUtils(window);
+
+var gTestTab;
+var gContentAPI;
+
+requestLongerTimeout(2);
+
+add_task(setup_UITourTest);
+
+add_UITour_task(async function test_info_icon() {
+ let popup = document.getElementById("UITourTooltip");
+ let title = document.getElementById("UITourTooltipTitle");
+ let desc = document.getElementById("UITourTooltipDescription");
+ let icon = document.getElementById("UITourTooltipIcon");
+ let buttons = document.getElementById("UITourTooltipButtons");
+
+ // Disable the animation to prevent the mouse clicks from hitting the main
+ // window during the transition instead of the buttons in the popup.
+ popup.setAttribute("animate", "false");
+
+ await showInfoPromise("urlbar", "a title", "some text", "image.png");
+
+ is(title.textContent, "a title", "Popup should have correct title");
+ is(
+ desc.textContent,
+ "some text",
+ "Popup should have correct description text"
+ );
+
+ let imageURL = getRootDirectory(gTestPath) + "image.png";
+ imageURL = imageURL.replace(
+ "chrome://mochitests/content/",
+ "https://example.org/"
+ );
+ is(icon.src, imageURL, "Popup should have correct icon shown");
+
+ is(buttons.hasChildNodes(), false, "Popup should have no buttons");
+});
+
+add_UITour_task(async function test_info_buttons_1() {
+ let popup = document.getElementById("UITourTooltip");
+ let title = document.getElementById("UITourTooltipTitle");
+ let desc = document.getElementById("UITourTooltipDescription");
+ let icon = document.getElementById("UITourTooltipIcon");
+
+ await showInfoPromise(
+ "urlbar",
+ "another title",
+ "moar text",
+ "./image.png",
+ "makeButtons"
+ );
+
+ is(title.textContent, "another title", "Popup should have correct title");
+ is(
+ desc.textContent,
+ "moar text",
+ "Popup should have correct description text"
+ );
+
+ let imageURL = getRootDirectory(gTestPath) + "image.png";
+ imageURL = imageURL.replace(
+ "chrome://mochitests/content/",
+ "https://example.org/"
+ );
+ is(icon.src, imageURL, "Popup should have correct icon shown");
+
+ let buttons = document.getElementById("UITourTooltipButtons");
+ is(buttons.childElementCount, 4, "Popup should have four buttons");
+
+ is(buttons.children[0].nodeName, "label", "Text label should be a <label>");
+ is(
+ buttons.children[0].getAttribute("value"),
+ "Regular text",
+ "Text label should have correct value"
+ );
+ is(
+ buttons.children[0].getAttribute("image"),
+ "",
+ "Text should have no image"
+ );
+ is(buttons.children[0].className, "", "Text should have no class");
+
+ is(buttons.children[1].nodeName, "button", "Link should be a <button>");
+ is(
+ buttons.children[1].getAttribute("label"),
+ "Link",
+ "Link should have correct label"
+ );
+ is(
+ buttons.children[1].getAttribute("image"),
+ "",
+ "Link should have no image"
+ );
+ is(buttons.children[1].className, "button-link", "Check link class");
+
+ is(buttons.children[2].nodeName, "button", "Button 1 should be a <button>");
+ is(
+ buttons.children[2].getAttribute("label"),
+ "Button 1",
+ "First button should have correct label"
+ );
+ is(
+ buttons.children[2].getAttribute("image"),
+ "",
+ "First button should have no image"
+ );
+ is(buttons.children[2].className, "", "Button 1 should have no class");
+
+ is(buttons.children[3].nodeName, "button", "Button 2 should be a <button>");
+ is(
+ buttons.children[3].getAttribute("label"),
+ "Button 2",
+ "Second button should have correct label"
+ );
+ is(
+ buttons.children[3].getAttribute("image"),
+ imageURL,
+ "Second button should have correct image"
+ );
+ is(buttons.children[3].className, "button-primary", "Check button 2 class");
+
+ let promiseHidden = promisePanelElementHidden(window, popup);
+ EventUtils.synthesizeMouseAtCenter(buttons.children[2], {}, window);
+ await promiseHidden;
+
+ ok(true, "Popup should close automatically");
+
+ let returnValue = await waitForCallbackResultPromise();
+ is(returnValue.result, "button1", "Correct callback should have been called");
+});
+
+add_UITour_task(async function test_info_buttons_2() {
+ let popup = document.getElementById("UITourTooltip");
+ let title = document.getElementById("UITourTooltipTitle");
+ let desc = document.getElementById("UITourTooltipDescription");
+ let icon = document.getElementById("UITourTooltipIcon");
+
+ await showInfoPromise(
+ "urlbar",
+ "another title",
+ "moar text",
+ "./image.png",
+ "makeButtons"
+ );
+
+ is(title.textContent, "another title", "Popup should have correct title");
+ is(
+ desc.textContent,
+ "moar text",
+ "Popup should have correct description text"
+ );
+
+ let imageURL = getRootDirectory(gTestPath) + "image.png";
+ imageURL = imageURL.replace(
+ "chrome://mochitests/content/",
+ "https://example.org/"
+ );
+ is(icon.src, imageURL, "Popup should have correct icon shown");
+
+ let buttons = document.getElementById("UITourTooltipButtons");
+ is(buttons.childElementCount, 4, "Popup should have four buttons");
+
+ is(
+ buttons.children[1].getAttribute("label"),
+ "Link",
+ "Link should have correct label"
+ );
+ is(
+ buttons.children[1].getAttribute("image"),
+ "",
+ "Link should have no image"
+ );
+ ok(
+ buttons.children[1].classList.contains("button-link"),
+ "Link should have button-link class"
+ );
+
+ is(
+ buttons.children[2].getAttribute("label"),
+ "Button 1",
+ "First button should have correct label"
+ );
+ is(
+ buttons.children[2].getAttribute("image"),
+ "",
+ "First button should have no image"
+ );
+
+ is(
+ buttons.children[3].getAttribute("label"),
+ "Button 2",
+ "Second button should have correct label"
+ );
+ is(
+ buttons.children[3].getAttribute("image"),
+ imageURL,
+ "Second button should have correct image"
+ );
+
+ let promiseHidden = promisePanelElementHidden(window, popup);
+ EventUtils.synthesizeMouseAtCenter(buttons.children[3], {}, window);
+ await promiseHidden;
+
+ ok(true, "Popup should close automatically");
+
+ let returnValue = await waitForCallbackResultPromise();
+
+ is(returnValue.result, "button2", "Correct callback should have been called");
+});
+
+add_UITour_task(async function test_info_close_button() {
+ let closeButton = document.getElementById("UITourTooltipClose");
+
+ await showInfoPromise(
+ "urlbar",
+ "Close me",
+ "X marks the spot",
+ null,
+ null,
+ "makeInfoOptions"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(closeButton, {}, window);
+
+ let returnValue = await waitForCallbackResultPromise();
+
+ is(returnValue.result, "closeButton", "Close button callback called");
+});
+
+add_UITour_task(async function test_info_target_callback() {
+ let popup = document.getElementById("UITourTooltip");
+
+ await showInfoPromise(
+ "appMenu",
+ "I want to know when the target is clicked",
+ "*click*",
+ null,
+ null,
+ "makeInfoOptions"
+ );
+
+ await gCUITestUtils.openMainMenu();
+
+ let returnValue = await waitForCallbackResultPromise();
+
+ is(returnValue.result, "target", "target callback called");
+ is(
+ returnValue.data.target,
+ "appMenu",
+ "target callback was from the appMenu"
+ );
+ is(
+ returnValue.data.type,
+ "popupshown",
+ "target callback was from the mousedown"
+ );
+
+ // Cleanup.
+ await hideInfoPromise();
+
+ popup.removeAttribute("animate");
+});
+
+add_UITour_task(async function test_getConfiguration_selectedSearchEngine() {
+ let engine = await Services.search.getDefault();
+ let data = await getConfigurationPromise("selectedSearchEngine");
+ is(
+ data.searchEngineIdentifier,
+ engine.identifier,
+ "Correct engine identifier"
+ );
+});
+
+add_UITour_task(async function test_setSearchTerm() {
+ // Place the search bar in the navigation toolbar temporarily.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.widget.inNavBar", true]],
+ });
+
+ const TERM = "UITour Search Term";
+ await gContentAPI.setSearchTerm(TERM);
+
+ let searchbar = document.getElementById("searchbar");
+ // The UITour gets to the searchbar element through a promise, so the value setting
+ // only happens after a tick.
+ await waitForConditionPromise(
+ () => searchbar.value == TERM,
+ "Correct term set"
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_UITour_task(async function test_clearSearchTerm() {
+ // Place the search bar in the navigation toolbar temporarily.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.widget.inNavBar", true]],
+ });
+
+ await gContentAPI.setSearchTerm("");
+
+ let searchbar = document.getElementById("searchbar");
+ // The UITour gets to the searchbar element through a promise, so the value setting
+ // only happens after a tick.
+ await waitForConditionPromise(
+ () => searchbar.value == "",
+ "Search term cleared"
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/uitour/test/browser_UITour4.js b/browser/components/uitour/test/browser_UITour4.js
new file mode 100644
index 0000000000..718a5331c7
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour4.js
@@ -0,0 +1,235 @@
+"use strict";
+
+var gTestTab;
+var gContentAPI;
+
+add_task(setup_UITourTest);
+
+add_UITour_task(
+ async function test_highligh_between_buttonOnAppMenu_and_buttonOnPageActionPanel() {
+ let highlight = document.getElementById("UITourHighlight");
+ is_element_hidden(highlight, "Highlight should initially be hidden");
+
+ let appMenu = window.PanelUI.panel;
+ let pageActionPanel = BrowserPageActions.panelNode;
+
+ // Test highlighting the addons button on the app menu
+ let appMenuShownPromise = promisePanelElementShown(window, appMenu);
+ let highlightVisiblePromise = elementVisiblePromise(
+ highlight,
+ "Should show highlight"
+ );
+ gContentAPI.showHighlight("addons");
+ await appMenuShownPromise;
+ await highlightVisiblePromise;
+ is(
+ appMenu.state,
+ "open",
+ "Should open the app menu to highlight the addons button"
+ );
+ is(pageActionPanel.state, "closed", "Shouldn't open the page action panel");
+ is(
+ getShowHighlightTargetName(),
+ "addons",
+ "Should highlight the addons button on the app menu"
+ );
+ }
+);
+
+add_UITour_task(
+ async function test_showInfo_between_buttonOnPageActionPanel_and_buttonOnAppMenu() {
+ let tooltip = document.getElementById("UITourTooltip");
+ is_element_hidden(tooltip, "Tooltip should initially be hidden");
+
+ let appMenu = window.PanelUI.panel;
+ let pageActionPanel = BrowserPageActions.panelNode;
+ let tooltipVisiblePromise = elementVisiblePromise(
+ tooltip,
+ "Should show info tooltip"
+ );
+
+ let appMenuShownPromise = promisePanelElementShown(window, appMenu);
+ await showInfoPromise("addons", "title", "text");
+ await appMenuShownPromise;
+ await tooltipVisiblePromise;
+ is(
+ appMenu.state,
+ "open",
+ "Should open the app menu to show info on the addons button"
+ );
+ is(
+ pageActionPanel.state,
+ "closed",
+ "Should close the page action panel after no more show info for the copyURL button"
+ );
+ is(
+ getShowInfoTargetName(),
+ "addons",
+ "Should show info tooltip on the addons button on the app menu"
+ );
+
+ // Test hiding info tooltip
+ let appMenuHiddenPromise = promisePanelElementHidden(window, appMenu);
+ let tooltipHiddenPromise = elementHiddenPromise(
+ tooltip,
+ "Should hide info"
+ );
+ gContentAPI.hideInfo();
+ await appMenuHiddenPromise;
+ await tooltipHiddenPromise;
+ is(appMenu.state, "closed", "Should close the app menu after hiding info");
+ is(
+ pageActionPanel.state,
+ "closed",
+ "Shouldn't open the page action panel after hiding info"
+ );
+ }
+);
+
+add_UITour_task(
+ async function test_highlight_buttonOnPageActionPanel_and_showInfo_buttonOnAppMenu() {
+ let highlight = document.getElementById("UITourHighlight");
+ is_element_hidden(highlight, "Highlight should initially be hidden");
+ let tooltip = document.getElementById("UITourTooltip");
+ is_element_hidden(tooltip, "Tooltip should initially be hidden");
+
+ let appMenu = window.PanelUI.panel;
+ let pageActionPanel = BrowserPageActions.panelNode;
+ let pageActionPanelHiddenPromise = Promise.resolve();
+
+ // Test showing info tooltip on the privateWindow button on the app menu
+ let appMenuShownPromise = promisePanelElementShown(window, appMenu);
+ let tooltipVisiblePromise = elementVisiblePromise(
+ tooltip,
+ "Should show info tooltip"
+ );
+ let highlightHiddenPromise = elementHiddenPromise(
+ highlight,
+ "Should hide highlight"
+ );
+ await showInfoPromise("privateWindow", "title", "text");
+ await appMenuShownPromise;
+ await tooltipVisiblePromise;
+ await pageActionPanelHiddenPromise;
+ await highlightHiddenPromise;
+ is(
+ appMenu.state,
+ "open",
+ "Should open the app menu to show info on the privateWindow button"
+ );
+ is(pageActionPanel.state, "closed", "Should close the page action panel");
+ is(
+ getShowInfoTargetName(),
+ "privateWindow",
+ "Should show info tooltip on the privateWindow button on the app menu"
+ );
+
+ // Test hiding info tooltip
+ let appMenuHiddenPromise = promisePanelElementHidden(window, appMenu);
+ let tooltipHiddenPromise = elementHiddenPromise(
+ tooltip,
+ "Should hide info"
+ );
+ gContentAPI.hideInfo();
+ await appMenuHiddenPromise;
+ await tooltipHiddenPromise;
+ is(
+ appMenu.state,
+ "closed",
+ "Should close the app menu after hiding info tooltip"
+ );
+ }
+);
+
+add_UITour_task(
+ async function test_showInfo_buttonOnAppMenu_and_highlight_buttonOnPageActionPanel() {
+ let highlight = document.getElementById("UITourHighlight");
+ is_element_hidden(highlight, "Highlight should initially be hidden");
+ let tooltip = document.getElementById("UITourTooltip");
+ is_element_hidden(tooltip, "Tooltip should initially be hidden");
+
+ let appMenu = window.PanelUI.panel;
+ let pageActionPanel = BrowserPageActions.panelNode;
+
+ // Test showing info tooltip on the privateWindow button on the app menu
+ let appMenuShownPromise = promisePanelElementShown(window, appMenu);
+ let tooltipVisiblePromise = elementVisiblePromise(
+ tooltip,
+ "Should show info tooltip"
+ );
+ await showInfoPromise("privateWindow", "title", "text");
+ await appMenuShownPromise;
+ await tooltipVisiblePromise;
+ is(
+ appMenu.state,
+ "open",
+ "Should open the app menu to show info on the privateWindow button"
+ );
+ is(pageActionPanel.state, "closed", "Shouldn't open the page action panel");
+ is(
+ getShowInfoTargetName(),
+ "privateWindow",
+ "Should show info tooltip on the privateWindow button on the app menu"
+ );
+ }
+);
+
+add_UITour_task(
+ async function test_show_pageActionPanel_and_showInfo_buttonOnAppMenu() {
+ let tooltip = document.getElementById("UITourTooltip");
+ is_element_hidden(tooltip, "Tooltip should initially be hidden");
+
+ let appMenu = window.PanelUI.panel;
+ let pageActionPanel = BrowserPageActions.panelNode;
+
+ // Test showing info tooltip on the privateWindow button on the app menu
+ let appMenuShownPromise = promisePanelElementShown(window, appMenu);
+ let tooltipVisiblePromise = elementVisiblePromise(
+ tooltip,
+ "Should show info tooltip"
+ );
+ await showInfoPromise("privateWindow", "title", "text");
+ await appMenuShownPromise;
+ await tooltipVisiblePromise;
+ is(
+ appMenu.state,
+ "open",
+ "Should open the app menu to show info on the privateWindow button"
+ );
+ is(
+ pageActionPanel.state,
+ "closed",
+ "Check state of the page action panel if it was opened explictly by api user."
+ );
+ is(
+ getShowInfoTargetName(),
+ "privateWindow",
+ "Should show info tooltip on the privateWindow button on the app menu"
+ );
+
+ is_element_visible(tooltip, "Tooltip should still be visible");
+ is(appMenu.state, "open", "Shouldn't close the app menu");
+ is(
+ pageActionPanel.state,
+ "closed",
+ "Should close the page action panel after hideMenu"
+ );
+ is(
+ getShowInfoTargetName(),
+ "privateWindow",
+ "Should still show info tooltip on the privateWindow button on the app menu"
+ );
+
+ // Test hiding info tooltip
+ let appMenuHiddenPromise = promisePanelElementHidden(window, appMenu);
+ let tooltipHiddenPromise = elementHiddenPromise(
+ tooltip,
+ "Should hide info"
+ );
+ gContentAPI.hideInfo();
+ await appMenuHiddenPromise;
+ await tooltipHiddenPromise;
+ is(appMenu.state, "closed", "Should close the app menu after hideInfo");
+ is(pageActionPanel.state, "closed", "Shouldn't open the page action panel");
+ }
+);
diff --git a/browser/components/uitour/test/browser_UITour5.js b/browser/components/uitour/test/browser_UITour5.js
new file mode 100644
index 0000000000..50316d4225
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour5.js
@@ -0,0 +1,60 @@
+"use strict";
+
+var gTestTab;
+var gContentAPI;
+
+add_task(setup_UITourTest);
+
+add_UITour_task(async function test_highlight_help_and_show_help_subview() {
+ let highlight = document.getElementById("UITourHighlight");
+ is_element_hidden(highlight, "Highlight should initially be hidden");
+
+ // Test highlighting the library button
+ let appMenu = PanelUI.panel;
+ let appMenuShownPromise = promisePanelElementShown(window, appMenu);
+ let highlightVisiblePromise = elementVisiblePromise(
+ highlight,
+ "Should show highlight"
+ );
+ gContentAPI.showHighlight("help");
+ await appMenuShownPromise;
+ await highlightVisiblePromise;
+ is(
+ appMenu.state,
+ "open",
+ "Should open the app menu to highlight the help button"
+ );
+ is(
+ getShowHighlightTargetName(),
+ "help",
+ "Should highlight the help button on the app menu"
+ );
+
+ // Click the help button to show the subview
+ let ViewShownPromise = new Promise(resolve => {
+ appMenu.addEventListener("ViewShown", resolve, { once: true });
+ });
+ let highlightHiddenPromise = elementHiddenPromise(
+ highlight,
+ "Should hide highlight"
+ );
+
+ let helpButtonID = "appMenu-help-button2";
+ let helpBtn = document.getElementById(helpButtonID);
+ helpBtn.dispatchEvent(new Event("command"));
+ await highlightHiddenPromise;
+ await ViewShownPromise;
+ let helpView = document.getElementById("PanelUI-helpView");
+ ok(PanelView.forNode(helpView).active, "Should show the help subview");
+ is(
+ appMenu.state,
+ "open",
+ "Should still open the app menu for the help subview"
+ );
+
+ // Clean up
+ let appMenuHiddenPromise = promisePanelElementHidden(window, appMenu);
+ gContentAPI.hideMenu("appMenu");
+ await appMenuHiddenPromise;
+ is(appMenu.state, "closed", "Should close the app menu");
+});
diff --git a/browser/components/uitour/test/browser_UITour_annotation_size_attributes.js b/browser/components/uitour/test/browser_UITour_annotation_size_attributes.js
new file mode 100644
index 0000000000..21742a5951
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour_annotation_size_attributes.js
@@ -0,0 +1,65 @@
+/*
+ * Test that width and height attributes don't get set by widget code on the highlight panel.
+ */
+
+"use strict";
+
+var gTestTab;
+var gContentAPI;
+var highlight = UITour.getHighlightContainerAndMaybeCreate(document);
+var tooltip = UITour.getTooltipAndMaybeCreate(document);
+
+add_task(setup_UITourTest);
+
+add_UITour_task(async function test_highlight_size_attributes() {
+ await gContentAPI.showHighlight("appMenu");
+ await elementVisiblePromise(
+ highlight,
+ "Highlight should be shown after showHighlight() for the appMenu"
+ );
+ await gContentAPI.showHighlight("urlbar");
+ await elementVisiblePromise(
+ highlight,
+ "Highlight should be moved to the urlbar"
+ );
+ await new Promise(resolve => {
+ SimpleTest.executeSoon(() => {
+ is(
+ highlight.style.height,
+ "",
+ "Highlight panel should have no explicit height set"
+ );
+ is(
+ highlight.style.width,
+ "",
+ "Highlight panel should have no explicit width set"
+ );
+ resolve();
+ });
+ });
+});
+
+add_UITour_task(async function test_info_size_attributes() {
+ await gContentAPI.showInfo("appMenu", "test title", "test text");
+ await elementVisiblePromise(
+ tooltip,
+ "Tooltip should be shown after showInfo() for the appMenu"
+ );
+ await gContentAPI.showInfo("urlbar", "new title", "new text");
+ await elementVisiblePromise(tooltip, "Tooltip should be moved to the urlbar");
+ await new Promise(resolve => {
+ SimpleTest.executeSoon(() => {
+ is(
+ tooltip.style.height,
+ "",
+ "Info panel should have no explicit height set"
+ );
+ is(
+ tooltip.style.width,
+ "",
+ "Info panel should have no explicit width set"
+ );
+ resolve();
+ });
+ });
+});
diff --git a/browser/components/uitour/test/browser_UITour_availableTargets.js b/browser/components/uitour/test/browser_UITour_availableTargets.js
new file mode 100644
index 0000000000..0e9ac45513
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour_availableTargets.js
@@ -0,0 +1,129 @@
+"use strict";
+
+var gTestTab;
+var gContentAPI;
+
+var hasPocket = Services.prefs.getBoolPref("extensions.pocket.enabled");
+var hasQuit = AppConstants.platform != "macosx";
+
+requestLongerTimeout(2);
+
+function getExpectedTargets() {
+ return [
+ "accountStatus",
+ "addons",
+ "appMenu",
+ "backForward",
+ "help",
+ "logins",
+ "pageAction-bookmark",
+ ...(hasPocket ? ["pocket"] : []),
+ "privateWindow",
+ ...(hasQuit ? ["quit"] : []),
+ "readerMode-urlBar",
+ "urlbar",
+ ];
+}
+
+add_task(setup_UITourTest);
+
+add_UITour_task(async function test_availableTargets() {
+ let data = await getConfigurationPromise("availableTargets");
+ let expecteds = getExpectedTargets();
+ ok_targets(data, expecteds);
+ ok(UITour.availableTargetsCache.has(window), "Targets should now be cached");
+});
+
+add_UITour_task(async function test_availableTargets_changeWidgets() {
+ CustomizableUI.addWidgetToArea(
+ "bookmarks-menu-button",
+ CustomizableUI.AREA_NAVBAR,
+ 0
+ );
+ ok(
+ !UITour.availableTargetsCache.has(window),
+ "Targets should be evicted from cache after widget change"
+ );
+ let data = await getConfigurationPromise("availableTargets");
+ let expecteds = getExpectedTargets();
+ expecteds = ["bookmarks", ...expecteds];
+ ok_targets(data, expecteds);
+
+ ok(
+ UITour.availableTargetsCache.has(window),
+ "Targets should now be cached again"
+ );
+ CustomizableUI.reset();
+ ok(
+ !UITour.availableTargetsCache.has(window),
+ "Targets should not be cached after reset"
+ );
+});
+
+add_UITour_task(async function test_availableTargets_search() {
+ Services.prefs.setBoolPref("browser.search.widget.inNavBar", true);
+ try {
+ let data = await getConfigurationPromise("availableTargets");
+ let expecteds = getExpectedTargets();
+ expecteds = ["search", "searchIcon", ...expecteds];
+ ok_targets(data, expecteds);
+ } finally {
+ Services.prefs.clearUserPref("browser.search.widget.inNavBar");
+ }
+});
+
+function ok_targets(actualData, expectedTargets) {
+ // Depending on how soon after page load this is called, the selected tab icon
+ // may or may not be showing the loading throbber. We can't be sure whether
+ // it appears in the list of targets, so remove it.
+ let index = actualData.targets.indexOf("selectedTabIcon");
+ if (index != -1) {
+ actualData.targets.splice(index, 1);
+ }
+
+ ok(Array.isArray(actualData.targets), "data.targets should be an array");
+ actualData.targets.sort();
+ expectedTargets.sort();
+ Assert.deepEqual(
+ actualData.targets,
+ expectedTargets,
+ "Targets should be as expected"
+ );
+ if (actualData.targets.toString() != expectedTargets.toString()) {
+ for (let actualItem of actualData.targets) {
+ if (!expectedTargets.includes(actualItem)) {
+ ok(false, `${actualItem} was an unexpected target.`);
+ }
+ }
+ for (let expectedItem of expectedTargets) {
+ if (!actualData.targets.includes(expectedItem)) {
+ ok(false, `${expectedItem} should have been a target.`);
+ }
+ }
+ }
+}
+
+async function assertTargetNode(targetName, expectedNodeId) {
+ let target = await UITour.getTarget(window, targetName);
+ is(target.node.id, expectedNodeId, "UITour should get the right target node");
+}
+
+var pageActionsHelper = {
+ setActionsUrlbarState(inUrlbar) {
+ this._originalStates = [];
+ PageActions._actionsByID.forEach(action => {
+ this._originalStates.push([action, action.pinnedToUrlbar]);
+ action.pinnedToUrlbar = inUrlbar;
+ });
+ },
+
+ restoreActionsUrlbarState() {
+ if (!this._originalStates) {
+ return;
+ }
+ for (let [action, originalState] of this._originalStates) {
+ action.pinnedToUrlbar = originalState;
+ }
+ this._originalStates = null;
+ },
+};
diff --git a/browser/components/uitour/test/browser_UITour_colorway.js b/browser/components/uitour/test/browser_UITour_colorway.js
new file mode 100644
index 0000000000..ede28a38b0
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour_colorway.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var gTestTab;
+var gContentAPI;
+add_task(setup_UITourTest);
+
+// Tests assume there's at least 1 builtin theme with colorway id.
+const { BuiltInThemes } = ChromeUtils.importESModule(
+ "resource:///modules/BuiltInThemes.sys.mjs"
+);
+const COLORWAY_IDS = [...BuiltInThemes.builtInThemeMap.keys()].filter(
+ id =>
+ id.endsWith("-colorway@mozilla.org") && !BuiltInThemes.themeIsExpired(id)
+);
+
+add_UITour_task(async function test_getColorways() {
+ const data = await getConfigurationPromise("colorway");
+
+ ok(
+ Array.isArray(data),
+ "getConfiguration result should be an array of colorways"
+ );
+});
+
+add_UITour_task(async function test_setColorway_unknown() {
+ await gContentAPI.setConfiguration("colorway", "unknown");
+
+ ok(
+ (await AddonManager.getAddonByID("default-theme@mozilla.org")).isActive,
+ "gContentAPI did not activate unknown colorway"
+ );
+});
+
+add_UITour_task(async function test_setColorway() {
+ const id = COLORWAY_IDS.at(0);
+ if (!id) {
+ info("No colorways to test");
+ return;
+ }
+
+ await gContentAPI.setConfiguration("colorway", id);
+
+ ok(
+ (await AddonManager.getAddonByID(id)).isActive,
+ `gContentAPI activated colorway ${id}`
+ );
+});
+
+add_UITour_task(async function test_anotherColorway() {
+ const id = COLORWAY_IDS.at(-1);
+ if (!id) {
+ info("No colorways to test");
+ return;
+ }
+
+ await gContentAPI.setConfiguration("colorway", id);
+
+ ok(
+ (await AddonManager.getAddonByID(id)).isActive,
+ `gContentAPI activated another colorway ${id}`
+ );
+});
+
+add_UITour_task(async function test_resetColorway() {
+ await gContentAPI.setConfiguration("colorway");
+
+ ok(
+ (await AddonManager.getAddonByID("default-theme@mozilla.org")).isActive,
+ "gContentAPI reset colorway to original theme"
+ );
+});
diff --git a/browser/components/uitour/test/browser_UITour_defaultBrowser.js b/browser/components/uitour/test/browser_UITour_defaultBrowser.js
new file mode 100644
index 0000000000..b05e988a67
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour_defaultBrowser.js
@@ -0,0 +1,66 @@
+"use strict";
+
+var gTestTab;
+var gContentAPI;
+var setDefaultBrowserCalled = false;
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/MockObjects.js",
+ this
+);
+
+function MockShellService() {}
+MockShellService.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIShellService"]),
+ isDefaultBrowser(aStartupCheck, aForAllTypes) {
+ return false;
+ },
+ setDefaultBrowser(aClaimAllTypes, aForAllUsers) {
+ setDefaultBrowserCalled = true;
+ },
+ shouldCheckDefaultBrowser: false,
+ canSetDesktopBackground: false,
+ BACKGROUND_TILE: 1,
+ BACKGROUND_STRETCH: 2,
+ BACKGROUND_CENTER: 3,
+ BACKGROUND_FILL: 4,
+ BACKGROUND_FIT: 5,
+ BACKGROUND_SPAN: 6,
+ setDesktopBackground(aElement, aPosition) {},
+ desktopBackgroundColor: 0,
+};
+
+var mockShellService = new MockObjectRegisterer(
+ "@mozilla.org/browser/shell-service;1",
+ MockShellService
+);
+
+// Temporarily disabled, see note at test_setDefaultBrowser.
+// mockShellService.register();
+
+add_task(setup_UITourTest);
+
+/* This test is disabled (bug 1180714) since the MockObjectRegisterer
+ is not actually replacing the original ShellService.
+add_UITour_task(function* test_setDefaultBrowser() {
+ try {
+ yield gContentAPI.setConfiguration("defaultBrowser");
+ ok(setDefaultBrowserCalled, "setDefaultBrowser called");
+ } finally {
+ mockShellService.unregister();
+ }
+});
+*/
+
+add_UITour_task(async function test_isDefaultBrowser() {
+ let shell = Cc["@mozilla.org/browser/shell-service;1"].getService(
+ Ci.nsIShellService
+ );
+ let isDefault = shell.isDefaultBrowser(false);
+ let data = await getConfigurationPromise("appinfo");
+ is(
+ isDefault,
+ data.defaultBrowser,
+ "gContentAPI result should match shellService.isDefaultBrowser"
+ );
+});
diff --git a/browser/components/uitour/test/browser_UITour_detach_tab.js b/browser/components/uitour/test/browser_UITour_detach_tab.js
new file mode 100644
index 0000000000..8e4256841b
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour_detach_tab.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Detaching a tab to a new window shouldn't break the menu panel.
+ */
+
+"use strict";
+
+var gTestTab;
+var gContentAPI;
+var gContentDoc;
+
+var detachedWindow;
+
+function test() {
+ registerCleanupFunction(function () {
+ gContentDoc = null;
+ });
+ UITourTest();
+}
+
+/**
+ * When tab is changed we're tearing the tour down. So the UITour client has to always be aware of this
+ * fact and therefore listens to pageshow events.
+ * In particular this scenario happens for detaching the tab (ie. moving it to a new window).
+ */
+var tests = [
+ taskify(async function test_move_tab_to_new_window() {
+ const myDocIdentifier =
+ "Hello, I'm a unique expando to identify this document.";
+
+ let highlight = document.getElementById("UITourHighlight");
+
+ let browserStartupDeferred = PromiseUtils.defer();
+ Services.obs.addObserver(function onBrowserDelayedStartup(aWindow) {
+ Services.obs.removeObserver(
+ onBrowserDelayedStartup,
+ "browser-delayed-startup-finished"
+ );
+ browserStartupDeferred.resolve(aWindow);
+ }, "browser-delayed-startup-finished");
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [myDocIdentifier],
+ contentMyDocIdentifier => {
+ let onPageShow = () => {
+ if (!content.document.hidden) {
+ let win = Cu.waiveXrays(content);
+ win.Mozilla.UITour.showHighlight("appMenu");
+ }
+ };
+ content.window.addEventListener("pageshow", onPageShow, {
+ mozSystemGroup: true,
+ });
+ content.document.myExpando = contentMyDocIdentifier;
+ }
+ );
+ gContentAPI.showHighlight("appMenu");
+
+ await elementVisiblePromise(highlight, "old window highlight");
+
+ detachedWindow = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
+ await browserStartupDeferred.promise;
+
+ // This highlight should be shown thanks to the pageshow listener.
+ let newWindowHighlight = UITour.getHighlightAndMaybeCreate(
+ detachedWindow.document
+ );
+ await elementVisiblePromise(newWindowHighlight, "new window highlight");
+
+ let selectedTab = detachedWindow.gBrowser.selectedTab;
+ await SpecialPowers.spawn(
+ selectedTab.linkedBrowser,
+ [myDocIdentifier],
+ contentMyDocIdentifier => {
+ is(
+ content.document.myExpando,
+ contentMyDocIdentifier,
+ "Document should be selected in new window"
+ );
+ }
+ );
+ ok(
+ UITour.tourBrowsersByWindow &&
+ UITour.tourBrowsersByWindow.has(detachedWindow),
+ "Window should be known"
+ );
+ ok(
+ UITour.tourBrowsersByWindow
+ .get(detachedWindow)
+ .has(selectedTab.linkedBrowser),
+ "Selected browser should be known"
+ );
+
+ // Need this because gContentAPI in e10s land will try to use gTestTab to
+ // spawn a content task, which doesn't work if the tab is dead, for obvious
+ // reasons.
+ gTestTab = detachedWindow.gBrowser.selectedTab;
+
+ let shownPromise = promisePanelShown(detachedWindow);
+ gContentAPI.showMenu("appMenu");
+ await shownPromise;
+
+ isnot(detachedWindow.PanelUI.panel.state, "closed", "Panel should be open");
+ gContentAPI.hideHighlight();
+ gContentAPI.hideMenu("appMenu");
+ gTestTab = null;
+
+ await BrowserTestUtils.closeWindow(detachedWindow);
+ }),
+];
diff --git a/browser/components/uitour/test/browser_UITour_forceReaderMode.js b/browser/components/uitour/test/browser_UITour_forceReaderMode.js
new file mode 100644
index 0000000000..b69956c447
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour_forceReaderMode.js
@@ -0,0 +1,24 @@
+"use strict";
+
+var gTestTab;
+var gContentAPI;
+
+add_task(setup_UITourTest);
+
+add_UITour_task(async function () {
+ ok(
+ !gBrowser.selectedBrowser.isArticle,
+ "Should not be an article when we start"
+ );
+ ok(
+ document.getElementById("reader-mode-button").hidden,
+ "Button should be hidden."
+ );
+ await gContentAPI.forceShowReaderIcon();
+ await waitForConditionPromise(() => gBrowser.selectedBrowser.isArticle);
+ ok(gBrowser.selectedBrowser.isArticle, "Should suddenly be an article.");
+ ok(
+ !document.getElementById("reader-mode-button").hidden,
+ "Button should now be visible."
+ );
+});
diff --git a/browser/components/uitour/test/browser_UITour_modalDialog.js b/browser/components/uitour/test/browser_UITour_modalDialog.js
new file mode 100644
index 0000000000..397269bcb9
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour_modalDialog.js
@@ -0,0 +1,116 @@
+"use strict";
+
+/* eslint-disable mozilla/use-chromeutils-generateqi */
+
+var gTestTab;
+var gContentAPI;
+var handleDialog;
+
+// Modified from toolkit/components/passwordmgr/test/prompt_common.js
+var didDialog;
+
+var timer; // keep in outer scope so it's not GC'd before firing
+function startCallbackTimer() {
+ didDialog = false;
+
+ // Delay before the callback twiddles the prompt.
+ const dialogDelay = 10;
+
+ // Use a timer to invoke a callback to twiddle the authentication dialog
+ timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.init(observer, dialogDelay, Ci.nsITimer.TYPE_ONE_SHOT);
+}
+
+var observer = SpecialPowers.wrapCallbackObject({
+ QueryInterface(iid) {
+ const interfaces = [
+ Ci.nsIObserver,
+ Ci.nsISupports,
+ Ci.nsISupportsWeakReference,
+ ];
+
+ if (
+ !interfaces.some(function (v) {
+ return iid.equals(v);
+ })
+ ) {
+ throw SpecialPowers.Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ return this;
+ },
+
+ observe(subject, topic, data) {
+ var doc = getDialogDoc();
+ if (doc) {
+ handleDialog(doc);
+ } else {
+ startCallbackTimer();
+ } // try again in a bit
+ },
+});
+
+function getDialogDoc() {
+ // Find the <browser> which contains notifyWindow, by looking
+ // through all the open windows and all the <browsers> in each.
+
+ // var enumerator = wm.getEnumerator("navigator:browser");
+ for (let { docShell } of Services.wm.getEnumerator(null)) {
+ var containedDocShells = docShell.getAllDocShellsInSubtree(
+ docShell.typeChrome,
+ docShell.ENUMERATE_FORWARDS
+ );
+ for (let childDocShell of containedDocShells) {
+ // Get the corresponding document for this docshell
+ // We don't want it if it's not done loading.
+ if (childDocShell.busyFlags != Ci.nsIDocShell.BUSY_FLAGS_NONE) {
+ continue;
+ }
+ var childDoc = childDocShell.contentViewer.DOMDocument;
+
+ // ok(true, "Got window: " + childDoc.location.href);
+ if (
+ childDoc.location.href == "chrome://global/content/commonDialog.xhtml"
+ ) {
+ return childDoc;
+ }
+ }
+ }
+
+ return null;
+}
+
+function test() {
+ UITourTest();
+}
+
+var tests = [
+ taskify(async function test_modal_dialog_while_opening_tooltip() {
+ let panelShown;
+ let popup;
+
+ handleDialog = doc => {
+ popup = document.getElementById("UITourTooltip");
+ gContentAPI.showInfo("appMenu", "test title", "test text");
+ doc.defaultView.setTimeout(function () {
+ is(
+ popup.state,
+ "closed",
+ "Popup shouldn't be shown while dialog is up"
+ );
+ panelShown = promisePanelElementShown(window, popup);
+ let dialog = doc.getElementById("commonDialog");
+ dialog.acceptDialog();
+ }, 1000);
+ };
+ startCallbackTimer();
+ executeSoon(() => alert("test"));
+ await waitForConditionPromise(
+ () => panelShown,
+ "Timed out waiting for panel promise to be assigned",
+ 100
+ );
+ await panelShown;
+
+ await hideInfoPromise();
+ }),
+];
diff --git a/browser/components/uitour/test/browser_UITour_observe.js b/browser/components/uitour/test/browser_UITour_observe.js
new file mode 100644
index 0000000000..d9ecf6fc7d
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour_observe.js
@@ -0,0 +1,99 @@
+"use strict";
+
+var gTestTab;
+var gContentAPI;
+
+function test() {
+ requestLongerTimeout(2);
+ UITourTest();
+}
+
+var tests = [
+ function test_no_params(done) {
+ function listener(event, params) {
+ is(event, "test-event-1", "Correct event name");
+ ok(!params, "No param object");
+ gContentAPI.observe(null);
+ done();
+ }
+
+ gContentAPI.observe(listener, () => {
+ UITour.notify("test-event-1");
+ });
+ },
+ function test_param_string(done) {
+ function listener(event, params) {
+ is(event, "test-event-2", "Correct event name");
+ is(params, "a param", "Correct param string");
+ gContentAPI.observe(null);
+ done();
+ }
+
+ gContentAPI.observe(listener, () => {
+ UITour.notify("test-event-2", "a param");
+ });
+ },
+ function test_param_object(done) {
+ function listener(event, params) {
+ is(event, "test-event-3", "Correct event name");
+ is(
+ JSON.stringify(params),
+ JSON.stringify({ key: "something" }),
+ "Correct param object"
+ );
+ gContentAPI.observe(null);
+ done();
+ }
+
+ gContentAPI.observe(listener, () => {
+ UITour.notify("test-event-3", { key: "something" });
+ });
+ },
+ function test_background_tab(done) {
+ function listener(event, params) {
+ is(event, "test-event-background-1", "Correct event name");
+ ok(!params, "No param object");
+ gContentAPI.observe(null);
+ gBrowser.removeCurrentTab();
+ done();
+ }
+
+ gContentAPI.observe(listener, () => {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+ isnot(
+ gBrowser.selectedTab,
+ gTestTab,
+ "Make sure the selected tab changed"
+ );
+
+ UITour.notify("test-event-background-1");
+ });
+ },
+ // Make sure the tab isn't torn down when switching back to the tour one.
+ function test_background_then_foreground_tab(done) {
+ let blankTab = null;
+ function listener(event, params) {
+ is(event, "test-event-4", "Correct event name");
+ ok(!params, "No param object");
+ gContentAPI.observe(null);
+ gBrowser.removeTab(blankTab);
+ done();
+ }
+
+ gContentAPI.observe(listener, () => {
+ blankTab = gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:blank"
+ );
+ isnot(
+ gBrowser.selectedTab,
+ gTestTab,
+ "Make sure the selected tab changed"
+ );
+ gBrowser.selectedTab = gTestTab;
+ is(gBrowser.selectedTab, gTestTab, "Switch back to the test tab");
+
+ UITour.notify("test-event-4");
+ });
+ },
+];
diff --git a/browser/components/uitour/test/browser_UITour_panel_close_annotation.js b/browser/components/uitour/test/browser_UITour_panel_close_annotation.js
new file mode 100644
index 0000000000..c12e225cbe
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour_panel_close_annotation.js
@@ -0,0 +1,227 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that annotations disappear when their target is hidden.
+ */
+
+"use strict";
+
+var gTestTab;
+var gContentAPI;
+var highlight = UITour.getHighlightContainerAndMaybeCreate(document);
+var tooltip = UITour.getTooltipAndMaybeCreate(document);
+
+function test() {
+ registerCleanupFunction(() => {
+ // Close the find bar in case it's open in the remaining tab
+ let findBar = gBrowser.getCachedFindBar(gBrowser.selectedTab);
+ if (findBar) {
+ findBar.close();
+ }
+ });
+ UITourTest();
+}
+
+var tests = [
+ function test_highlight_move_outside_panel(done) {
+ gContentAPI.showInfo("urlbar", "test title", "test text");
+ gContentAPI.showHighlight("addons");
+ waitForElementToBeVisible(
+ highlight,
+ function checkPanelIsOpen() {
+ isnot(PanelUI.panel.state, "closed", "Panel should have opened");
+
+ // Move the highlight outside which should close the app menu.
+ gContentAPI.showHighlight("appMenu");
+ waitForPopupAtAnchor(
+ highlight.parentElement,
+ document.getElementById("PanelUI-button"),
+ () => {
+ isnot(
+ PanelUI.panel.state,
+ "open",
+ "Panel should have closed after the highlight moved elsewhere."
+ );
+ ok(
+ tooltip.state == "showing" || tooltip.state == "open",
+ "The info panel should have remained open"
+ );
+ done();
+ },
+ "Highlight should move to the appMenu button and still be visible"
+ );
+ },
+ "Highlight should be shown after showHighlight() for fixed panel items"
+ );
+ },
+
+ function test_highlight_panel_hideMenu(done) {
+ gContentAPI.showHighlight("addons");
+ gContentAPI.showInfo("search", "test title", "test text");
+ waitForElementToBeVisible(
+ highlight,
+ function checkPanelIsOpen() {
+ isnot(PanelUI.panel.state, "closed", "Panel should have opened");
+
+ // Close the app menu and make sure the highlight also disappeared.
+ gContentAPI.hideMenu("appMenu");
+ waitForElementToBeHidden(
+ highlight,
+ function checkPanelIsClosed() {
+ isnot(
+ PanelUI.panel.state,
+ "open",
+ "Panel still should have closed"
+ );
+ ok(
+ tooltip.state == "showing" || tooltip.state == "open",
+ "The info panel should have remained open"
+ );
+ done();
+ },
+ "Highlight should have disappeared when panel closed"
+ );
+ },
+ "Highlight should be shown after showHighlight() for fixed panel items"
+ );
+ },
+
+ function test_highlight_panel_click_find(done) {
+ gContentAPI.showHighlight("help");
+ gContentAPI.showInfo("searchIcon", "test title", "test text");
+ waitForElementToBeVisible(
+ highlight,
+ function checkPanelIsOpen() {
+ isnot(PanelUI.panel.state, "closed", "Panel should have opened");
+
+ // Click the find button which should close the panel.
+ let findButton = document.getElementById("find-button");
+ EventUtils.synthesizeMouseAtCenter(findButton, {});
+ waitForElementToBeHidden(
+ highlight,
+ function checkPanelIsClosed() {
+ isnot(
+ PanelUI.panel.state,
+ "open",
+ "Panel should have closed when the find bar opened"
+ );
+ ok(
+ tooltip.state == "showing" || tooltip.state == "open",
+ "The info panel should have remained open"
+ );
+ done();
+ },
+ "Highlight should have disappeared when panel closed"
+ );
+ },
+ "Highlight should be shown after showHighlight() for fixed panel items"
+ );
+ },
+
+ function test_highlight_info_panel_click_find(done) {
+ gContentAPI.showHighlight("help");
+ gContentAPI.showInfo("addons", "Add addons!", "awesome!");
+ waitForElementToBeVisible(
+ highlight,
+ function checkPanelIsOpen() {
+ isnot(PanelUI.panel.state, "closed", "Panel should have opened");
+
+ // Click the find button which should close the panel.
+ let findButton = document.getElementById("find-button");
+ EventUtils.synthesizeMouseAtCenter(findButton, {});
+ waitForElementToBeHidden(
+ highlight,
+ function checkPanelIsClosed() {
+ isnot(
+ PanelUI.panel.state,
+ "open",
+ "Panel should have closed when the find bar opened"
+ );
+ waitForElementToBeHidden(
+ tooltip,
+ function checkTooltipIsClosed() {
+ isnot(
+ tooltip.state,
+ "open",
+ "The info panel should have closed too"
+ );
+ done();
+ },
+ "Tooltip should hide with the menu"
+ );
+ },
+ "Highlight should have disappeared when panel closed"
+ );
+ },
+ "Highlight should be shown after showHighlight() for fixed panel items"
+ );
+ },
+
+ function test_highlight_panel_open_subview(done) {
+ gContentAPI.showHighlight("addons");
+ gContentAPI.showInfo("backForward", "test title", "test text");
+ waitForElementToBeVisible(
+ highlight,
+ function checkPanelIsOpen() {
+ isnot(PanelUI.panel.state, "closed", "Panel should have opened");
+
+ // Click the help button which should open the subview in the panel menu.
+ let helpButton = document.getElementById("PanelUI-help");
+ EventUtils.synthesizeMouseAtCenter(helpButton, {});
+ waitForElementToBeHidden(
+ highlight,
+ function highlightHidden() {
+ is(
+ PanelUI.panel.state,
+ "open",
+ "Panel should have stayed open when the subview opened"
+ );
+ ok(
+ tooltip.state == "showing" || tooltip.state == "open",
+ "The info panel should have remained open"
+ );
+ PanelUI.hide();
+ done();
+ },
+ "Highlight should have disappeared when the subview opened"
+ );
+ },
+ "Highlight should be shown after showHighlight() for fixed panel items"
+ );
+ },
+
+ function test_info_panel_open_subview(done) {
+ gContentAPI.showHighlight("urlbar");
+ gContentAPI.showInfo("addons", "Add addons!", "Open a subview");
+ waitForElementToBeVisible(
+ tooltip,
+ function checkPanelIsOpen() {
+ isnot(PanelUI.panel.state, "closed", "Panel should have opened");
+
+ // Click the help button which should open the subview in the panel menu.
+ let helpButton = document.getElementById("PanelUI-help");
+ EventUtils.synthesizeMouseAtCenter(helpButton, {});
+ waitForElementToBeHidden(
+ tooltip,
+ function tooltipHidden() {
+ is(
+ PanelUI.panel.state,
+ "open",
+ "Panel should have stayed open when the subview opened"
+ );
+ is(
+ highlight.parentElement.state,
+ "open",
+ "The highlight should have remained open"
+ );
+ PanelUI.hide();
+ done();
+ },
+ "Tooltip should have disappeared when the subview opened"
+ );
+ },
+ "Highlight should be shown after showHighlight() for fixed panel items"
+ );
+ },
+];
diff --git a/browser/components/uitour/test/browser_UITour_pocket.js b/browser/components/uitour/test/browser_UITour_pocket.js
new file mode 100644
index 0000000000..072e1251e1
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour_pocket.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var gTestTab;
+var gContentAPI;
+
+add_task(setup_UITourTest);
+
+add_UITour_task(async function test_menu_show() {
+ let panel = BrowserPageActions.activatedActionPanelNode;
+ Assert.ok(
+ !panel || panel.state == "closed",
+ "Pocket panel should initially be closed"
+ );
+ gContentAPI.showMenu("pocket");
+
+ // The panel gets created dynamically.
+ panel = null;
+ await waitForConditionPromise(() => {
+ panel = BrowserPageActions.activatedActionPanelNode;
+ return panel && panel.state == "open";
+ }, "Menu should be visible after showMenu()");
+
+ Assert.ok(
+ !panel.hasAttribute("noautohide"),
+ "@noautohide shouldn't be on the pocket panel"
+ );
+
+ panel.hidePopup();
+ await new Promise(resolve => {
+ panel = BrowserPageActions.activatedActionPanelNode;
+ if (!panel || panel.state == "closed") {
+ resolve();
+ }
+ });
+});
diff --git a/browser/components/uitour/test/browser_UITour_resetProfile.js b/browser/components/uitour/test/browser_UITour_resetProfile.js
new file mode 100644
index 0000000000..921b4bcefe
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour_resetProfile.js
@@ -0,0 +1,46 @@
+"use strict";
+
+var gTestTab;
+var gContentAPI;
+
+add_task(setup_UITourTest);
+
+// Test that a reset profile dialog appears when "resetFirefox" event is triggered
+add_UITour_task(async function test_resetFirefox() {
+ let canReset = await getConfigurationPromise("canReset");
+ ok(
+ !canReset,
+ "Shouldn't be able to reset from mochitest's temporary profile."
+ );
+ let dialogPromise = BrowserTestUtils.promiseAlertDialog(
+ "cancel",
+ "chrome://global/content/resetProfile.xhtml",
+ {
+ isSubDialog: true,
+ }
+ );
+
+ // make reset possible.
+ let profileService = Cc["@mozilla.org/toolkit/profile-service;1"].getService(
+ Ci.nsIToolkitProfileService
+ );
+ let currentProfileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let profileName = "mochitest-test-profile-temp-" + Date.now();
+ let tempProfile = profileService.createProfile(
+ currentProfileDir,
+ profileName
+ );
+ canReset = await getConfigurationPromise("canReset");
+ ok(
+ canReset,
+ "Should be able to reset from mochitest's temporary profile once it's in the profile manager."
+ );
+ await gContentAPI.resetFirefox();
+ await dialogPromise;
+ tempProfile.remove(false);
+ canReset = await getConfigurationPromise("canReset");
+ ok(
+ !canReset,
+ "Shouldn't be able to reset from mochitest's temporary profile once removed from the profile manager."
+ );
+});
diff --git a/browser/components/uitour/test/browser_UITour_showNewTab.js b/browser/components/uitour/test/browser_UITour_showNewTab.js
new file mode 100644
index 0000000000..386de3920f
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour_showNewTab.js
@@ -0,0 +1,25 @@
+"use strict";
+
+var gTestTab;
+var gContentAPI;
+
+add_task(setup_UITourTest);
+
+// Test that we can switch to about:newtab
+add_UITour_task(async function test_aboutNewTab() {
+ let newTabLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "about:newtab"
+ );
+ info("Showing about:newtab");
+ await gContentAPI.showNewTab();
+ info("Waiting for about:newtab to load");
+ await newTabLoaded;
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "about:newtab",
+ "Loaded about:newtab"
+ );
+ ok(gURLBar.focused, "Address bar gets focus");
+});
diff --git a/browser/components/uitour/test/browser_UITour_showProtectionReport.js b/browser/components/uitour/test/browser_UITour_showProtectionReport.js
new file mode 100644
index 0000000000..fd8be561ac
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour_showProtectionReport.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var gTestTab;
+var gContentAPI;
+
+add_task(setup_UITourTest);
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.contentblocking.database.enabled", false],
+ ["browser.contentblocking.report.monitor.enabled", false],
+ ["browser.contentblocking.report.lockwise.enabled", false],
+ ["browser.contentblocking.report.proxy.enabled", false],
+ ],
+ });
+});
+
+// Test that we can switch to about:protections
+add_UITour_task(async function test_openProtectionReport() {
+ let aboutProtectionsLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "about:protections"
+ );
+ info("Showing about:protections");
+ await gContentAPI.showProtectionReport();
+ info("Waiting for about:protections to load");
+ await aboutProtectionsLoaded;
+ // When the graph is built it means the messaging has finished,
+ // we can close the tab.
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ await ContentTaskUtils.waitForCondition(() => {
+ let bars = content.document.querySelectorAll(".graph-bar");
+ return bars.length;
+ }, "The graph has been built");
+ });
+
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "about:protections",
+ "Loaded about:protections"
+ );
+});
diff --git a/browser/components/uitour/test/browser_UITour_sync.js b/browser/components/uitour/test/browser_UITour_sync.js
new file mode 100644
index 0000000000..e7393966ce
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour_sync.js
@@ -0,0 +1,231 @@
+"use strict";
+
+var gTestTab;
+var gContentAPI;
+
+const MOCK_FLOW_ID =
+ "5445b28b8b7ba6cf71e345f8fff4bc59b2a514f78f3e2cc99b696449427fd445";
+const MOCK_FLOW_BEGIN_TIME = 1590780440325;
+const MOCK_DEVICE_ID = "7e450f3337d3479b8582ea1c9bb5ba6c";
+
+registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("identity.fxaccounts.remote.root");
+ Services.prefs.clearUserPref("services.sync.username");
+});
+
+add_task(setup_UITourTest);
+
+add_setup(async function () {
+ Services.prefs.setCharPref(
+ "identity.fxaccounts.remote.root",
+ "https://example.com"
+ );
+});
+
+add_UITour_task(async function test_checkSyncSetup_disabled() {
+ let result = await getConfigurationPromise("sync");
+ is(result.setup, false, "Sync shouldn't be setup by default");
+});
+
+add_UITour_task(async function test_checkSyncSetup_enabled() {
+ Services.prefs.setCharPref(
+ "services.sync.username",
+ "uitour@tests.mozilla.org"
+ );
+ let result = await getConfigurationPromise("sync");
+ is(result.setup, true, "Sync should be setup");
+});
+
+add_UITour_task(async function test_checkSyncCounts() {
+ Services.prefs.setIntPref("services.sync.clients.devices.desktop", 4);
+ Services.prefs.setIntPref("services.sync.clients.devices.mobile", 5);
+ Services.prefs.setIntPref("services.sync.numClients", 9);
+ let result = await getConfigurationPromise("sync");
+ is(result.mobileDevices, 5, "mobileDevices should be set");
+ is(result.desktopDevices, 4, "desktopDevices should be set");
+ is(result.totalDevices, 9, "totalDevices should be set");
+
+ Services.prefs.clearUserPref("services.sync.clients.devices.desktop");
+ result = await getConfigurationPromise("sync");
+ is(result.mobileDevices, 5, "mobileDevices should be set");
+ is(result.desktopDevices, 0, "desktopDevices should be 0");
+ is(result.totalDevices, 9, "totalDevices should be set");
+
+ Services.prefs.clearUserPref("services.sync.clients.devices.mobile");
+ result = await getConfigurationPromise("sync");
+ is(result.mobileDevices, 0, "mobileDevices should be 0");
+ is(result.desktopDevices, 0, "desktopDevices should be 0");
+ is(result.totalDevices, 9, "totalDevices should be set");
+
+ Services.prefs.clearUserPref("services.sync.numClients");
+ result = await getConfigurationPromise("sync");
+ is(result.mobileDevices, 0, "mobileDevices should be 0");
+ is(result.desktopDevices, 0, "desktopDevices should be 0");
+ is(result.totalDevices, 0, "totalDevices should be 0");
+});
+
+// The showFirefoxAccounts API is sync related, so we test that here too...
+add_UITour_task(async function test_firefoxAccountsNoParams() {
+ info("Load https://accounts.firefox.com");
+ await gContentAPI.showFirefoxAccounts();
+ await BrowserTestUtils.browserLoaded(
+ gTestTab.linkedBrowser,
+ false,
+ "https://example.com/?context=fx_desktop_v3&entrypoint=uitour&action=email&service=sync"
+ );
+});
+
+add_UITour_task(async function test_firefoxAccountsValidParams() {
+ info("Load https://accounts.firefox.com");
+ await gContentAPI.showFirefoxAccounts({ utm_foo: "foo", utm_bar: "bar" });
+ await BrowserTestUtils.browserLoaded(
+ gTestTab.linkedBrowser,
+ false,
+ "https://example.com/?context=fx_desktop_v3&entrypoint=uitour&action=email&service=sync&utm_foo=foo&utm_bar=bar"
+ );
+});
+
+add_UITour_task(async function test_firefoxAccountsWithEmail() {
+ info("Load https://accounts.firefox.com");
+ await gContentAPI.showFirefoxAccounts(null, null, "foo@bar.com");
+ await BrowserTestUtils.browserLoaded(
+ gTestTab.linkedBrowser,
+ false,
+ "https://example.com/?context=fx_desktop_v3&entrypoint=uitour&email=foo%40bar.com&service=sync"
+ );
+});
+
+add_UITour_task(async function test_firefoxAccountsWithEmailAndFlowParams() {
+ info("Load https://accounts.firefox.com with flow params");
+ const flowParams = {
+ flow_id: MOCK_FLOW_ID,
+ flow_begin_time: MOCK_FLOW_BEGIN_TIME,
+ device_id: MOCK_DEVICE_ID,
+ };
+ await gContentAPI.showFirefoxAccounts(flowParams, null, "foo@bar.com");
+ await BrowserTestUtils.browserLoaded(
+ gTestTab.linkedBrowser,
+ false,
+ "https://example.com/?context=fx_desktop_v3&entrypoint=uitour&email=foo%40bar.com&service=sync&" +
+ `flow_id=${MOCK_FLOW_ID}&flow_begin_time=${MOCK_FLOW_BEGIN_TIME}&device_id=${MOCK_DEVICE_ID}`
+ );
+});
+
+add_UITour_task(
+ async function test_firefoxAccountsWithEmailAndBadFlowParamValues() {
+ info("Load https://accounts.firefox.com with bad flow params");
+ const BAD_MOCK_FLOW_ID = "1";
+ const BAD_MOCK_FLOW_BEGIN_TIME = 100;
+
+ await gContentAPI.showFirefoxAccounts(
+ {
+ flow_id: BAD_MOCK_FLOW_ID,
+ flow_begin_time: MOCK_FLOW_BEGIN_TIME,
+ device_id: MOCK_DEVICE_ID,
+ },
+ null,
+ "foo@bar.com"
+ );
+ await checkFxANotLoaded();
+
+ await gContentAPI.showFirefoxAccounts(
+ {
+ flow_id: MOCK_FLOW_ID,
+ flow_begin_time: BAD_MOCK_FLOW_BEGIN_TIME,
+ device_id: MOCK_DEVICE_ID,
+ },
+ null,
+ "foo@bar.com"
+ );
+ await checkFxANotLoaded();
+ }
+);
+
+add_UITour_task(
+ async function test_firefoxAccountsWithEmailAndMissingFlowParamValues() {
+ info("Load https://accounts.firefox.com with missing flow params");
+
+ await gContentAPI.showFirefoxAccounts(
+ {
+ flow_id: MOCK_FLOW_ID,
+ flow_begin_time: MOCK_FLOW_BEGIN_TIME,
+ },
+ null,
+ "foo@bar.com"
+ );
+ await BrowserTestUtils.browserLoaded(
+ gTestTab.linkedBrowser,
+ false,
+ "https://example.com/?context=fx_desktop_v3&entrypoint=uitour&email=foo%40bar.com&service=sync&" +
+ `flow_id=${MOCK_FLOW_ID}&flow_begin_time=${MOCK_FLOW_BEGIN_TIME}`
+ );
+ }
+);
+
+add_UITour_task(async function test_firefoxAccountsWithEmailAndEntrypoints() {
+ info("Load https://accounts.firefox.com with entrypoint parameters");
+
+ await gContentAPI.showFirefoxAccounts(
+ {
+ entrypoint_experiment: "exp",
+ entrypoint_variation: "var",
+ },
+ "entry",
+ "foo@bar.com"
+ );
+ await BrowserTestUtils.browserLoaded(
+ gTestTab.linkedBrowser,
+ false,
+ "https://example.com/?context=fx_desktop_v3&entrypoint=entry&email=foo%40bar.com&service=sync&" +
+ `entrypoint_experiment=exp&entrypoint_variation=var`
+ );
+});
+
+add_UITour_task(async function test_firefoxAccountsNonAlphaValue() {
+ // All characters in the value are allowed, but they must be automatically escaped.
+ // (we throw a unicode character in there too - it's not auto-utf8 encoded,
+ // but that's ok, so long as it is escaped correctly.)
+ let value = "foo& /=?:\\\xa9";
+ // encodeURIComponent encodes spaces to %20 but we want "+"
+ let expected = encodeURIComponent(value).replace(/%20/g, "+");
+ info("Load https://accounts.firefox.com");
+ await gContentAPI.showFirefoxAccounts({ utm_foo: value });
+ await BrowserTestUtils.browserLoaded(
+ gTestTab.linkedBrowser,
+ false,
+ "https://example.com/?context=fx_desktop_v3&entrypoint=uitour&action=email&service=sync&utm_foo=" +
+ expected
+ );
+});
+
+// A helper to check the request was ignored due to invalid params.
+async function checkFxANotLoaded() {
+ try {
+ await waitForConditionPromise(() => {
+ return gBrowser.selectedBrowser.currentURI.spec.startsWith(
+ "https://example.com"
+ );
+ }, "Check if FxA opened");
+ ok(false, "No FxA tab should have opened");
+ } catch (ex) {
+ ok(true, "No FxA tab opened");
+ }
+}
+
+add_UITour_task(async function test_firefoxAccountsNonObject() {
+ // non-string should be rejected.
+ await gContentAPI.showFirefoxAccounts(99);
+ await checkFxANotLoaded();
+});
+
+add_UITour_task(async function test_firefoxAccountsNonUtmPrefix() {
+ // Any non "utm_" name should should be rejected.
+ await gContentAPI.showFirefoxAccounts({ utm_foo: "foo", bar: "bar" });
+ await checkFxANotLoaded();
+});
+
+add_UITour_task(async function test_firefoxAccountsNonAlphaName() {
+ // Any "utm_" name which includes non-alpha chars should be rejected.
+ await gContentAPI.showFirefoxAccounts({ utm_foo: "foo", "utm_bar=": "bar" });
+ await checkFxANotLoaded();
+});
diff --git a/browser/components/uitour/test/browser_UITour_toggleReaderMode.js b/browser/components/uitour/test/browser_UITour_toggleReaderMode.js
new file mode 100644
index 0000000000..f3f45cdf27
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour_toggleReaderMode.js
@@ -0,0 +1,21 @@
+"use strict";
+
+var gTestTab;
+var gContentAPI;
+
+add_task(setup_UITourTest);
+
+add_UITour_task(async function () {
+ ok(
+ !gBrowser.selectedBrowser.currentURI.spec.startsWith("about:reader"),
+ "Should not be in reader mode at start of test."
+ );
+ await gContentAPI.toggleReaderMode();
+ await waitForConditionPromise(() =>
+ gBrowser.selectedBrowser.currentURI.spec.startsWith("about:reader")
+ );
+ ok(
+ gBrowser.selectedBrowser.currentURI.spec.startsWith("about:reader"),
+ "Should be in reader mode now."
+ );
+});
diff --git a/browser/components/uitour/test/browser_backgroundTab.js b/browser/components/uitour/test/browser_backgroundTab.js
new file mode 100644
index 0000000000..dbf14bdba5
--- /dev/null
+++ b/browser/components/uitour/test/browser_backgroundTab.js
@@ -0,0 +1,57 @@
+"use strict";
+
+var gTestTab;
+var gContentAPI;
+
+requestLongerTimeout(2);
+add_task(setup_UITourTest);
+
+add_UITour_task(async function test_bg_getConfiguration() {
+ info("getConfiguration is on the allowed list so should work");
+ await loadForegroundTab();
+ let data = await getConfigurationPromise("availableTargets");
+ ok(data, "Got data from getConfiguration");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_UITour_task(async function test_bg_showInfo() {
+ info("showInfo isn't on the allowed action list so should be denied");
+ await loadForegroundTab();
+
+ await showInfoPromise(
+ "appMenu",
+ "Hello from the background",
+ "Surprise!"
+ ).then(
+ () => ok(false, "panel shouldn't have shown from a background tab"),
+ () => ok(true, "panel wasn't shown from a background tab")
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+async function loadForegroundTab() {
+ // Spawn a content task that resolves once we're sure the visibilityState was
+ // changed. This state is what the tests in this file rely on.
+ let promise = SpecialPowers.spawn(
+ gBrowser.selectedTab.linkedBrowser,
+ [],
+ async function () {
+ return new Promise(resolve => {
+ let document = content.document;
+ document.addEventListener("visibilitychange", function onStateChange() {
+ Assert.equal(
+ document.visibilityState,
+ "hidden",
+ "UITour page should be hidden now."
+ );
+ document.removeEventListener("visibilitychange", onStateChange);
+ resolve();
+ });
+ });
+ }
+ );
+ await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ await promise;
+ isnot(gBrowser.selectedTab, gTestTab, "Make sure tour tab isn't selected");
+}
diff --git a/browser/components/uitour/test/browser_closeTab.js b/browser/components/uitour/test/browser_closeTab.js
new file mode 100644
index 0000000000..24984303f6
--- /dev/null
+++ b/browser/components/uitour/test/browser_closeTab.js
@@ -0,0 +1,23 @@
+"use strict";
+
+var gTestTab;
+var gContentAPI;
+
+add_task(setup_UITourTest);
+
+add_UITour_task(async function test_closeTab() {
+ // Setting gTestTab to null indicates that the tab has already been closed,
+ // and if this does not happen the test run will fail.
+ let closePromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabClose"
+ );
+ // In the e10s-case, having content request a tab close might mean
+ // that the ContentTask used to send this closeTab message won't
+ // get a response (since the message manager may have closed down).
+ // So we ignore the Promise that closeTab returns, and use the TabClose
+ // event to tell us when the tab has gone away.
+ gContentAPI.closeTab();
+ await closePromise;
+ gTestTab = null;
+});
diff --git a/browser/components/uitour/test/browser_fxa.js b/browser/components/uitour/test/browser_fxa.js
new file mode 100644
index 0000000000..4b5a867fc4
--- /dev/null
+++ b/browser/components/uitour/test/browser_fxa.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { UIState } = ChromeUtils.importESModule(
+ "resource://services-sync/UIState.sys.mjs"
+);
+
+XPCOMUtils.defineLazyGetter(this, "fxAccounts", () => {
+ return ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccounts.sys.mjs"
+ ).getFxAccountsSingleton();
+});
+
+var gTestTab;
+var gContentAPI;
+
+function test() {
+ UITourTest();
+}
+
+const oldState = UIState.get();
+registerCleanupFunction(async function () {
+ await signOut();
+ gSync.updateAllUI(oldState);
+});
+
+var tests = [
+ taskify(async function test_highlight_accountStatus_loggedOut() {
+ await showMenuPromise("appMenu");
+ await showHighlightPromise("accountStatus");
+ let highlight = document.getElementById("UITourHighlightContainer");
+ is(
+ highlight.getAttribute("targetName"),
+ "accountStatus",
+ "Correct highlight target"
+ );
+ }),
+
+ taskify(async function test_highlight_accountStatus_loggedIn() {
+ gSync.updateAllUI({
+ status: UIState.STATUS_SIGNED_IN,
+ lastSync: new Date(),
+ email: "foo@example.com",
+ });
+ await showMenuPromise("appMenu");
+ await showHighlightPromise("accountStatus");
+ let highlight = document.getElementById("UITourHighlightContainer");
+ is(
+ highlight.getAttribute("targetName"),
+ "accountStatus",
+ "Correct highlight target"
+ );
+ }),
+];
+
+function signOut() {
+ // we always want a "localOnly" signout here...
+ return fxAccounts.signOut(true);
+}
diff --git a/browser/components/uitour/test/browser_fxa_config.js b/browser/components/uitour/test/browser_fxa_config.js
new file mode 100644
index 0000000000..1da8bbc49f
--- /dev/null
+++ b/browser/components/uitour/test/browser_fxa_config.js
@@ -0,0 +1,379 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+const { UIState } = ChromeUtils.importESModule(
+ "resource://services-sync/UIState.sys.mjs"
+);
+
+var gTestTab;
+var gContentAPI;
+
+add_task(setup_UITourTest);
+
+add_UITour_task(async function test_no_user() {
+ const sandbox = sinon.createSandbox();
+ sandbox.stub(fxAccounts, "getSignedInUser").returns(null);
+ let result = await getConfigurationPromise("fxa");
+ Assert.deepEqual(result, { setup: false });
+ sandbox.restore();
+});
+
+add_UITour_task(async function test_no_sync_no_devices() {
+ const sandbox = sinon.createSandbox();
+ sandbox
+ .stub(fxAccounts, "getSignedInUser")
+ .returns({ email: "foo@example.com" });
+ sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => {
+ return [
+ {
+ id: 1,
+ name: "This Device",
+ isCurrentDevice: true,
+ type: "desktop",
+ },
+ ];
+ });
+ sandbox.stub(fxAccounts, "listAttachedOAuthClients").resolves([]);
+ sandbox.stub(fxAccounts, "hasLocalSession").resolves(true);
+
+ let result = await getConfigurationPromise("fxaConnections");
+ Assert.deepEqual(result, {
+ setup: true,
+ numOtherDevices: 0,
+ numDevicesByType: {},
+ accountServices: {},
+ });
+ sandbox.restore();
+});
+
+add_UITour_task(async function test_no_sync_many_devices() {
+ const sandbox = sinon.createSandbox();
+ sandbox
+ .stub(fxAccounts, "getSignedInUser")
+ .returns({ email: "foo@example.com" });
+ sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => {
+ return [
+ {
+ id: 1,
+ name: "This Device",
+ isCurrentDevice: true,
+ type: "desktop",
+ },
+ {
+ id: 2,
+ name: "Other Device",
+ type: "mobile",
+ },
+ {
+ id: 3,
+ name: "My phone",
+ type: "phone",
+ },
+ {
+ id: 4,
+ name: "Who knows?",
+ },
+ {
+ id: 5,
+ name: "Another desktop",
+ type: "desktop",
+ },
+ {
+ id: 6,
+ name: "Yet Another desktop",
+ type: "desktop",
+ },
+ ];
+ });
+ sandbox.stub(fxAccounts, "listAttachedOAuthClients").resolves([]);
+ sandbox.stub(fxAccounts, "hasLocalSession").resolves(true);
+
+ let result = await getConfigurationPromise("fxaConnections");
+ Assert.deepEqual(result, {
+ setup: true,
+ accountServices: {},
+ numOtherDevices: 5,
+ numDevicesByType: {
+ desktop: 2,
+ mobile: 1,
+ phone: 1,
+ unknown: 1,
+ },
+ });
+ sandbox.restore();
+});
+
+add_UITour_task(async function test_fxa_connections_no_cached_devices() {
+ const sandbox = sinon.createSandbox();
+ sandbox
+ .stub(fxAccounts, "getSignedInUser")
+ .returns({ email: "foo@example.com" });
+ let devicesStub = sandbox.stub(fxAccounts.device, "recentDeviceList");
+ devicesStub.get(() => {
+ // Sinon doesn't seem to support second `getters` returning a different
+ // value, so replace the getter here.
+ devicesStub.get(() => {
+ return [
+ {
+ id: 1,
+ name: "This Device",
+ isCurrentDevice: true,
+ type: "desktop",
+ },
+ {
+ id: 2,
+ name: "Other Device",
+ type: "mobile",
+ },
+ ];
+ });
+ // and here we want to say "nothing is yet cached"
+ return null;
+ });
+
+ sandbox.stub(fxAccounts, "listAttachedOAuthClients").resolves([]);
+ sandbox.stub(fxAccounts, "hasLocalSession").resolves(true);
+ let rdlStub = sandbox.stub(fxAccounts.device, "refreshDeviceList").resolves();
+
+ let result = await getConfigurationPromise("fxaConnections");
+ Assert.deepEqual(result, {
+ setup: true,
+ accountServices: {},
+ numOtherDevices: 1,
+ numDevicesByType: {
+ mobile: 1,
+ },
+ });
+ Assert.ok(rdlStub.called);
+ sandbox.restore();
+});
+
+add_UITour_task(async function test_account_connections() {
+ const sandbox = sinon.createSandbox();
+ sandbox
+ .stub(fxAccounts, "getSignedInUser")
+ .returns({ email: "foo@example.com" });
+ sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => []);
+ sandbox.stub(fxAccounts, "listAttachedOAuthClients").resolves([
+ {
+ id: "802d56ef2a9af9fa",
+ lastAccessedDaysAgo: 2,
+ },
+ {
+ id: "1f30e32975ae5112",
+ lastAccessedDaysAgo: 10,
+ },
+ {
+ id: null,
+ name: "Some browser",
+ lastAccessedDaysAgo: 10,
+ },
+ {
+ id: "null-last-accessed",
+ lastAccessedDaysAgo: null,
+ },
+ ]);
+ Assert.deepEqual(await getConfigurationPromise("fxaConnections"), {
+ setup: true,
+ numOtherDevices: 0,
+ numDevicesByType: {},
+ accountServices: {
+ "802d56ef2a9af9fa": {
+ id: "802d56ef2a9af9fa",
+ lastAccessedWeeksAgo: 0,
+ },
+ "1f30e32975ae5112": {
+ id: "1f30e32975ae5112",
+ lastAccessedWeeksAgo: 1,
+ },
+ "null-last-accessed": {
+ id: "null-last-accessed",
+ lastAccessedWeeksAgo: null,
+ },
+ },
+ });
+ sandbox.restore();
+});
+
+add_UITour_task(async function test_sync() {
+ const sandbox = sinon.createSandbox();
+ sandbox
+ .stub(fxAccounts, "getSignedInUser")
+ .returns({ email: "foo@example.com" });
+ sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => []);
+ sandbox.stub(fxAccounts, "listAttachedOAuthClients").resolves([]);
+ sandbox.stub(fxAccounts, "hasLocalSession").resolves(true);
+ Services.prefs.setCharPref("services.sync.username", "tests@mozilla.org");
+ Services.prefs.setIntPref("services.sync.clients.devices.desktop", 4);
+ Services.prefs.setIntPref("services.sync.clients.devices.mobile", 5);
+ Services.prefs.setIntPref("services.sync.numClients", 9);
+
+ Assert.deepEqual(await getConfigurationPromise("fxa"), {
+ setup: true,
+ accountStateOK: true,
+ browserServices: {
+ sync: {
+ setup: true,
+ mobileDevices: 5,
+ desktopDevices: 4,
+ totalDevices: 9,
+ },
+ },
+ });
+ Services.prefs.clearUserPref("services.sync.username");
+ Services.prefs.clearUserPref("services.sync.clients.devices.desktop");
+ Services.prefs.clearUserPref("services.sync.clients.devices.mobile");
+ Services.prefs.clearUserPref("services.sync.numClients");
+ sandbox.restore();
+});
+
+add_UITour_task(async function test_fxa_fails() {
+ const sandbox = sinon.createSandbox();
+ sandbox.stub(fxAccounts, "getSignedInUser").throws();
+ let result = await getConfigurationPromise("fxa");
+ Assert.deepEqual(result, {});
+ sandbox.restore();
+});
+
+/**
+ * Tests that a UITour page can get notifications on FxA sign-in state
+ * changes.
+ */
+add_UITour_task(async function test_fxa_signedin_state_change() {
+ const sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ let fxaConfig = await getConfigurationPromise("fxa");
+ Assert.ok(!fxaConfig.setup, "FxA should not yet be set up.");
+
+ // A helper function that waits for the state change event to fire
+ // in content, and returns a Promise that resolves to the status
+ // parameter on the event detail.
+ let waitForSignedInStateChange = () => {
+ return SpecialPowers.spawn(gTestTab.linkedBrowser, [], async () => {
+ let event = await ContentTaskUtils.waitForEvent(
+ content.document,
+ "mozUITourNotification",
+ false,
+ e => {
+ return e.detail.event === "FxA:SignedInStateChange";
+ },
+ true
+ );
+ return event.detail.params.status;
+ });
+ };
+
+ // We'll first test the STATUS_SIGNED_IN status.
+
+ let stateChangePromise = waitForSignedInStateChange();
+
+ // Per bug 1743857, we wait for a JSWindowActor message round trip to
+ // ensure that the mozUITourNotification event listener has been setup
+ // in the SpecialPowers.spawn task.
+ await new Promise(resolve => {
+ gContentAPI.ping(resolve);
+ });
+
+ let UIStateStub = sandbox.stub(UIState, "get").returns({
+ status: UIState.STATUS_SIGNED_IN,
+ syncEnabled: true,
+ email: "email@example.com",
+ });
+
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ let status = await stateChangePromise;
+ Assert.equal(
+ status,
+ UIState.STATUS_SIGNED_IN,
+ "FxA:SignedInStateChange should have notified that we'd signed in."
+ );
+
+ // We'll next test the STATUS_NOT_CONFIGURED status.
+
+ stateChangePromise = waitForSignedInStateChange();
+
+ // Per bug 1743857, we wait for a JSWindowActor message round trip to
+ // ensure that the mozUITourNotification event listener has been setup
+ // in the SpecialPowers.spawn task.
+ await new Promise(resolve => {
+ gContentAPI.ping(resolve);
+ });
+
+ UIStateStub.restore();
+ UIStateStub = sandbox.stub(UIState, "get").returns({
+ status: UIState.STATUS_NOT_CONFIGURED,
+ });
+
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ status = await stateChangePromise;
+ Assert.equal(
+ status,
+ UIState.STATUS_NOT_CONFIGURED,
+ "FxA:SignedInStateChange should have notified that we're not configured."
+ );
+
+ // We'll next test the STATUS_LOGIN_FAILED status.
+
+ stateChangePromise = waitForSignedInStateChange();
+
+ // Per bug 1743857, we wait for a JSWindowActor message round trip to
+ // ensure that the mozUITourNotification event listener has been setup
+ // in the SpecialPowers.spawn task.
+ await new Promise(resolve => {
+ gContentAPI.ping(resolve);
+ });
+
+ UIStateStub.restore();
+ UIStateStub = sandbox.stub(UIState, "get").returns({
+ email: "foo@example.com",
+ status: UIState.STATUS_LOGIN_FAILED,
+ });
+
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ status = await stateChangePromise;
+ Assert.equal(
+ status,
+ UIState.STATUS_LOGIN_FAILED,
+ "FxA:SignedInStateChange should have notified that login has failed."
+ );
+
+ // We'll next test the STATUS_NOT_VERIFIED status.
+
+ stateChangePromise = waitForSignedInStateChange();
+
+ // Per bug 1743857, we wait for a JSWindowActor message round trip to
+ // ensure that the mozUITourNotification event listener has been setup
+ // in the SpecialPowers.spawn task.
+ await new Promise(resolve => {
+ gContentAPI.ping(resolve);
+ });
+
+ UIStateStub.restore();
+ UIStateStub = sandbox.stub(UIState, "get").returns({
+ email: "foo@example.com",
+ status: UIState.STATUS_NOT_VERIFIED,
+ });
+
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ status = await stateChangePromise;
+ Assert.equal(
+ status,
+ UIState.STATUS_NOT_VERIFIED,
+ "FxA:SignedInStateChange should have notified that the login hasn't yet been verified."
+ );
+
+ sandbox.restore();
+});
diff --git a/browser/components/uitour/test/browser_openPreferences.js b/browser/components/uitour/test/browser_openPreferences.js
new file mode 100644
index 0000000000..04e46086b2
--- /dev/null
+++ b/browser/components/uitour/test/browser_openPreferences.js
@@ -0,0 +1,73 @@
+"use strict";
+
+var gTestTab;
+var gContentAPI;
+
+add_task(setup_UITourTest);
+
+add_UITour_task(async function test_openPreferences() {
+ let promiseTabOpened = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:preferences"
+ );
+ await gContentAPI.openPreferences();
+ let tab = await promiseTabOpened;
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_UITour_task(async function test_openInvalidPreferences() {
+ await gContentAPI.openPreferences(999);
+
+ try {
+ await waitForConditionPromise(() => {
+ return gBrowser.selectedBrowser.currentURI.spec.startsWith(
+ "about:preferences"
+ );
+ }, "Check if about:preferences opened");
+ ok(false, "No about:preferences tab should have opened");
+ } catch (ex) {
+ ok(true, "No about:preferences tab opened: " + ex);
+ }
+});
+
+add_UITour_task(async function test_openPrivacyPreferences() {
+ let promiseTabOpened = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:preferences#privacy"
+ );
+ await gContentAPI.openPreferences("privacy");
+ let tab = await promiseTabOpened;
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_UITour_task(async function test_openPrivacyReports() {
+ if (
+ !AppConstants.MOZ_TELEMETRY_REPORTING &&
+ !(AppConstants.MOZ_DATA_REPORTING && AppConstants.MOZ_CRASHREPORTER)
+ ) {
+ return;
+ }
+ let promiseTabOpened = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:preferences#privacy-reports"
+ );
+ await gContentAPI.openPreferences("privacy-reports");
+ let tab = await promiseTabOpened;
+ await BrowserTestUtils.waitForEvent(gBrowser.selectedBrowser, "Initialized");
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ is(
+ doc.location.hash,
+ "#privacy",
+ "Should not display the reports subcategory in the location hash."
+ );
+ await TestUtils.waitForCondition(
+ () => doc.querySelector(".spotlight"),
+ "Wait for the reports section is spotlighted."
+ );
+ is(
+ doc.querySelector(".spotlight").getAttribute("data-subcategory"),
+ "reports",
+ "The reports section is spotlighted."
+ );
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/uitour/test/browser_openSearchPanel.js b/browser/components/uitour/test/browser_openSearchPanel.js
new file mode 100644
index 0000000000..a60a550458
--- /dev/null
+++ b/browser/components/uitour/test/browser_openSearchPanel.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var gTestTab;
+var gContentAPI;
+
+function test() {
+ UITourTest();
+}
+
+var tests = [
+ function test_openSearchPanel(done) {
+ // If suggestions are enabled, the panel will attempt to use the network to
+ // connect to the suggestions provider, causing the test suite to fail. We
+ // also change the preference to display the search bar during the test.
+ Services.prefs.setBoolPref("browser.search.widget.inNavBar", true);
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.search.widget.inNavBar");
+ Services.prefs.clearUserPref("browser.search.suggest.enabled");
+ });
+
+ let searchbar = document.getElementById("searchbar");
+ ok(!searchbar.textbox.open, "Popup starts as closed");
+ gContentAPI.openSearchPanel(() => {
+ ok(searchbar.textbox.open, "Popup was opened");
+ searchbar.textbox.closePopup();
+ ok(!searchbar.textbox.open, "Popup was closed");
+ done();
+ });
+ },
+];
diff --git a/browser/components/uitour/test/head.js b/browser/components/uitour/test/head.js
new file mode 100644
index 0000000000..07b941ba1c
--- /dev/null
+++ b/browser/components/uitour/test/head.js
@@ -0,0 +1,539 @@
+"use strict";
+
+// This file expects these globals to be defined by the test case.
+/* global gTestTab:true, gContentAPI:true, tests:false */
+
+ChromeUtils.defineESModuleGetters(this, {
+ UITour: "resource:///modules/UITour.sys.mjs",
+});
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const SINGLE_TRY_TIMEOUT = 100;
+const NUMBER_OF_TRIES = 30;
+
+let gProxyCallbackMap = new Map();
+
+function waitForConditionPromise(
+ condition,
+ timeoutMsg,
+ tryCount = NUMBER_OF_TRIES
+) {
+ return new Promise((resolve, reject) => {
+ let tries = 0;
+ function checkCondition() {
+ if (tries >= tryCount) {
+ reject(timeoutMsg);
+ }
+ var conditionPassed;
+ try {
+ conditionPassed = condition();
+ } catch (e) {
+ return reject(e);
+ }
+ if (conditionPassed) {
+ return resolve();
+ }
+ tries++;
+ setTimeout(checkCondition, SINGLE_TRY_TIMEOUT);
+ return undefined;
+ }
+ setTimeout(checkCondition, SINGLE_TRY_TIMEOUT);
+ });
+}
+
+function waitForCondition(condition, nextTestFn, errorMsg) {
+ waitForConditionPromise(condition, errorMsg).then(nextTestFn, reason => {
+ ok(false, reason + (reason.stack ? "\n" + reason.stack : ""));
+ });
+}
+
+/**
+ * Wrapper to partially transition tests to Task. Use `add_UITour_task` instead for new tests.
+ */
+function taskify(fun) {
+ return doneFn => {
+ // Output the inner function name otherwise no name will be output.
+ info("\t" + fun.name);
+ return fun().then(doneFn, reason => {
+ console.error(reason);
+ ok(false, reason);
+ doneFn();
+ });
+ };
+}
+
+function is_hidden(element) {
+ let win = element.ownerGlobal;
+ let style = win.getComputedStyle(element);
+ if (style.display == "none") {
+ return true;
+ }
+ if (style.visibility != "visible") {
+ return true;
+ }
+ if (win.XULPopupElement.isInstance(element)) {
+ return ["hiding", "closed"].includes(element.state);
+ }
+
+ // Hiding a parent element will hide all its children
+ if (element.parentNode != element.ownerDocument) {
+ return is_hidden(element.parentNode);
+ }
+
+ return false;
+}
+
+function is_visible(element) {
+ let win = element.ownerGlobal;
+ let style = win.getComputedStyle(element);
+ if (style.display == "none") {
+ return false;
+ }
+ if (style.visibility != "visible") {
+ return false;
+ }
+ if (win.XULPopupElement.isInstance(element) && element.state != "open") {
+ return false;
+ }
+
+ // Hiding a parent element will hide all its children
+ if (element.parentNode != element.ownerDocument) {
+ return is_visible(element.parentNode);
+ }
+
+ return true;
+}
+
+function is_element_visible(element, msg) {
+ isnot(element, null, "Element should not be null, when checking visibility");
+ ok(is_visible(element), msg);
+}
+
+function waitForElementToBeVisible(element, nextTestFn, msg) {
+ waitForCondition(
+ () => is_visible(element),
+ () => {
+ ok(true, msg);
+ nextTestFn();
+ },
+ "Timeout waiting for visibility: " + msg
+ );
+}
+
+function waitForElementToBeHidden(element, nextTestFn, msg) {
+ waitForCondition(
+ () => is_hidden(element),
+ () => {
+ ok(true, msg);
+ nextTestFn();
+ },
+ "Timeout waiting for invisibility: " + msg
+ );
+}
+
+function elementVisiblePromise(element, msg) {
+ return waitForConditionPromise(
+ () => is_visible(element),
+ "Timeout waiting for visibility: " + msg
+ );
+}
+
+function elementHiddenPromise(element, msg) {
+ return waitForConditionPromise(
+ () => is_hidden(element),
+ "Timeout waiting for invisibility: " + msg
+ );
+}
+
+function waitForPopupAtAnchor(popup, anchorNode, nextTestFn, msg) {
+ waitForCondition(
+ () => is_visible(popup) && popup.anchorNode == anchorNode,
+ () => {
+ ok(true, msg);
+ is_element_visible(popup, "Popup should be visible");
+ nextTestFn();
+ },
+ "Timeout waiting for popup at anchor: " + msg
+ );
+}
+
+function getConfigurationPromise(configName) {
+ return SpecialPowers.spawn(
+ gTestTab.linkedBrowser,
+ [configName],
+ contentConfigName => {
+ return new Promise(resolve => {
+ let contentWin = Cu.waiveXrays(content);
+ contentWin.Mozilla.UITour.getConfiguration(contentConfigName, resolve);
+ });
+ }
+ );
+}
+
+function getShowHighlightTargetName() {
+ let highlight = document.getElementById("UITourHighlight");
+ return highlight.parentElement.getAttribute("targetName");
+}
+
+function getShowInfoTargetName() {
+ let tooltip = document.getElementById("UITourTooltip");
+ return tooltip.getAttribute("targetName");
+}
+
+function hideInfoPromise(...args) {
+ let popup = document.getElementById("UITourTooltip");
+ gContentAPI.hideInfo.apply(gContentAPI, args);
+ return promisePanelElementHidden(window, popup);
+}
+
+/**
+ * `buttons` and `options` require functions from the content scope so we take a
+ * function name to call to generate the buttons/options instead of the
+ * buttons/options themselves. This makes the signature differ from the content one.
+ */
+function showInfoPromise(
+ target,
+ title,
+ text,
+ icon,
+ buttonsFunctionName,
+ optionsFunctionName
+) {
+ let popup = document.getElementById("UITourTooltip");
+ let shownPromise = promisePanelElementShown(window, popup);
+ return SpecialPowers.spawn(gTestTab.linkedBrowser, [[...arguments]], args => {
+ let contentWin = Cu.waiveXrays(content);
+ let [
+ contentTarget,
+ contentTitle,
+ contentText,
+ contentIcon,
+ contentButtonsFunctionName,
+ contentOptionsFunctionName,
+ ] = args;
+ let buttons = contentButtonsFunctionName
+ ? contentWin[contentButtonsFunctionName]()
+ : null;
+ let options = contentOptionsFunctionName
+ ? contentWin[contentOptionsFunctionName]()
+ : null;
+ contentWin.Mozilla.UITour.showInfo(
+ contentTarget,
+ contentTitle,
+ contentText,
+ contentIcon,
+ buttons,
+ options
+ );
+ }).then(() => shownPromise);
+}
+
+function showHighlightPromise(...args) {
+ let popup = document.getElementById("UITourHighlightContainer");
+ gContentAPI.showHighlight.apply(gContentAPI, args);
+ return promisePanelElementShown(window, popup);
+}
+
+function showMenuPromise(name) {
+ return SpecialPowers.spawn(gTestTab.linkedBrowser, [name], contentName => {
+ return new Promise(resolve => {
+ let contentWin = Cu.waiveXrays(content);
+ contentWin.Mozilla.UITour.showMenu(contentName, resolve);
+ });
+ });
+}
+
+function waitForCallbackResultPromise() {
+ return SpecialPowers.spawn(gTestTab.linkedBrowser, [], async function () {
+ let contentWin = Cu.waiveXrays(content);
+ await ContentTaskUtils.waitForCondition(() => {
+ return contentWin.callbackResult;
+ }, "callback should be called");
+ return {
+ data: contentWin.callbackData,
+ result: contentWin.callbackResult,
+ };
+ });
+}
+
+function promisePanelShown(win) {
+ let panelEl = win.PanelUI.panel;
+ return promisePanelElementShown(win, panelEl);
+}
+
+function promisePanelElementEvent(win, aPanel, aEvent) {
+ return new Promise((resolve, reject) => {
+ let timeoutId = win.setTimeout(() => {
+ aPanel.removeEventListener(aEvent, onPanelEvent);
+ reject(aEvent + " event did not happen within 5 seconds.");
+ }, 5000);
+
+ function onPanelEvent(e) {
+ aPanel.removeEventListener(aEvent, onPanelEvent);
+ win.clearTimeout(timeoutId);
+ // Wait one tick to let UITour.sys.mjs process the event as well.
+ executeSoon(resolve);
+ }
+
+ aPanel.addEventListener(aEvent, onPanelEvent);
+ });
+}
+
+function promisePanelElementShown(win, aPanel) {
+ return promisePanelElementEvent(win, aPanel, "popupshown");
+}
+
+function promisePanelElementHidden(win, aPanel) {
+ return promisePanelElementEvent(win, aPanel, "popuphidden");
+}
+
+function is_element_hidden(element, msg) {
+ isnot(element, null, "Element should not be null, when checking visibility");
+ ok(is_hidden(element), msg);
+}
+
+function isTourBrowser(aBrowser) {
+ let chromeWindow = aBrowser.ownerGlobal;
+ return (
+ UITour.tourBrowsersByWindow.has(chromeWindow) &&
+ UITour.tourBrowsersByWindow.get(chromeWindow).has(aBrowser)
+ );
+}
+
+async function loadUITourTestPage(callback, host = "https://example.org/") {
+ if (gTestTab) {
+ gProxyCallbackMap.clear();
+ gBrowser.removeTab(gTestTab);
+ }
+
+ if (!window.gProxyCallbackMap) {
+ window.gProxyCallbackMap = gProxyCallbackMap;
+ }
+
+ let url = getRootDirectory(gTestPath) + "uitour.html";
+ url = url.replace("chrome://mochitests/content/", host);
+
+ gTestTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ // When e10s is enabled, make gContentAPI a proxy which has every property
+ // return a function which calls the method of the same name on
+ // contentWin.Mozilla.UITour in a ContentTask.
+ let UITourHandler = {
+ get(target, prop, receiver) {
+ return (...args) => {
+ let browser = gTestTab.linkedBrowser;
+ // We need to proxy any callback functions using messages:
+ let fnIndices = [];
+ args = args.map((arg, index) => {
+ // Replace function arguments with "", and add them to the list of
+ // forwarded functions. We'll construct a function on the content-side
+ // that forwards all its arguments to a message, and we'll listen for
+ // those messages on our side and call the corresponding function with
+ // the arguments we got from the content side.
+ if (typeof arg == "function") {
+ gProxyCallbackMap.set(index, arg);
+ fnIndices.push(index);
+ return "";
+ }
+ return arg;
+ });
+ let taskArgs = {
+ methodName: prop,
+ args,
+ fnIndices,
+ };
+ return SpecialPowers.spawn(
+ browser,
+ [taskArgs],
+ async function (contentArgs) {
+ let contentWin = Cu.waiveXrays(content);
+ let callbacksCalled = 0;
+ let resolveCallbackPromise;
+ let allCallbacksCalledPromise = new Promise(
+ resolve => (resolveCallbackPromise = resolve)
+ );
+ let argumentsWithFunctions = Cu.cloneInto(
+ contentArgs.args.map((arg, index) => {
+ if (arg === "" && contentArgs.fnIndices.includes(index)) {
+ return function () {
+ callbacksCalled++;
+ SpecialPowers.spawnChrome(
+ [index, Array.from(arguments)],
+ (indexParent, argumentsParent) => {
+ // Please note that this handler only allows the callback to be used once.
+ // That means that a single gContentAPI.observer() call can't be used
+ // to observe multiple events.
+ let window = this.browsingContext.topChromeWindow;
+ let cb = window.gProxyCallbackMap.get(indexParent);
+ window.gProxyCallbackMap.delete(indexParent);
+ cb.apply(null, argumentsParent);
+ }
+ );
+ if (callbacksCalled >= contentArgs.fnIndices.length) {
+ resolveCallbackPromise();
+ }
+ };
+ }
+ return arg;
+ }),
+ content,
+ { cloneFunctions: true }
+ );
+ let rv = contentWin.Mozilla.UITour[contentArgs.methodName].apply(
+ contentWin.Mozilla.UITour,
+ argumentsWithFunctions
+ );
+ if (contentArgs.fnIndices.length) {
+ await allCallbacksCalledPromise;
+ }
+ return rv;
+ }
+ );
+ };
+ },
+ };
+ gContentAPI = new Proxy({}, UITourHandler);
+
+ await SimpleTest.promiseFocus(gTestTab.linkedBrowser);
+ callback();
+}
+
+// Wrapper for UITourTest to be used by add_task tests.
+function setup_UITourTest() {
+ return UITourTest(true);
+}
+
+// Use `add_task(setup_UITourTest);` instead as we will fold this into `setup_UITourTest` once all tests are using `add_UITour_task`.
+function UITourTest(usingAddTask = false) {
+ Services.prefs.setBoolPref("browser.uitour.enabled", true);
+ let testHttpsOrigin = "https://example.org";
+ let testHttpOrigin = "http://example.org";
+ PermissionTestUtils.add(
+ testHttpsOrigin,
+ "uitour",
+ Services.perms.ALLOW_ACTION
+ );
+ PermissionTestUtils.add(
+ testHttpOrigin,
+ "uitour",
+ Services.perms.ALLOW_ACTION
+ );
+
+ UITour.getHighlightContainerAndMaybeCreate(window.document);
+ UITour.getTooltipAndMaybeCreate(window.document);
+
+ // If a test file is using add_task, we don't need to have a test function or
+ // call `waitForExplicitFinish`.
+ if (!usingAddTask) {
+ waitForExplicitFinish();
+ }
+
+ registerCleanupFunction(function () {
+ delete window.gContentAPI;
+ if (gTestTab) {
+ gBrowser.removeTab(gTestTab);
+ }
+ delete window.gTestTab;
+ delete window.gProxyCallbackMap;
+ Services.prefs.clearUserPref("browser.uitour.enabled");
+ PermissionTestUtils.remove(testHttpsOrigin, "uitour");
+ PermissionTestUtils.remove(testHttpOrigin, "uitour");
+ });
+
+ // When using tasks, the harness will call the next added task for us.
+ if (!usingAddTask) {
+ nextTest();
+ }
+}
+
+function done(usingAddTask = false) {
+ info("== Done test, doing shared checks before teardown ==");
+ return new Promise(resolve => {
+ executeSoon(() => {
+ if (gTestTab) {
+ gBrowser.removeTab(gTestTab);
+ }
+ gTestTab = null;
+ gProxyCallbackMap.clear();
+
+ let highlight = document.getElementById("UITourHighlightContainer");
+ is_element_hidden(
+ highlight,
+ "Highlight should be closed/hidden after UITour tab is closed"
+ );
+
+ let tooltip = document.getElementById("UITourTooltip");
+ is_element_hidden(
+ tooltip,
+ "Tooltip should be closed/hidden after UITour tab is closed"
+ );
+
+ ok(
+ !PanelUI.panel.hasAttribute("noautohide"),
+ "@noautohide on the menu panel should have been cleaned up"
+ );
+ ok(
+ !PanelUI.panel.hasAttribute("panelopen"),
+ "The panel shouldn't have @panelopen"
+ );
+ isnot(PanelUI.panel.state, "open", "The panel shouldn't be open");
+ is(
+ document.getElementById("PanelUI-menu-button").hasAttribute("open"),
+ false,
+ "Menu button should know that the menu is closed"
+ );
+
+ info("Done shared checks");
+ if (usingAddTask) {
+ executeSoon(resolve);
+ } else {
+ executeSoon(nextTest);
+ }
+ });
+ });
+}
+
+function nextTest() {
+ if (!tests.length) {
+ info("finished tests in this file");
+ finish();
+ return;
+ }
+ let test = tests.shift();
+ info("Starting " + test.name);
+ waitForFocus(function () {
+ loadUITourTestPage(function () {
+ test(done);
+ });
+ });
+}
+
+/**
+ * All new tests that need the help of `loadUITourTestPage` should use this
+ * wrapper around their test's generator function to reduce boilerplate.
+ */
+function add_UITour_task(func) {
+ let genFun = async function () {
+ await new Promise(resolve => {
+ waitForFocus(function () {
+ loadUITourTestPage(function () {
+ let funcPromise = (func() || Promise.resolve()).then(
+ () => done(true),
+ reason => {
+ ok(false, reason);
+ return done(true);
+ }
+ );
+ resolve(funcPromise);
+ });
+ });
+ });
+ };
+ Object.defineProperty(genFun, "name", {
+ configurable: true,
+ value: func.name,
+ });
+ add_task(genFun);
+}
diff --git a/browser/components/uitour/test/image.png b/browser/components/uitour/test/image.png
new file mode 100644
index 0000000000..597c7fd2cb
--- /dev/null
+++ b/browser/components/uitour/test/image.png
Binary files differ
diff --git a/browser/components/uitour/test/uitour.html b/browser/components/uitour/test/uitour.html
new file mode 100644
index 0000000000..09b8f4bd7f
--- /dev/null
+++ b/browser/components/uitour/test/uitour.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>UITour test</title>
+ <script type="application/javascript" src="UITour-lib.js">
+ </script>
+ <script type="application/javascript">
+ var callbackResult, callbackData;
+ function makeCallback(name) {
+ return (function(data) {
+ callbackResult = name;
+ callbackData = data;
+ });
+ }
+
+ // Defined in content to avoid weird issues when crossing between chrome/content.
+ function makeButtons() {
+ return [
+ {label: "Regular text", style: "text"},
+ {label: "Link", callback: makeCallback("link"), style: "link"},
+ {label: "Button 1", callback: makeCallback("button1")},
+ {label: "Button 2", callback: makeCallback("button2"), icon: "image.png",
+ style: "primary"},
+ ];
+ }
+
+ function makeInfoOptions() {
+ return {
+ closeButtonCallback: makeCallback("closeButton"),
+ targetCallback: makeCallback("target"),
+ };
+ }
+ </script>
+ </head>
+ <body>
+ <h1>UITour tests</h1>
+ <p>Because Firefox is...</p>
+ <p>Never gonna let you down</p>
+ <p>Never gonna give you up</p>
+ </body>
+</html>