Bug 1357020 - Should hide the onboarding tour if user explicitly checked the hide-the-tour checkbox, r?mossop, gasolin, rexboy
This patch
- adds one hide-onboarding-tour checkbox
- after toggling the overlay, hides the onboarding tour if user checked hide-the-tour checkbox
- creates the message channel between the chrome process and the content process to set prefs.
- listens to the pref-updated event and then hide the onboarding tour across pages.
- Add one browser_onboarding_hide_tours.js test
MozReview-Commit-ID: 7ZjbrhfO9dB
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1674,16 +1674,21 @@ pref("browser.sessionstore.restore_tabs_
pref("urlclassifier.malwareTable", "goog-malware-shavar,goog-unwanted-shavar,goog-malware-proto,goog-unwanted-proto,test-malware-simple,test-unwanted-simple");
pref("urlclassifier.phishTable", "goog-phish-shavar,goog-phish-proto,test-phish-simple");
#endif
pref("browser.suppress_first_window_animation", true);
// Preferences for Photon onboarding system extension
pref("browser.onboarding.enabled", true);
+pref("browser.onboarding.hidden", false);
+// On the Activity-Stream page, the snippet's position overlaps with our notification.
+// So use `browser.onboarding.notification.finished` to let the AS page know
+// if our notification is finished and safe to show their snippet.
+pref("browser.onboarding.notification.finished", false);
// Preferences for the Screenshots feature:
// Temporarily disable Screenshots in Beta & Release, so that we can gradually
// roll out the feature using SHIELD pref flipping.
#ifdef NIGHTLY_BUILD
pref("extensions.screenshots.system-disabled", false);
#else
pref("extensions.screenshots.system-disabled", true);
--- a/browser/extensions/onboarding/bootstrap.js
+++ b/browser/extensions/onboarding/bootstrap.js
@@ -1,18 +1,54 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+const PREF_WHITELIST = [
+ "browser.onboarding.enabled",
+ "browser.onboarding.hidden",
+ "browser.onboarding.notification.finished"
+];
+
+/**
+ * Set pref. Why no `getPrefs` function is due to the priviledge level.
+ * We cannot set prefs inside a framescript but can read.
+ * For simplicity and effeciency, we still read prefs inside the framescript.
+ *
+ * @param {Array} prefs the array of prefs to set.
+ * The array element carrys info to set pref, should contain
+ * - {String} name the pref name, such as `browser.onboarding.hidden`
+ * - {*} value the value to set
+ **/
+function setPrefs(prefs) {
+ prefs.forEach(pref => {
+ if (PREF_WHITELIST.includes(pref.name)) {
+ Preferences.set(pref.name, pref.value);
+ }
+ });
+}
+
+function initContentMessageListener() {
+ Services.mm.addMessageListener("Onboarding:OnContentMessage", msg => {
+ switch (msg.data.action) {
+ case "set-prefs":
+ setPrefs(msg.data.params);
+ break;
+ }
+ });
+}
function install(aData, aReason) {}
function uninstall(aData, aReason) {}
function startup(aData, reason) {
Services.mm.loadFrameScript("resource://onboarding/onboarding.js", true);
+ initContentMessageListener();
}
function shutdown(aData, reason) {}
--- a/browser/extensions/onboarding/content/onboarding.css
+++ b/browser/extensions/onboarding/content/onboarding.css
@@ -91,16 +91,22 @@
margin-inline-end: 0;
margin-inline-start: 0;
padding: 0;
}
#onboarding-overlay-dialog > footer {
grid-row: footer-start;
grid-column: dialog-start / tour-end;
+ font-size: 13px;
+}
+
+#onboarding-tour-hidden-checkbox {
+ margin-inline-start: 27px;
+ margin-inline-end: 10px;
}
/* Onboarding tour list */
#onboarding-tour-list {
margin: 40px 0 0 0;
padding: 0;
}
--- a/browser/extensions/onboarding/content/onboarding.js
+++ b/browser/extensions/onboarding/content/onboarding.js
@@ -1,18 +1,19 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-/* global content */
+/* eslint-env mozilla/frame-script */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
const ONBOARDING_CSS_URL = "resource://onboarding/onboarding.css";
const ABOUT_HOME_URL = "about:home";
const ABOUT_NEWTAB_URL = "about:newtab";
const BUNDLE_URI = "chrome://onboarding/locale/onboarding.properties";
const UITOUR_JS_URI = "chrome://browser/content/UITour-lib.js";
const TOUR_AGENT_JS_URI = "resource://onboarding/onboarding-tour-agent.js";
const BRAND_SHORT_NAME = Services.strings
@@ -120,16 +121,53 @@ class Onboarding {
this._loadJS(UITOUR_JS_URI);
this._loadJS(TOUR_AGENT_JS_URI);
this._overlayIcon.addEventListener("click", this);
this._overlay.addEventListener("click", this);
// Destroy on unload. This is to ensure we remove all the stuff we left.
// No any leak out there.
this._window.addEventListener("unload", () => this.destroy());
+
+ this._initPrefObserver();
+ }
+
+ _initPrefObserver() {
+ if (this._prefsObserved) {
+ return;
+ }
+
+ this._prefsObserved = new Map();
+ this._prefsObserved.set("browser.onboarding.hidden", prefValue => {
+ if (prefValue) {
+ this.destroy();
+ }
+ });
+ for (let [name, callback] of this._prefsObserved) {
+ Preferences.observe(name, callback);
+ }
+ }
+
+ _clearPrefObserver() {
+ if (this._prefsObserved) {
+ for (let [name, callback] of this._prefsObserved) {
+ Preferences.ignore(name, callback);
+ }
+ this._prefsObserved = null;
+ }
+ }
+
+ /**
+ * @param {String} action the action to ask the chrome to do
+ * @param {Array} params the parameters for the action
+ */
+ sendMessageToChrome(action, params) {
+ sendAsyncMessage("Onboarding:OnContentMessage", {
+ action, params
+ });
}
handleEvent(evt) {
switch (evt.target.id) {
case "onboarding-overlay-icon":
case "onboarding-overlay-close-btn":
// If the clicking target is directly on the outer-most overlay,
// that means clicking outside the tour content area.
@@ -139,26 +177,34 @@ class Onboarding {
break;
}
if (evt.target.classList.contains("onboarding-tour-item")) {
this.gotoPage(evt.target.id);
}
}
destroy() {
+ this._clearPrefObserver();
this._overlayIcon.remove();
this._overlay.remove();
}
toggleOverlay() {
if (this._tourItems.length == 0) {
// Lazy loading until first toggle.
this._loadTours(onboardingTours);
}
+ this._overlay.classList.toggle("opened");
+ let hiddenCheckbox = this._window.document.getElementById("onboarding-tour-hidden-checkbox");
+ if (hiddenCheckbox.checked) {
+ this.hide();
+ return;
+ }
+
this._overlay.classList.toggle("onboarding-opened");
}
gotoPage(tourId) {
let targetPageId = `${tourId}-page`;
for (let page of this._tourPages) {
page.style.display = page.id != targetPageId ? "none" : "";
}
@@ -166,34 +212,50 @@ class Onboarding {
if (li.id == tourId) {
li.classList.add("onboarding-active");
} else {
li.classList.remove("onboarding-active");
}
}
}
+ hide() {
+ this.sendMessageToChrome("set-prefs", [
+ {
+ name: "browser.onboarding.hidden",
+ value: true
+ },
+ {
+ name: "browser.onboarding.notification.finished",
+ value: true
+ }
+ ]);
+ }
+
_renderOverlay() {
let div = this._window.document.createElement("div");
div.id = "onboarding-overlay";
// Here we use `innerHTML` is for more friendly reading.
// The security should be fine because this is not from an external input.
// We're not shipping yet so l10n strings is going to be closed for now.
div.innerHTML = `
<div id="onboarding-overlay-dialog">
<span id="onboarding-overlay-close-btn"></span>
<header id="onboarding-header"></header>
<nav>
<ul id="onboarding-tour-list"></ul>
</nav>
<footer id="onboarding-footer">
+ <input type="checkbox" id="onboarding-tour-hidden-checkbox" /><label for="onboarding-tour-hidden-checkbox"></label>
</footer>
</div>
`;
+ div.querySelector("label[for='onboarding-tour-hidden-checkbox']").textContent =
+ this._bundle.GetStringFromName("onboarding.hidden-checkbox-label");
div.querySelector("#onboarding-header").textContent =
this._bundle.formatStringFromName("onboarding.overlay-title", [BRAND_SHORT_NAME], 1);
return div;
}
_renderOverlayIcon() {
let img = this._window.document.createElement("div");
img.id = "onboarding-overlay-icon";
@@ -259,25 +321,27 @@ class Onboarding {
let doc = this._window.document;
let script = doc.createElement("script");
script.type = "text/javascript";
script.src = uri;
doc.head.appendChild(script);
}
}
-addEventListener("load", function onLoad(evt) {
- if (!content || evt.target != content.document) {
- return;
- }
- removeEventListener("load", onLoad);
+// Load onboarding module only when we enable it.
+if (Services.prefs.getBoolPref("browser.onboarding.enabled", false) &&
+ !Services.prefs.getBoolPref("browser.onboarding.hidden", false)) {
+
+ addEventListener("load", function onLoad(evt) {
+ if (!content || evt.target != content.document) {
+ return;
+ }
+ removeEventListener("load", onLoad);
- let window = evt.target.defaultView;
- // Load onboarding module only when we enable it.
- if ((window.location.href == ABOUT_NEWTAB_URL ||
- window.location.href == ABOUT_HOME_URL) &&
- Services.prefs.getBoolPref("browser.onboarding.enabled", false)) {
-
- window.requestIdleCallback(() => {
- new Onboarding(window);
- });
- }
-}, true);
+ let window = evt.target.defaultView;
+ let location = window.location.href;
+ if (location == ABOUT_NEWTAB_URL || location == ABOUT_HOME_URL) {
+ window.requestIdleCallback(() => {
+ new Onboarding(window);
+ });
+ }
+ }, true);
+}
--- a/browser/extensions/onboarding/locales/en-US/onboarding.properties
+++ b/browser/extensions/onboarding/locales/en-US/onboarding.properties
@@ -16,8 +16,9 @@ onboarding.tour-search.description=Having a default search engine doesn’t mean it’s the only one you use. Pick a search engine or a site, like Amazon or Wikipedia, to search on the fly.
onboarding.tour-search.button=Open One-Click Search
onboarding.tour-private-browsing=Private Browsing
onboarding.tour-private-browsing.title=A little privacy goes a long way.
# LOCALIZATION NOTE(onboarding.tour-private-browsing.description): %S is
# brandShortName.
onboarding.tour-private-browsing.description=Browse the internet without saving your searches or the sites you visited. When your session ends, the cookies disappear from %S like they were never there.
onboarding.tour-private-browsing.button=Show Private Browsing in Menu
+onboarding.hidden-checkbox-label=Hide the tour
--- a/browser/extensions/onboarding/moz.build
+++ b/browser/extensions/onboarding/moz.build
@@ -12,9 +12,11 @@ DIRS += ['locales']
FINAL_TARGET_PP_FILES.features['onboarding@mozilla.org'] += [
'install.rdf.in'
]
FINAL_TARGET_FILES.features['onboarding@mozilla.org'] += [
'bootstrap.js',
]
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
+
JAR_MANIFESTS += ['jar.mn']
new file mode 100644
--- /dev/null
+++ b/browser/extensions/onboarding/test/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "plugin:mozilla/browser-test"
+ ],
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/onboarding/test/browser/browser.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[browser_onboarding_hide_tours.js]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/onboarding/test/browser/browser_onboarding_hide_tours.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const ABOUT_HOME_URL = "about:home";
+const ABOUT_NEWTAB_URL = "about:newtab";
+
+function assertOnboardingDestroyed(browser) {
+ return ContentTask.spawn(browser, {}, function() {
+ let expectedRemovals = [
+ "#onboarding-overlay",
+ "#onboarding-overlay-icon"
+ ];
+ for (let selector of expectedRemovals) {
+ let removal = content.document.querySelector(selector);
+ ok(!removal, `Should remove ${selector} onboarding element`);
+ }
+ });
+}
+
+add_task(async function test_hide_onboarding_tours() {
+ await SpecialPowers.pushPrefEnv({set: [["browser.onboarding.enabled", true]]});
+ await SpecialPowers.pushPrefEnv({set: [["browser.onboarding.hidden", false]]});
+ await SpecialPowers.pushPrefEnv({set: [["browser.onboarding.notification.finished", false]]});
+
+ let newtab = await BrowserTestUtils.openNewForegroundTab(gBrowser, ABOUT_NEWTAB_URL);
+ await promiseOnboardingOverlayLoaded(newtab.linkedBrowser);
+ let hometab = await BrowserTestUtils.openNewForegroundTab(gBrowser, ABOUT_HOME_URL);
+ await promiseOnboardingOverlayLoaded(hometab.linkedBrowser);
+
+ let expectedPrefUpdates = [
+ promisePrefUpdated("browser.onboarding.hidden", true),
+ promisePrefUpdated("browser.onboarding.notification.finished", true)
+ ];
+ await BrowserTestUtils.synthesizeMouseAtCenter("#onboarding-overlay-icon", {}, hometab.linkedBrowser);
+ await promiseOnboardingOverlayOpened(hometab.linkedBrowser);
+ await BrowserTestUtils.synthesizeMouseAtCenter("#onboarding-tour-hidden-checkbox", {}, hometab.linkedBrowser);
+ await BrowserTestUtils.synthesizeMouseAtCenter("#onboarding-overlay-close-btn", {}, hometab.linkedBrowser);
+ await Promise.all(expectedPrefUpdates);
+
+ // Test the hiding operation works arcoss pages
+ await assertOnboardingDestroyed(hometab.linkedBrowser);
+ await BrowserTestUtils.removeTab(hometab);
+ await assertOnboardingDestroyed(newtab.linkedBrowser);
+ await BrowserTestUtils.removeTab(newtab);
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/onboarding/test/browser/head.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+function promiseOnboardingOverlayLoaded(browser) {
+ // The onboarding overlay is init inside window.requestIdleCallback, not immediately,
+ // so we use check conditions here.
+ let condition = () => {
+ return ContentTask.spawn(browser, {}, function() {
+ return new Promise(resolve => {
+ let doc = content && content.document;
+ if (doc && doc.querySelector("#onboarding-overlay")) {
+ resolve(true);
+ return;
+ }
+ resolve(false);
+ });
+ })
+ };
+ return BrowserTestUtils.waitForCondition(
+ condition,
+ "Should load onboarding overlay",
+ 100,
+ 30
+ );
+}
+
+function promiseOnboardingOverlayOpened(browser) {
+ let condition = () => {
+ return ContentTask.spawn(browser, {}, function() {
+ return new Promise(resolve => {
+ let overlay = content.document.querySelector("#onboarding-overlay");
+ if (overlay.classList.contains("opened")) {
+ resolve(true);
+ return;
+ }
+ resolve(false);
+ });
+ })
+ };
+ return BrowserTestUtils.waitForCondition(
+ condition,
+ "Should open onboarding overlay",
+ 100,
+ 30
+ );
+}
+
+function promisePrefUpdated(name, expectedValue) {
+ return new Promise(resolve => {
+ let onUpdate = actualValue => {
+ Preferences.ignore(name, onUpdate);
+ is(expectedValue, actualValue, `Should update the pref of ${name}`);
+ resolve();
+ };
+ Preferences.observe(name, onUpdate);
+ });
+}