Bug 1393489 - Add API to give Snippets in Activity Stream control over when to show onboarding draft onboarding
authork88hudson <khudson@mozilla.com>
Mon, 11 Sep 2017 14:28:04 -0400
changeset 667820 0c93796334876ad2c2677076db8d973b65982062
parent 662980 b0e945eed81db8bf076daf64e381c514f70144f0
child 732523 24bcbda6b573be32f3072525b4cd621f368a33a9
push id80859
push userkhudson@mozilla.com
push dateWed, 20 Sep 2017 19:53:06 +0000
bugs1393489
milestone57.0a1
Bug 1393489 - Add API to give Snippets in Activity Stream control over when to show onboarding MozReview-Commit-ID: D5sHjvYeZoB
browser/extensions/activity-stream/data/content/activity-stream-prerendered.html
browser/extensions/activity-stream/data/content/activity-stream.html
browser/extensions/onboarding/content/onboarding.js
browser/extensions/onboarding/test/browser/browser.ini
browser/extensions/onboarding/test/browser/browser_onboarding_notification.js
browser/extensions/onboarding/test/browser/browser_onboarding_notification_4.js
browser/extensions/onboarding/test/browser/browser_onboarding_notification_5.js
browser/extensions/onboarding/test/browser/browser_onboarding_notification_enabled.js
browser/extensions/onboarding/test/browser/head.js
--- a/browser/extensions/activity-stream/data/content/activity-stream-prerendered.html
+++ b/browser/extensions/activity-stream/data/content/activity-stream-prerendered.html
@@ -3,17 +3,17 @@
   <head>
     <meta charset="utf-8">
     <meta http-equiv="Content-Security-Policy-Report-Only" content="script-src 'unsafe-inline'; img-src http: https: data: blob:; style-src 'unsafe-inline'; child-src 'none'; object-src 'none'; report-uri https://tiles.services.mozilla.com/v4/links/activity-stream/csp">
     <title></title>
     <link rel="icon" type="image/png" id="favicon" href="chrome://branding/content/icon32.png"/>
     <link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
     <link rel="stylesheet" href="resource://activity-stream/data/content/activity-stream.css" />
   </head>
-  <body class="activity-stream">
+  <body class="activity-stream" data-auto-onboarding-notification="false">
     <div id="root"><div class="outer-wrapper" data-reactroot="" data-reactid="1" data-react-checksum="57168132"><main data-reactid="2"><div class="search-wrapper" data-reactid="3"><label for="newtab-search-text" class="search-label" data-reactid="4"><span class="sr-only" data-reactid="5"><span data-reactid="6">Search the Web</span></span></label><input type="search" id="newtab-search-text" maxlength="256" placeholder="Search the Web" title="Search the Web" data-reactid="7"/><button id="searchSubmit" class="search-button" title=" " data-reactid="8"><span class="sr-only" data-reactid="9"><span data-reactid="10"> </span></span></button></div><section class="top-sites" data-reactid="11"><h3 class="section-title" data-reactid="12"><span class="icon icon-small-spacer icon-topsites" data-reactid="13"></span><span data-reactid="14"> </span></h3><ul class="top-sites-list" data-reactid="15"><li class="top-site-outer placeholder" data-reactid="16"><a data-reactid="17"><div class="tile" aria-hidden="true" data-reactid="18"><span class="letter-fallback" data-reactid="19"></span><div class="screenshot" style="background-image:none;" data-reactid="20"></div></div><div class="title " data-reactid="21"><span dir="auto" data-reactid="22"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="23"><a data-reactid="24"><div class="tile" aria-hidden="true" data-reactid="25"><span class="letter-fallback" data-reactid="26"></span><div class="screenshot" style="background-image:none;" data-reactid="27"></div></div><div class="title " data-reactid="28"><span dir="auto" data-reactid="29"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="30"><a data-reactid="31"><div class="tile" aria-hidden="true" data-reactid="32"><span class="letter-fallback" data-reactid="33"></span><div class="screenshot" style="background-image:none;" data-reactid="34"></div></div><div class="title " data-reactid="35"><span dir="auto" data-reactid="36"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="37"><a data-reactid="38"><div class="tile" aria-hidden="true" data-reactid="39"><span class="letter-fallback" data-reactid="40"></span><div class="screenshot" style="background-image:none;" data-reactid="41"></div></div><div class="title " data-reactid="42"><span dir="auto" data-reactid="43"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="44"><a data-reactid="45"><div class="tile" aria-hidden="true" data-reactid="46"><span class="letter-fallback" data-reactid="47"></span><div class="screenshot" style="background-image:none;" data-reactid="48"></div></div><div class="title " data-reactid="49"><span dir="auto" data-reactid="50"></span></div></a></li><li class="top-site-outer placeholder" data-reactid="51"><a data-reactid="52"><div class="tile" aria-hidden="true" data-reactid="53"><span class="letter-fallback" data-reactid="54"></span><div class="screenshot" style="background-image:none;" data-reactid="55"></div></div><div class="title " data-reactid="56"><span dir="auto" data-reactid="57"></span></div></a></li></ul></section><div class="sections-list" data-reactid="58"><section data-reactid="59"><div class="section-top-bar" data-reactid="60"><h3 class="section-title" data-reactid="61"><span class="icon icon-small-spacer icon-pocket" data-reactid="62"></span><span data-reactid="63"> </span></h3></div><ul class="section-list" style="padding:0;" data-reactid="64"><li class="card-outer placeholder" data-reactid="65"><a data-reactid="66"><div class="card" data-reactid="67"><div class="card-details no-image" data-reactid="68"><div class="card-text no-image no-host-name no-context" data-reactid="69"><h4 class="card-title" dir="auto" data-reactid="70"></h4><p class="card-description" dir="auto" data-reactid="71"></p></div></div></div></a></li><li class="card-outer placeholder" data-reactid="72"><a data-reactid="73"><div class="card" data-reactid="74"><div class="card-details no-image" data-reactid="75"><div class="card-text no-image no-host-name no-context" data-reactid="76"><h4 class="card-title" dir="auto" data-reactid="77"></h4><p class="card-description" dir="auto" data-reactid="78"></p></div></div></div></a></li><li class="card-outer placeholder" data-reactid="79"><a data-reactid="80"><div class="card" data-reactid="81"><div class="card-details no-image" data-reactid="82"><div class="card-text no-image no-host-name no-context" data-reactid="83"><h4 class="card-title" dir="auto" data-reactid="84"></h4><p class="card-description" dir="auto" data-reactid="85"></p></div></div></div></a></li></ul><div class="topic" data-reactid="86"><span data-reactid="87"><span data-reactid="88"> </span></span><ul data-reactid="89"><li data-reactid="90"><a class="topic-link" data-reactid="91"></a></li></ul></div></section></div><!-- react-empty: 92 --></main></div></div>
     <div id="snippets-container">
       <div id="snippets"></div>
     </div>
 <script src="resource://activity-stream/data/content/activity-stream-initial-state.js"></script>
     <script src="chrome://browser/content/contentSearchUI.js"></script>
     <script src="resource://activity-stream/vendor/react.js"></script>
     <script src="resource://activity-stream/vendor/react-dom.js"></script>
--- a/browser/extensions/activity-stream/data/content/activity-stream.html
+++ b/browser/extensions/activity-stream/data/content/activity-stream.html
@@ -3,17 +3,17 @@
   <head>
     <meta charset="utf-8">
     <meta http-equiv="Content-Security-Policy-Report-Only" content="script-src 'unsafe-inline'; img-src http: https: data: blob:; style-src 'unsafe-inline'; child-src 'none'; object-src 'none'; report-uri https://tiles.services.mozilla.com/v4/links/activity-stream/csp">
     <title></title>
     <link rel="icon" type="image/png" id="favicon" href="chrome://branding/content/icon32.png"/>
     <link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
     <link rel="stylesheet" href="resource://activity-stream/data/content/activity-stream.css" />
   </head>
-  <body class="activity-stream">
+  <body class="activity-stream" data-auto-onboarding-notification="false">
     <div id="root"></div>
     <div id="snippets-container">
       <div id="snippets"></div>
     </div>
     <script src="chrome://browser/content/contentSearchUI.js"></script>
     <script src="resource://activity-stream/vendor/react.js"></script>
     <script src="resource://activity-stream/vendor/react-dom.js"></script>
     <script src="resource://activity-stream/vendor/react-intl.js"></script>
--- a/browser/extensions/onboarding/content/onboarding.js
+++ b/browser/extensions/onboarding/content/onboarding.js
@@ -18,17 +18,18 @@ const UITOUR_JS_URI = "resource://onboar
 const TOUR_AGENT_JS_URI = "resource://onboarding/onboarding-tour-agent.js";
 const BRAND_SHORT_NAME = Services.strings
                      .createBundle("chrome://branding/locale/brand.properties")
                      .GetStringFromName("brandShortName");
 const PROMPT_COUNT_PREF = "browser.onboarding.notification.prompt-count";
 const ONBOARDING_DIALOG_ID = "onboarding-overlay-dialog";
 const ONBOARDING_MIN_WIDTH_PX = 960;
 const SPEECH_BUBBLE_MIN_WIDTH_PX = 1130;
-
+const PREF_AS_ENABLED = "browser.newtabpage.activity-stream.enabled";
+const PREF_AS_HOME_ENABLED = "browser.newtabpage.activity-stream.aboutHome.enabled";
 /**
  * Add any number of tours, key is the tourId, value should follow the format below
  * "tourId": { // The short tour id which could be saved in pref
  *   // The unique tour id
  *   id: "onboarding-tour-addons",
  *   // The string id of tour name which would be displayed on the navigation bar
  *   tourNameId: "onboarding.tour-addon",
  *   // The method returing strings used on tour notification
@@ -339,21 +340,29 @@ function sendMessageToChrome(action, par
   });
 }
 /**
  * The script won't be initialized if we turned off onboarding by
  * setting "browser.onboarding.enabled" to false.
  */
 class Onboarding {
   constructor(contentWindow) {
+
+    this._window = contentWindow;
+
+    // If we're loading on a page with activity stream, do not enable notifications right away.
+    // Instead, they will be enabled by the gOnboarding API.
+    const autoShowNotifications = this._window.document.body.getAttribute("data-auto-onboarding-notification") !== "false";
+    this._notificationsEnabled = autoShowNotifications;
+
+    this._notificationInitializationNeeded = false;
     this.init(contentWindow);
   }
 
   async init(contentWindow) {
-    this._window = contentWindow;
     this._tours = [];
     this._tourType = Services.prefs.getStringPref("browser.onboarding.tour-type", "update");
 
     let tourIds = this._getTourIDList();
     tourIds.forEach(tourId => {
       if (onboardingTourset[tourId]) {
         this._tours.push(onboardingTourset[tourId]);
       }
@@ -374,16 +383,38 @@ class Onboarding {
 
     // 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.uiInitialized = false;
     this._resizeTimerId =
       this._window.requestIdleCallback(() => this._resizeUI());
+
+    exposeOnboarding(contentWindow, this)
+  }
+
+  get notificationsEnabled() {
+    return this._notificationsEnabled;
+  }
+
+  enableNotifications() {
+    this._notificationsEnabled = true;
+    if (this._notificationInitializationNeeded) {
+      this.initNotifications();
+    }
+  }
+
+  disableNotifications() {
+    this._notificationsEnabled = false;
+    this.hideNotification();
+  }
+
+  initNotifications() {
+    this._window.requestIdleCallback(() => this._initNotification());
   }
 
   _resizeUI() {
     let width = this._window.document.body.getBoundingClientRect().width;
     if (width < ONBOARDING_MIN_WIDTH_PX) {
       // Don't show the overlay UI before we get to a better, responsive design.
       this.destroy();
       return;
@@ -414,26 +445,37 @@ class Onboarding {
     this._overlay = this._renderOverlay();
     this._overlay.addEventListener("click", this);
     this._overlay.addEventListener("keypress", this);
     body.appendChild(this._overlay);
 
     this._loadJS(TOUR_AGENT_JS_URI);
 
     this._initPrefObserver();
-    // Doing tour notification takes some effort. Let's do it on idle.
-    this._window.requestIdleCallback(() => this._initNotification());
+
+    this._notificationInitializationNeeded = true;
+
+    if (this.notificationsEnabled) {
+      this.initNotifications();
+    }
+
   }
 
   _getTourIDList() {
     let tours = Services.prefs.getStringPref(`browser.onboarding.${this._tourType}tour`, "");
     return tours.split(",").filter(tourId => tourId !== "").map(tourId => tourId.trim());
   }
 
   _initNotification() {
+    if (!this._notificationInitializationNeeded || !this.shouldShowNotifications) {
+      return;
+    }
+
+    this._notificationInitializationNeeded = false;
+
     let doc = this._window.document;
     if (doc.hidden) {
       // When the preloaded-browser feature is on,
       // it would preload an hidden about:newtab in the background.
       // We don't want to show notification in that hidden state.
       let onVisible = () => {
         if (!doc.hidden) {
           doc.removeEventListener("visibilitychange", onVisible);
@@ -878,25 +920,30 @@ class Onboarding {
       sendMessageToChrome("set-prefs", [{
         name: "browser.onboarding.notification.tour-ids-queue",
         value: queue
       }]);
     }
     return queue ? queue.split(",") : [];
   }
 
-  showNotification() {
+  get shouldShowNotifications() {
     if (Services.prefs.getBoolPref("browser.onboarding.notification.finished", false)) {
-      return;
+      return false;
     }
-
     let lastTime = this._getLastTourChangeTime();
     if (this._muteNotificationOnFirstSession(lastTime)) {
-      return;
+      return false;
     }
+    return true;
+  }
+
+  showNotification() {
+    let lastTime = this._getLastTourChangeTime();
+
     // After the notification mute on the 1st session,
     // we don't want to show the speech bubble by default
     this._overlayIcon.classList.remove("onboarding-speech-bubble");
 
     let queue = this._getNotificationQueue();
     let totalMaxTime = Services.prefs.getIntPref("browser.onboarding.notification.max-life-time-all-tours-ms");
     if (lastTime && Date.now() - lastTime >= totalMaxTime) {
       // Reach total max life time for all tour notifications.
@@ -1134,16 +1181,34 @@ class Onboarding {
     let doc = this._window.document;
     let script = doc.createElement("script");
     script.type = "text/javascript";
     script.src = uri;
     doc.head.appendChild(script);
   }
 }
 
+// This should be called with onboarding has initialized
+function exposeOnboarding(window, onboarding) {
+  const gOnboarding = {
+    shouldShowNotifications: false,
+    notificationsEnabled: false
+  };
+  if (onboarding) {
+    gOnboarding.notificationsEnabled = onboarding.notificationsEnabled;
+    gOnboarding.tourType = onboarding._tourType;
+    gOnboarding.shouldShowNotifications = onboarding.shouldShowNotifications;
+    gOnboarding.enableNotifications = () => onboarding.enableNotifications();
+    gOnboarding.disableNotifications = () => onboarding.disableNotifications();
+  }
+  window.wrappedJSObject.gOnboarding = Cu.cloneInto(gOnboarding, window, {cloneFunctions: true});
+  const event = new Event("gOnboarding:Loaded");
+  window.dispatchEvent(event);
+}
+
 // Load onboarding module only when we enable it.
 if (Services.prefs.getBoolPref("browser.onboarding.enabled", false)) {
   addEventListener("load", function onLoad(evt) {
     if (!content || evt.target != content.document) {
       return;
     }
 
     let window = evt.target.defaultView;
--- a/browser/extensions/onboarding/test/browser/browser.ini
+++ b/browser/extensions/onboarding/test/browser/browser.ini
@@ -6,13 +6,14 @@ support-files =
 [browser_onboarding_keyboard.js]
 skip-if = debug || os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences
 [browser_onboarding_notification.js]
 [browser_onboarding_notification_2.js]
 [browser_onboarding_notification_3.js]
 [browser_onboarding_notification_4.js]
 [browser_onboarding_notification_5.js]
 [browser_onboarding_notification_click_auto_complete_tour.js]
+[browser_onboarding_notification_enabled.js]
 [browser_onboarding_select_default_tour.js]
 [browser_onboarding_skip_tour.js]
 [browser_onboarding_tours.js]
 [browser_onboarding_tourset.js]
 [browser_onboarding_uitour.js]
--- a/browser/extensions/onboarding/test/browser/browser_onboarding_notification.js
+++ b/browser/extensions/onboarding/test/browser/browser_onboarding_notification.js
@@ -15,16 +15,17 @@ add_task(async function test_show_tour_n
   let targetTourId = null;
   let expectedPrefUpdate = null;
   await loopTourNotificationQueueOnceInOrder();
   await loopTourNotificationQueueOnceInOrder();
 
   expectedPrefUpdate = promisePrefUpdated("browser.onboarding.notification.finished", true);
   await reloadTab(tab);
   await promiseOnboardingOverlayLoaded(tab.linkedBrowser);
+  await promiseTourNotificationOpened(tab.linkedBrowser, false);
   await expectedPrefUpdate;
   let tourId = await getCurrentNotificationTargetTourId(tab.linkedBrowser);
   ok(!tourId, "Should not prompt each tour for more than 2 chances.");
   await BrowserTestUtils.removeTab(tab);
 
   async function loopTourNotificationQueueOnceInOrder() {
     for (let i = 0; i < tourIds.length; ++i) {
       if (tab) {
--- a/browser/extensions/onboarding/test/browser/browser_onboarding_notification_4.js
+++ b/browser/extensions/onboarding/test/browser/browser_onboarding_notification_4.js
@@ -12,16 +12,17 @@ add_task(async function test_remove_all_
   let tourIds = TOUR_IDs;
   let tab = null;
   let targetTourId = null;
   await closeTourNotificationsOneByOne();
 
   let expectedPrefUpdate = promisePrefUpdated("browser.onboarding.notification.finished", true);
   await reloadTab(tab);
   await promiseOnboardingOverlayLoaded(tab.linkedBrowser);
+  await promiseTourNotificationOpened(tab.linkedBrowser, false);
   await expectedPrefUpdate;
   let tourId = await getCurrentNotificationTargetTourId(tab.linkedBrowser);
   ok(!tourId, "Should not prompt tour notifications any more after closing all notifcations.");
   await BrowserTestUtils.removeTab(tab);
 
   async function closeTourNotificationsOneByOne() {
     for (let i = 0; i < tourIds.length; ++i) {
       if (tab) {
@@ -46,16 +47,17 @@ add_task(async function test_remove_all_
   let tourIds = TOUR_IDs;
   let tab = null;
   let targetTourId = null;
   await clickTourNotificationActionButtonsOneByOne();
 
   let expectedPrefUpdate = promisePrefUpdated("browser.onboarding.notification.finished", true);
   await reloadTab(tab);
   await promiseOnboardingOverlayLoaded(tab.linkedBrowser);
+  await promiseTourNotificationOpened(tab.linkedBrowser, false);
   await expectedPrefUpdate;
   let tourId = await getCurrentNotificationTargetTourId(tab.linkedBrowser);
   ok(!tourId, "Should not prompt tour notifcations any more after taking actions on all notifcations.");
   await BrowserTestUtils.removeTab(tab);
 
   async function clickTourNotificationActionButtonsOneByOne() {
     for (let i = 0; i < tourIds.length; ++i) {
       if (tab) {
--- a/browser/extensions/onboarding/test/browser/browser_onboarding_notification_5.js
+++ b/browser/extensions/onboarding/test/browser/browser_onboarding_notification_5.js
@@ -11,11 +11,12 @@ add_task(async function test_finish_tour
   await promiseOnboardingOverlayLoaded(tab.linkedBrowser);
   await promiseTourNotificationOpened(tab.linkedBrowser);
 
   let totalMaxTime = Preferences.get("browser.onboarding.notification.max-life-time-all-tours-ms");
   Preferences.set("browser.onboarding.notification.last-time-of-changing-tour-sec", Math.floor((Date.now() - totalMaxTime) / 1000));
   let expectedPrefUpdate = promisePrefUpdated("browser.onboarding.notification.finished", true);
   await reloadTab(tab);
   await promiseOnboardingOverlayLoaded(tab.linkedBrowser);
+  await promiseTourNotificationOpened(tab.linkedBrowser, false);
   await expectedPrefUpdate;
   await BrowserTestUtils.removeTab(tab);
 });
new file mode 100644
--- /dev/null
+++ b/browser/extensions/onboarding/test/browser/browser_onboarding_notification_enabled.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+requestLongerTimeout(3);
+
+async function openTabWithPrefs(url, prefs) {
+  resetOnboardingDefaultState();
+  skipMuteNotificationOnFirstSession();
+  await SpecialPowers.pushPrefEnv({set: prefs});
+  return await openTab(url);
+}
+
+async function getNotificationAttribute(browser) {
+  return ContentTask.spawn(browser, {}, async () => {
+    return content.document.body.getAttribute("data-auto-onboarding-notification");
+  });
+}
+
+add_task(async function test_activity_stream_has_expectedAttribute() {
+  const tab = await openTabWithPrefs(ABOUT_NEWTAB_URL, [
+    ["browser.newtabpage.activity-stream.enabled", true]
+  ]);
+
+  is(await getNotificationAttribute(tab.linkedBrowser), "false",
+    "data-auto-onboarding-notification attr on <body> should be \"false\" on about:newtab with Activity Stream");
+
+  await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_activity_stream_has_expectedAttribute() {
+  const tab = await openTabWithPrefs(ABOUT_HOME_URL, [
+    ["browser.newtabpage.activity-stream.enabled", true],
+    ["browser.newtabpage.activity-stream.aboutHome.enabled", true],
+  ]);
+
+  is(await getNotificationAttribute(tab.linkedBrowser), "false",
+    "data-auto-onboarding-notification attr on <body> should be \"false\" on about:home with Activity Stream");
+
+  await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_activity_stream_notificationsEnabled_about_newtab_with_as() {
+  const tab = await openTabWithPrefs(ABOUT_NEWTAB_URL, [
+    ["browser.newtabpage.activity-stream.enabled", true],
+    ["browser.newtabpage.activity-stream.aboutHome.enabled", false],
+  ]);
+
+  const shown = await checkNotificationUIShown(tab.linkedBrowser, false);
+  is(shown, false, "should hide notifications on about:newtab with AS");
+
+  await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_activity_stream_notificationsEnabled_about_newtab_without_as() {
+  const tab = await openTabWithPrefs(ABOUT_NEWTAB_URL, [
+    ["browser.newtabpage.activity-stream.enabled", false],
+    ["browser.newtabpage.activity-stream.aboutHome.enabled", false],
+  ]);
+
+  const shown = await checkNotificationUIShown(tab.linkedBrowser, false);
+  is(shown, true, "should show notifications on about:newtab without AS");
+
+  await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_activity_stream_notificationsEnabled_about_home_without_as() {
+  const tab = await openTabWithPrefs(ABOUT_HOME_URL, [
+    ["browser.newtabpage.activity-stream.enabled", true],
+    ["browser.newtabpage.activity-stream.aboutHome.enabled", false],
+  ]);
+
+  const shown = await checkNotificationUIShown(tab.linkedBrowser, true);
+  is(shown, true, "should show notifications on about:home without AS");
+
+  await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_activity_stream_notificationsEnabled_about_home_with_as() {
+  const tab = await openTabWithPrefs(ABOUT_HOME_URL, [
+    ["browser.newtabpage.activity-stream.enabled", true],
+    ["browser.newtabpage.activity-stream.aboutHome.enabled", true],
+  ]);
+
+  const shown = await checkNotificationUIShown(tab.linkedBrowser, false);
+  is(shown, false, "should hide notifications on about:home with AS");
+
+  await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_enable_notifications() {
+  const tab = await openTabWithPrefs(ABOUT_HOME_URL, [
+    ["browser.newtabpage.activity-stream.enabled", true],
+    ["browser.newtabpage.activity-stream.aboutHome.enabled", true],
+  ]);
+
+  await promiseTourNotificationOpened(tab.linkedBrowser);
+  ok(true, "should show notifications after calling gOnboarding.enableNotifications()");
+
+  await BrowserTestUtils.removeTab(tab);
+});
--- a/browser/extensions/onboarding/test/browser/head.js
+++ b/browser/extensions/onboarding/test/browser/head.js
@@ -112,42 +112,58 @@ function promisePrefUpdated(name, expect
       Preferences.ignore(name, onUpdate);
       is(expectedValue, actualValue, `Should update the pref of ${name}`);
       resolve();
     };
     Preferences.observe(name, onUpdate);
   });
 }
 
-function promiseTourNotificationOpened(browser) {
-  function isOpened() {
-    let doc = content && content.document;
-    let notification = doc.querySelector("#onboarding-notification-bar");
-    if (notification && notification.classList.contains("onboarding-opened")) {
-      ok(true, "Should open tour notification");
-      return Promise.resolve();
+function promiseWaitForOnboardingAPI(browser) {
+  return ContentTask.spawn(browser, {}, async () => {
+    if (!content.wrappedJSObject.gOnboarding) {
+      await new Promise(resolve => {
+        content.addEventListener("gOnboarding:Loaded", resolve);
+      });
     }
-    return new Promise(resolve => {
-      let observer = new content.MutationObserver(mutations => {
-        mutations.forEach(mutation => {
-          let bar = Array.from(mutation.addedNodes)
-                         .find(node => node.id == "onboarding-notification-bar");
-          if (bar && bar.classList.contains("onboarding-opened")) {
-            observer.disconnect();
-            ok(true, "Should open tour notification");
-            resolve();
-          }
-        });
+  });
+}
+
+async function checkNotificationUIShown(browser, shouldBeShown) {
+  await promiseWaitForOnboardingAPI(browser);
+  if (shouldBeShown) {
+    await ContentTask.spawn(browser, {}, async () => {
+      await ContentTaskUtils.waitForCondition(() => {
+        const el = content.document.getElementById("onboarding-notification-bar")
+        return el && el.classList.contains("onboarding-opened");
       });
-      observer.observe(doc.body, { childList: true });
     });
+    return true;
   }
-  return ContentTask.spawn(browser, {}, isOpened);
+  return await ContentTask.spawn(browser, {}, async () => {
+    const el = content.document.getElementById("onboarding-notification-bar")
+    if (el && el.classList.contains("onboarding-opened")) {
+      return true;
+    }
+    return content.wrappedJSObject.gOnboarding.notificationsEnabled;
+  });
 }
 
+async function promiseTourNotificationOpened(browser, waitForUi = true) {
+  await promiseWaitForOnboardingAPI(browser);
+  await ContentTask.spawn(browser, {}, async function() {
+    ok(content.wrappedJSObject.gOnboarding.shouldShowNotifications, "gOnboarding.shouldShowNotifications is true");
+    content.wrappedJSObject.gOnboarding.enableNotifications();
+  });
+  if (waitForUi) {
+    await checkNotificationUIShown(browser, true);
+  }
+}
+
+
 function promiseTourNotificationClosed(browser) {
   let condition = () => {
     return ContentTask.spawn(browser, {}, function() {
       return new Promise(resolve => {
         let bar = content.document.querySelector("#onboarding-notification-bar");
         if (bar && !bar.classList.contains("onboarding-opened")) {
           resolve(true);
           return;