Bug 1392475 - [Onboarding] Turn fox logo to watermark if all tours or notifications are finished. r=gasolin, Fischer draft
authorRex Lee <rexboy@mozilla.com>
Wed, 30 Aug 2017 15:24:47 +0800
changeset 663493 8388a80c370c926a639dc34a041c9708ee933f85
parent 663134 a73cc4e08bf5a005722c95b43f84ab0c8ff2bc7c
child 731234 0cf27aedd3440ee8fafdff370e870194c30249db
push id79470
push userbmo:rexboy@mozilla.com
push dateWed, 13 Sep 2017 05:54:40 +0000
reviewersgasolin, Fischer
bugs1392475
milestone57.0a1
Bug 1392475 - [Onboarding] Turn fox logo to watermark if all tours or notifications are finished. r=gasolin, Fischer MozReview-Commit-ID: CLbiHqCmxr0
browser/app/profile/firefox.js
browser/components/nsBrowserGlue.js
browser/extensions/onboarding/OnboardingTourType.jsm
browser/extensions/onboarding/README.md
browser/extensions/onboarding/bootstrap.js
browser/extensions/onboarding/content/img/watermark64.png
browser/extensions/onboarding/content/onboarding.css
browser/extensions/onboarding/content/onboarding.js
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_skip_tour.js
browser/extensions/onboarding/test/browser/browser_onboarding_tours.js
browser/extensions/onboarding/test/browser/head.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1711,16 +1711,17 @@ pref("urlclassifier.downloadAllowTable",
 pref("urlclassifier.downloadBlockTable", "goog-badbinurl-proto");
 
 pref("browser.suppress_first_window_animation", true);
 
 // Preferences for Photon onboarding system extension
 pref("browser.onboarding.enabled", true);
 // Mark this as an upgraded profile so we don't offer the initial new user onboarding tour.
 pref("browser.onboarding.tourset-version", 2);
+pref("browser.onboarding.state", "default");
 // 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);
 pref("browser.onboarding.notification.mute-duration-on-first-session-ms", 300000); // 5 mins
 pref("browser.onboarding.notification.max-life-time-per-tour-ms", 432000000); // 5 days
 pref("browser.onboarding.notification.max-life-time-all-tours-ms", 1209600000); // 14 days
 pref("browser.onboarding.notification.max-prompt-count-per-tour", 8);
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -1682,17 +1682,17 @@ BrowserGlue.prototype = {
         return;
       this._openPreferences("sync", { origin: "doorhanger" });
     }
     this.AlertsService.showAlertNotification(null, title, body, true, null, clickCallback);
   },
 
   // eslint-disable-next-line complexity
   _migrateUI: function BG__migrateUI() {
-    const UI_VERSION = 53;
+    const UI_VERSION = 54;
     const BROWSER_DOCURL = "chrome://browser/content/browser.xul";
 
     let currentUIVersion;
     if (Services.prefs.prefHasUserValue("browser.migration.version")) {
       currentUIVersion = Services.prefs.getIntPref("browser.migration.version");
     } else {
       // This is a new profile, nothing to migrate.
       Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
@@ -2073,16 +2073,25 @@ BrowserGlue.prototype = {
         let malwareList = Services.prefs.getCharPref(MALWARE_PREF);
         if (malwareList.indexOf("goog-malware-shavar") != -1) {
           malwareList.replace("goog-malware-shavar", "goog-malware-proto");
           Services.prefs.setCharPref(MALWARE_PREF, malwareList);
         }
       }
     }
 
+    if (currentUIVersion < 54) {
+      // Migrate browser.onboarding.hidden to browser.onboarding.state.
+      if (Services.prefs.prefHasUserValue("browser.onboarding.hidden")) {
+        let state = Services.prefs.getBoolPref("browser.onboarding.hidden") ? "watermark" : "default";
+        Services.prefs.setStringPref("browser.onboarding.state", state);
+        Services.prefs.clearUserPref("browser.onboarding.hidden");
+      }
+    }
+
     // Update the migration version.
     Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
   },
 
   _checkForDefaultBrowser() {
     // Perform default browser checking.
     if (!ShellService) {
       return;
--- a/browser/extensions/onboarding/OnboardingTourType.jsm
+++ b/browser/extensions/onboarding/OnboardingTourType.jsm
@@ -32,12 +32,13 @@ var OnboardingTourType = {
     } else if (Services.prefs.getIntPref(PREF_SEEN_TOURSET_VERSION) < TOURSET_VERSION) {
       // show the update user tour when tour set version is larger than the seen tourset version
       Services.prefs.setStringPref(PREF_TOUR_TYPE, "update");
       // Reset all the notification-related prefs because tours update.
       Services.prefs.setBoolPref("browser.onboarding.notification.finished", false);
       Services.prefs.clearUserPref("browser.onboarding.notification.prompt-count");
       Services.prefs.clearUserPref("browser.onboarding.notification.last-time-of-changing-tour-sec");
       Services.prefs.clearUserPref("browser.onboarding.notification.tour-ids-queue");
+      Services.prefs.clearUserPref("browser.onboarding.state");
     }
     Services.prefs.setIntPref(PREF_SEEN_TOURSET_VERSION, TOURSET_VERSION);
   },
 };
--- a/browser/extensions/onboarding/README.md
+++ b/browser/extensions/onboarding/README.md
@@ -47,8 +47,16 @@ Edit `browser/app/profile/firefox.js` an
 
 The tourset version is used to track the last major tourset change version. The `tourset-version` pref store the major tourset version (ex: `1`) but not the current browser version. When browser update to the next version (ex: 58, 59) the tourset pref is still `1` if we didn't do any major tourset update.
 
 Once the tour set version is updated (ex: `2`), onboarding overlay should show the update tour to the updated user (ex: update from v56 -> v57), even when user has watched the previous tours or preferred to hide the previous tours.
 
 Edit `browser/app/profile/firefox.js` and set `browser.onboarding.tourset-version` as `[tourset version]` (in integer format).
 
 For example, if we update the tourset in v60 and decide to show all update users the tour, we set `browser.onboarding.tourset-version`  as `3`.
+
+## Icon states
+
+Onboarding module has two states for its overlay icon: `default` and `watermark`.
+By default, it shows `default` state.
+When either tours or notifications are all completed, the icon changes to the `watermark` state.
+The icon state is stored in `browser.onboarding.state`.
+When `tourset-version` is updated, or when we detect the `tour-type` is changed to `update`, icon state will be changed back to the `default` state.
--- a/browser/extensions/onboarding/bootstrap.js
+++ b/browser/extensions/onboarding/bootstrap.js
@@ -15,16 +15,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource://gre/modules/FxAccounts.jsm");
 
 const {PREF_STRING, PREF_BOOL, PREF_INT} = Ci.nsIPrefBranch;
 
 const BROWSER_READY_NOTIFICATION = "browser-delayed-startup-finished";
 const BROWSER_SESSION_STORE_NOTIFICATION = "sessionstore-windows-restored";
 const PREF_WHITELIST = [
   ["browser.onboarding.enabled", PREF_BOOL],
+  ["browser.onboarding.state", PREF_STRING],
   ["browser.onboarding.notification.finished", PREF_BOOL],
   ["browser.onboarding.notification.prompt-count", PREF_INT],
   ["browser.onboarding.notification.last-time-of-changing-tour-sec", PREF_INT],
   ["browser.onboarding.notification.tour-ids-queue", PREF_STRING],
 ];
 
 [
   "onboarding-tour-addons",
@@ -42,17 +43,17 @@ let waitingForBrowserReady = true;
 
 /**
  * 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
+ *   - {String} name the pref name, such as `browser.onboarding.state`
  *   - {*} value the value to set
  **/
 function setPrefs(prefs) {
   prefs.forEach(pref => {
     let prefObj = PREF_WHITELIST.find(([name, ]) => name == pref.name);
     if (!prefObj) {
       return;
     }
new file mode 100644
index 0000000000000000000000000000000000000000..b8c0ffe162691995065ba0ab13189e33138dc7ec
GIT binary patch
literal 2118
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
--- a/browser/extensions/onboarding/content/onboarding.css
+++ b/browser/extensions/onboarding/content/onboarding.css
@@ -40,17 +40,17 @@
 /* Keyboard focus styling */
 #onboarding-overlay-button:-moz-focusring {
   outline: solid 2px rgba(0, 0, 0, 0.1);
   -moz-outline-radius: 5px;
   outline-offset: 5px;
   transition: outline-offset 150ms;
 }
 
-#onboarding-overlay-button-icon {
+#onboarding-overlay-button > img {
   width: 32px;
   vertical-align: top;
 }
 
 #onboarding-overlay-button::after {
   content: " ";
   border-radius: 50%;
   margin-top: -1px;
@@ -89,16 +89,26 @@
   margin-top: -10px;
   box-shadow: -2px 0 5px 0 rgba(74, 74, 79, 0.25);
 }
 
 #onboarding-overlay-button:dir(rtl)::after {
   box-shadow: 2px 0 5px 0 rgba(74, 74, 79, 0.25);
 }
 
+#onboarding-overlay-button-watermark-icon,
+#onboarding-overlay-button.onboarding-watermark:not(:hover)::after,
+#onboarding-overlay-button.onboarding-watermark:not(:hover) > #onboarding-overlay-button-icon {
+  display: none;
+}
+
+#onboarding-overlay-button.onboarding-watermark:not(:hover) > #onboarding-overlay-button-watermark-icon {
+  display: block;
+}
+
 #onboarding-overlay-dialog,
 .onboarding-hidden,
 #onboarding-tour-sync-page[data-login-state=logged-in] .show-on-logged-out,
 #onboarding-tour-sync-page[data-login-state=logged-out] .show-on-logged-in {
   display: none;
 }
 
 .onboarding-close-btn {
--- a/browser/extensions/onboarding/content/onboarding.js
+++ b/browser/extensions/onboarding/content/onboarding.js
@@ -18,16 +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 ICON_STATE_WATERMARK = "watermark";
+const ICON_STATE_DEFAULT = "default";
 
 /**
  * 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",
@@ -414,16 +416,18 @@ 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();
+    this._onIconStateChange(Services.prefs.getStringPref("browser.onboarding.state", ICON_STATE_DEFAULT));
+
     // Doing tour notification takes some effort. Let's do it on idle.
     this._window.requestIdleCallback(() => this._initNotification());
   }
 
   _getTourIDList() {
     let tours = Services.prefs.getStringPref(`browser.onboarding.${this._tourType}tour`, "");
     return tours.split(",").filter(tourId => tourId !== "").map(tourId => tourId.trim());
   }
@@ -447,27 +451,41 @@ class Onboarding {
   }
 
   _initPrefObserver() {
     if (this._prefsObserved) {
       return;
     }
 
     this._prefsObserved = new Map();
+    this._prefsObserved.set("browser.onboarding.state", () => {
+      this._onIconStateChange(Services.prefs.getStringPref("browser.onboarding.state", ICON_STATE_DEFAULT));
+    });
     this._tours.forEach(tour => {
       let tourId = tour.id;
       this._prefsObserved.set(`browser.onboarding.tour.${tourId}.completed`, () => {
         this.markTourCompletionState(tourId);
+        this._checkWatermarkByTours();
       });
     });
     for (let [name, callback] of this._prefsObserved) {
       Services.prefs.addObserver(name, callback);
     }
   }
 
+  _checkWatermarkByTours() {
+    let tourDone = this._tours.every(tour => this.isTourCompleted(tour.id));
+    if (tourDone) {
+      sendMessageToChrome("set-prefs", [{
+        name: "browser.onboarding.state",
+        value: ICON_STATE_WATERMARK
+      }]);
+    }
+  }
+
   _clearPrefObserver() {
     if (this._prefsObserved) {
       for (let [name, callback] of this._prefsObserved) {
         Services.prefs.removeObserver(name, callback);
       }
       this._prefsObserved = null;
     }
   }
@@ -650,16 +668,28 @@ class Onboarding {
     this._overlay.remove();
     if (this._notificationBar) {
       this._notificationBar.remove();
     }
     this._tourItems = this._tourPages =
     this._overlayIcon = this._overlay = this._notificationBar = null;
   }
 
+  _onIconStateChange(state) {
+    switch (state) {
+      case ICON_STATE_DEFAULT:
+        this._overlayIcon.classList.remove("onboarding-watermark");
+        break;
+      case ICON_STATE_WATERMARK:
+        this._overlayIcon.classList.add("onboarding-watermark");
+        break;
+    }
+    return true;
+  }
+
   showOverlay() {
     if (this._tourItems.length == 0) {
       // Lazy loading until first toggle.
       this._loadTours(this._tours);
     }
 
     this.hideNotification();
     this.toggleModal(this._overlay.classList.toggle("onboarding-opened"));
@@ -918,16 +948,20 @@ class Onboarding {
       sendMessageToChrome("set-prefs", [
         {
           name: "browser.onboarding.notification.finished",
           value: true
         },
         {
           name: "browser.onboarding.notification.tour-ids-queue",
           value: ""
+        },
+        {
+          name: "browser.onboarding.state",
+          value: ICON_STATE_WATERMARK
         }
       ]);
       return;
     }
     let targetTourId = queue[0];
     let targetTour = this._tours.find(tour => tour.id == targetTourId);
 
     // Show the target tour notification
@@ -1001,16 +1035,20 @@ class Onboarding {
   }
 
   skipTour() {
     this.setToursCompleted(this._tours.map(tour => tour.id));
     sendMessageToChrome("set-prefs", [
       {
         name: "browser.onboarding.notification.finished",
         value: true
+      },
+      {
+        name: "browser.onboarding.state",
+        value: ICON_STATE_WATERMARK
       }
     ]);
   }
 
   _renderOverlay() {
     let div = this._window.document.createElement("div");
     div.id = "onboarding-overlay";
     // We use `innerHTML` for more friendly reading.
@@ -1045,21 +1083,26 @@ class Onboarding {
     let button = this._window.document.createElement("button");
     let tooltipStringId = this._tourType === "new" ?
       "onboarding.overlay-icon-tooltip2" : "onboarding.overlay-icon-tooltip-updated2";
     let tooltip = this._bundle.formatStringFromName(tooltipStringId, [BRAND_SHORT_NAME], 1);
     button.setAttribute("aria-label", tooltip);
     button.id = "onboarding-overlay-button";
     button.setAttribute("aria-haspopup", true);
     button.setAttribute("aria-controls", `${ONBOARDING_DIALOG_ID}`);
-    let img = this._window.document.createElement("img");
-    img.id = "onboarding-overlay-button-icon";
-    img.setAttribute("role", "presentation");
-    img.src = "chrome://branding/content/icon64.png";
-    button.appendChild(img);
+    let defaultImg = this._window.document.createElement("img");
+    defaultImg.id = "onboarding-overlay-button-icon";
+    defaultImg.setAttribute("role", "presentation");
+    defaultImg.src = "chrome://branding/content/icon64.png";
+    button.appendChild(defaultImg);
+    let watermarkImg = this._window.document.createElement("img");
+    watermarkImg.id = "onboarding-overlay-button-watermark-icon";
+    watermarkImg.setAttribute("role", "presentation");
+    watermarkImg.src = "resource://onboarding/img/watermark64.png";
+    button.appendChild(watermarkImg);
     return button;
   }
 
   _loadTours(tours) {
     let itemsFrag = this._window.document.createDocumentFragment();
     let pagesFrag = this._window.document.createDocumentFragment();
     for (let tour of tours) {
       // Create tour navigation items dynamically
--- a/browser/extensions/onboarding/test/browser/browser_onboarding_notification.js
+++ b/browser/extensions/onboarding/test/browser/browser_onboarding_notification.js
@@ -8,24 +8,28 @@ requestLongerTimeout(3);
 add_task(async function test_show_tour_notifications_in_order() {
   resetOnboardingDefaultState();
   Preferences.set("browser.onboarding.notification.max-prompt-count-per-tour", 1);
   skipMuteNotificationOnFirstSession();
 
   let tourIds = TOUR_IDs;
   let tab = null;
   let targetTourId = null;
-  let expectedPrefUpdate = null;
+  let expectedPrefUpdates = null;
   await loopTourNotificationQueueOnceInOrder();
   await loopTourNotificationQueueOnceInOrder();
 
-  expectedPrefUpdate = promisePrefUpdated("browser.onboarding.notification.finished", true);
+  expectedPrefUpdates = Promise.all([
+    promisePrefUpdated("browser.onboarding.notification.finished", true),
+    promisePrefUpdated("browser.onboarding.state", ICON_STATE_WATERMARK)
+  ]);
   await reloadTab(tab);
   await promiseOnboardingOverlayLoaded(tab.linkedBrowser);
-  await expectedPrefUpdate;
+  await expectedPrefUpdates;
+  await assertWatermarkIconDisplayed(tab.linkedBrowser);
   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) {
         await reloadTab(tab);
--- a/browser/extensions/onboarding/test/browser/browser_onboarding_notification_4.js
+++ b/browser/extensions/onboarding/test/browser/browser_onboarding_notification_4.js
@@ -9,20 +9,25 @@ add_task(async function test_remove_all_
   resetOnboardingDefaultState();
   skipMuteNotificationOnFirstSession();
 
   let tourIds = TOUR_IDs;
   let tab = null;
   let targetTourId = null;
   await closeTourNotificationsOneByOne();
 
-  let expectedPrefUpdate = promisePrefUpdated("browser.onboarding.notification.finished", true);
+  let expectedPrefUpdates = [
+    promisePrefUpdated("browser.onboarding.notification.finished", true),
+    promisePrefUpdated("browser.onboarding.state", ICON_STATE_WATERMARK)
+  ];
   await reloadTab(tab);
   await promiseOnboardingOverlayLoaded(tab.linkedBrowser);
-  await expectedPrefUpdate;
+  await Promise.all(expectedPrefUpdates);
+  await assertWatermarkIconDisplayed(tab.linkedBrowser);
+
   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) {
         await reloadTab(tab);
@@ -43,20 +48,25 @@ add_task(async function test_remove_all_
   resetOnboardingDefaultState();
   skipMuteNotificationOnFirstSession();
 
   let tourIds = TOUR_IDs;
   let tab = null;
   let targetTourId = null;
   await clickTourNotificationActionButtonsOneByOne();
 
-  let expectedPrefUpdate = promisePrefUpdated("browser.onboarding.notification.finished", true);
+  let expectedPrefUpdates = [
+    promisePrefUpdated("browser.onboarding.notification.finished", true),
+    promisePrefUpdated("browser.onboarding.state", ICON_STATE_WATERMARK)
+  ];
   await reloadTab(tab);
   await promiseOnboardingOverlayLoaded(tab.linkedBrowser);
-  await expectedPrefUpdate;
+  await Promise.all(expectedPrefUpdates);
+  await assertWatermarkIconDisplayed(tab.linkedBrowser);
+
   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) {
         await reloadTab(tab);
--- a/browser/extensions/onboarding/test/browser/browser_onboarding_notification_5.js
+++ b/browser/extensions/onboarding/test/browser/browser_onboarding_notification_5.js
@@ -8,14 +8,18 @@ add_task(async function test_finish_tour
   skipMuteNotificationOnFirstSession();
 
   let tab = await openTab(ABOUT_NEWTAB_URL);
   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);
+  let expectedPrefUpdates = Promise.all([
+    promisePrefUpdated("browser.onboarding.notification.finished", true),
+    promisePrefUpdated("browser.onboarding.state", ICON_STATE_WATERMARK)
+  ]);
   await reloadTab(tab);
   await promiseOnboardingOverlayLoaded(tab.linkedBrowser);
-  await expectedPrefUpdate;
+  await expectedPrefUpdates;
+  await assertWatermarkIconDisplayed(tab.linkedBrowser);
   await BrowserTestUtils.removeTab(tab);
 });
--- a/browser/extensions/onboarding/test/browser/browser_onboarding_skip_tour.js
+++ b/browser/extensions/onboarding/test/browser/browser_onboarding_skip_tour.js
@@ -3,24 +3,26 @@
 
  "use strict";
 
 add_task(async function test_skip_onboarding_tours() {
   resetOnboardingDefaultState();
 
   let tourIds = TOUR_IDs;
   let expectedPrefUpdates = [
-    promisePrefUpdated("browser.onboarding.notification.finished", true)
+    promisePrefUpdated("browser.onboarding.notification.finished", true),
+    promisePrefUpdated("browser.onboarding.state", ICON_STATE_WATERMARK)
   ];
   tourIds.forEach((id, idx) => expectedPrefUpdates.push(promisePrefUpdated(`browser.onboarding.tour.${id}.completed`, true)));
 
   let tab = await openTab(ABOUT_NEWTAB_URL);
   await promiseOnboardingOverlayLoaded(tab.linkedBrowser);
   await BrowserTestUtils.synthesizeMouseAtCenter("#onboarding-overlay-button", {}, tab.linkedBrowser);
   await promiseOnboardingOverlayOpened(tab.linkedBrowser);
 
   let overlayClosedPromise = promiseOnboardingOverlayClosed(tab.linkedBrowser);
   await BrowserTestUtils.synthesizeMouseAtCenter("#onboarding-skip-tour-button", {}, tab.linkedBrowser);
   await overlayClosedPromise;
   await Promise.all(expectedPrefUpdates);
+  await assertWatermarkIconDisplayed(tab.linkedBrowser);
 
   await BrowserTestUtils.removeTab(tab);
 });
--- a/browser/extensions/onboarding/test/browser/browser_onboarding_tours.js
+++ b/browser/extensions/onboarding/test/browser/browser_onboarding_tours.js
@@ -83,8 +83,33 @@ add_task(async function test_click_actio
     let tab = tabs[i];
     await assertOverlaySemantics(tab.linkedBrowser);
     for (let id of tourIds) {
       await assertTourCompleted(id, id == completedTourId, tab.linkedBrowser);
     }
     await BrowserTestUtils.removeTab(tab);
   }
 });
+
+add_task(async function test_set_watermark_after_all_tour_completed() {
+  resetOnboardingDefaultState();
+
+  await SpecialPowers.pushPrefEnv({set: [
+    ["browser.onboarding.tour-type", "new"]
+  ]});
+
+  let tabs = [];
+  for (let url of URLs) {
+    let tab = await openTab(url);
+    await promiseOnboardingOverlayLoaded(tab.linkedBrowser);
+    await BrowserTestUtils.synthesizeMouseAtCenter("#onboarding-overlay-button", {}, tab.linkedBrowser);
+    await promiseOnboardingOverlayOpened(tab.linkedBrowser);
+    tabs.push(tab);
+  }
+  let expectedPrefUpdate = promisePrefUpdated("browser.onboarding.state", ICON_STATE_WATERMARK);
+  TOUR_IDs.forEach(id => Preferences.set(`browser.onboarding.tour.${id}.completed`, true));
+  await expectedPrefUpdate;
+
+  for (let tab of tabs) {
+    await assertWatermarkIconDisplayed(tab.linkedBrowser);
+    await BrowserTestUtils.removeTab(tab);
+  }
+});
--- a/browser/extensions/onboarding/test/browser/head.js
+++ b/browser/extensions/onboarding/test/browser/head.js
@@ -17,23 +17,26 @@ const TOUR_IDs = [
 const UPDATE_TOUR_IDs = [
   "onboarding-tour-performance",
   "onboarding-tour-library",
   "onboarding-tour-screenshots",
   "onboarding-tour-singlesearch",
   "onboarding-tour-customize",
   "onboarding-tour-sync",
 ];
+const ICON_STATE_WATERMARK = "watermark";
+const ICON_STATE_DEFAULT = "default";
 
 registerCleanupFunction(resetOnboardingDefaultState);
 
 function resetOnboardingDefaultState() {
   // All the prefs should be reset to the default states
   // and no need to revert back so we don't use `SpecialPowers.pushPrefEnv` here.
   Preferences.set("browser.onboarding.enabled", true);
+  Preferences.set("browser.onboarding.state", ICON_STATE_DEFAULT);
   Preferences.set("browser.onboarding.notification.finished", false);
   Preferences.set("browser.onboarding.notification.mute-duration-on-first-session-ms", 300000);
   Preferences.set("browser.onboarding.notification.max-life-time-per-tour-ms", 432000000);
   Preferences.set("browser.onboarding.notification.max-life-time-all-tours-ms", 1209600000);
   Preferences.set("browser.onboarding.notification.max-prompt-count-per-tour", 8);
   Preferences.reset("browser.onboarding.notification.last-time-of-changing-tour-sec");
   Preferences.reset("browser.onboarding.notification.prompt-count");
   Preferences.reset("browser.onboarding.notification.tour-ids-queue");
@@ -270,8 +273,15 @@ function assertModalDialog(browser, args
         is(overlayButton, doc.activeElement,
           "Focus should be set on overlay button");
       }
       ok(!overlayButton.dataset.keyboardFocus,
         "Overlay button focus state should be cleared");
     }
   });
 }
+
+function assertWatermarkIconDisplayed(browser) {
+  return ContentTask.spawn(browser, {}, function() {
+    let overlayButton = content.document.getElementById("onboarding-overlay-button");
+    ok(overlayButton.classList.contains("onboarding-watermark"), "Should display the watermark onboarding icon");
+  });
+}