Bug 1138818 - Part 1 - Onboarding UI without the tile images. r=adw
☠☠ backed out by 9c709ae78ec4 ☠ ☠
authorMarina Samuel <msamuel@mozilla.com>
Fri, 08 May 2015 12:38:54 -0700
changeset 274519 5b45e43dec405b2d8ea561e523011c4fcd4c695d
parent 274518 ff746c53035c410f63563d0795b798881e76994a
child 274520 cb5842311d437dfda46ab9f20bc0e046d2f5a14b
push id863
push userraliiev@mozilla.com
push dateMon, 03 Aug 2015 13:22:43 +0000
treeherdermozilla-release@f6321b14228d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersadw
bugs1138818
milestone40.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1138818 - Part 1 - Onboarding UI without the tile images. r=adw
browser/app/profile/firefox.js
browser/base/content/newtab/intro.js
browser/base/content/newtab/newTab.css
browser/base/content/newtab/newTab.xul
browser/base/content/test/newtab/browser_newtab_intro.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1626,16 +1626,19 @@ pref("browser.panorama.animate_zoom", tr
 // Defines the url to be used for new tabs.
 pref("browser.newtab.url", "about:newtab");
 // Activates preloading of the new tab url.
 pref("browser.newtab.preload", true);
 
 // Remembers if the about:newtab intro has been shown
 pref("browser.newtabpage.introShown", false);
 
+// Remembers if the about:newtab update intro has been shown
+pref("browser.newtabpage.updateIntroShown", false);
+
 // Toggles the content of 'about:newtab'. Shows the grid when enabled.
 pref("browser.newtabpage.enabled", true);
 
 // number of rows of newtab grid
 pref("browser.newtabpage.rows", 3);
 
 // number of columns of newtab grid
 pref("browser.newtabpage.columns", 5);
--- a/browser/base/content/newtab/intro.js
+++ b/browser/base/content/newtab/intro.js
@@ -1,45 +1,231 @@
 #ifdef 0
 /* 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/. */
 #endif
 
 const PREF_INTRO_SHOWN = "browser.newtabpage.introShown";
+const PREF_UPDATE_INTRO_SHOWN = "browser.newtabpage.updateIntroShown";
+
+// These consts indicate the type of intro/onboarding we show.
+const WELCOME = "welcome";
+const UPDATE = "update";
+
+// The maximum paragraph ID listed for 'newtab.intro.paragraph'
+// strings in newTab.properties
+const MAX_PARAGRAPH_ID = 9;
+
+const NUM_INTRO_PAGES = 3;
 
 let gIntro = {
+  _enUSStrings: {
+    "newtab.intro.paragraph2": "In order to provide this service, Mozilla collects and uses certain analytics information relating to your use of the tiles in accordance with our %1$S.",
+    "newtab.intro.paragraph4": "You can turn off this feature by clicking the gear (%1$S) button and selecting 'Show blank page' in the %2$S menu.",
+    "newtab.intro.paragraph5": "New Tab will show the sites you visit most frequently, along with sites we think might be of interest to you. To get started, you'll see several sites from Mozilla.",
+    "newtab.intro.paragraph6": "You can %1$S or %2$S any site by using the controls available on rollover.",
+    "newtab.intro.paragraph7": "Some of the sites you will see may be suggested by Mozilla and may be sponsored by a Mozilla partner. We'll always indicate which sites are sponsored.",
+    "newtab.intro.paragraph8": "Firefox will only show sites that most closely match your interests on the Web. %1$S",
+    "newtab.intro.paragraph9": "Now when you open New Tab, you'll also see sites we think might be interesting to you.",
+    "newtab.intro.controls": "New Tab Controls",
+    "newtab.learn.link2": "More about New Tab",
+    "newtab.privacy.link2": "About your privacy",
+    "newtab.intro.remove": "remove",
+    "newtab.intro.pin": "pin",
+    "newtab.intro.header.welcome": "Welcome to New Tab on %1$S",
+    "newtab.intro.header.update": "New Tab got an update!",
+    "newtab.intro.skip": "Skip this",
+    "newtab.intro.continue": "Continue tour",
+    "newtab.intro.back": "Back",
+    "newtab.intro.next": "Next",
+    "newtab.intro.gotit": "Got it!",
+    "newtab.intro.firefox": "Firefox!"
+  },
+
   _nodeIDSuffixes: [
     "panel",
     "what",
+    "mask",
+    "modal",
+    "numerical-progress",
+    "text",
+    "buttons",
+    "header",
+    "footer"
   ],
 
+  /**
+   * The paragraphs & buttons to show on each page in the intros.
+   *
+   * _introPages.welcome and _introPages.update contain an array of
+   * indices of paragraphs to be used to lookup text in _paragraphs
+   * for each page of the intro.
+   *
+   * Similarly, _introPages.buttons is used to lookup text for buttons
+   * on each page of the intro.
+   */
+  _introPages: {
+    "welcome": [[0,1],[2,3],[4,5]],
+    "update": [[6,5],[4,3],[0,1]],
+    "buttons": [["skip", "continue"],["back", "next"],["back", "gotit"]]
+  },
+
+  _paragraphs: [],
+
   _nodes: {},
 
   init: function() {
     for (let idSuffix of this._nodeIDSuffixes) {
       this._nodes[idSuffix] = document.getElementById("newtab-intro-" + idSuffix);
     }
 
-    this._nodes.panel.addEventListener("popupshowing", e => this._setUpPanel());
-    this._nodes.panel.addEventListener("popuphidden", e => this._hidePanel());
-    this._nodes.what.addEventListener("click", e => this.showPanel());
+    if (DirectoryLinksProvider.locale != "en-US") {
+      this._nodes.what.style.display = "block";
+      this._nodes.panel.addEventListener("popupshowing", e => this._setUpPanel());
+      this._nodes.panel.addEventListener("popuphidden", e => this._hidePanel());
+      this._nodes.what.addEventListener("click", e => this.showPanel());
+    }
+  },
+
+  _goToPage: function(pageNum) {
+    this._currPage = pageNum;
+
+    this._nodes["numerical-progress"].innerHTML = `${this._bold(pageNum + 1)} / ${NUM_INTRO_PAGES}`;
+    this._nodes["numerical-progress"].setAttribute("page", pageNum);
+
+    // Set the paragraphs
+    let paragraphNodes = this._nodes.text.getElementsByTagName("p");
+    let paragraphIDs = this._introPages[this._onboardingType][pageNum];
+    paragraphIDs.forEach((arg, index) => {
+      paragraphNodes[index].innerHTML = this._paragraphs[arg];
+    });
+
+    // Set the buttons
+    let buttonNodes = this._nodes.buttons.getElementsByTagName("input");
+    let buttonIDs = this._introPages.buttons[pageNum];
+    buttonIDs.forEach((arg, index) => {
+      buttonNodes[index].setAttribute("value", this._newTabString("intro." + arg));
+    });
+  },
+
+  _bold: function(str) {
+    return `<strong>${str}</strong>`
+  },
+
+  _link: function(url, text) {
+    return `<a href="${url}" target="_blank">${text}</a>`;
+  },
+
+  _span: function(text, className) {
+    return `<span class="${className}">${text}</span>`;
+  },
+
+  _exitIntro: function() {
+    this._nodes.mask.style.opacity = 0;
+    this._nodes.mask.addEventListener("transitionend", () => {
+      this._nodes.mask.style.display = "none";
+    });
+  },
+
+  _back: function() {
+    if (this._currPage == 0) {
+      // We're on the first page so 'back' means exit.
+      this._exitIntro();
+      return;
+    }
+    this._goToPage(this._currPage - 1);
+  },
+
+  _next: function() {
+    if (this._currPage == (NUM_INTRO_PAGES - 1)) {
+      // We're on the last page so 'next' means exit.
+      this._exitIntro();
+      return;
+    }
+    this._goToPage(this._currPage + 1);
+  },
+
+  _generateParagraphs: function() {
+    let customizeIcon = '<input type="button" class="newtab-control newtab-customize"/>';
+
+    let substringMappings = {
+      "2": [this._link(TILES_PRIVACY_LINK, newTabString("privacy.link"))],
+      "4": [customizeIcon, this._bold(this._newTabString("intro.controls"))],
+      "6": [this._bold(this._newTabString("intro.remove")), this._bold(this._newTabString("intro.pin"))],
+      "7": [this._link(TILES_INTRO_LINK, newTabString("learn.link"))],
+      "8": [this._link(TILES_INTRO_LINK, newTabString("learn.link"))]
+    }
+
+    for (let i = 1; i <= MAX_PARAGRAPH_ID; i++) {
+      try {
+        this._paragraphs.push(this._newTabString("intro.paragraph" + i, substringMappings[i]))
+      } catch (ex) {
+        // Paragraph with this ID doesn't exist so continue
+      }
+    }
+  },
+
+  _newTabString: function(str, substrArr) {
+    let regExp = /%[0-9]\$S/g;
+    let paragraph = this._enUSStrings["newtab." + str];
+
+    if (!paragraph) {
+      throw new Error("Paragraph doesn't exist");
+    }
+
+    let matches;
+    while ((matches = regExp.exec(paragraph)) !== null) {
+      let match = matches[0];
+      let index = match.charAt(1); // Get the digit in the regExp.
+      paragraph = paragraph.replace(match, substrArr[index - 1]);
+    }
+    return paragraph;
   },
 
   showIfNecessary: function() {
     if (!Services.prefs.getBoolPref(PREF_INTRO_SHOWN)) {
-      Services.prefs.setBoolPref(PREF_INTRO_SHOWN, true);
+      this._onboardingType = WELCOME;
+      this.showPanel();
+    } else if (!Services.prefs.getBoolPref(PREF_UPDATE_INTRO_SHOWN) && DirectoryLinksProvider.locale == "en-US") {
+      this._onboardingType = UPDATE;
       this.showPanel();
     }
+    Services.prefs.setBoolPref(PREF_INTRO_SHOWN, true);
+    Services.prefs.setBoolPref(PREF_UPDATE_INTRO_SHOWN, true);
   },
 
   showPanel: function() {
-    // Point the panel at the 'what' link
-    this._nodes.panel.hidden = false;
-    this._nodes.panel.openPopup(this._nodes.what);
+    if (DirectoryLinksProvider.locale != "en-US") {
+      // Point the panel at the 'what' link
+      this._nodes.panel.hidden = false;
+      this._nodes.panel.openPopup(this._nodes.what);
+      return;
+    }
+
+    this._nodes.mask.style.display = "block";
+    this._nodes.mask.style.opacity = 1;
+
+    if (!this._paragraphs.length) {
+      // It's our first time showing the panel. Do some initial setup
+      this._generateParagraphs();
+    }
+    this._goToPage(0);
+
+    // Header text
+    let boldSubstr = this._onboardingType == WELCOME ? this._span(this._newTabString("intro.firefox"), "bold") : "";
+    this._nodes.header.innerHTML = this._newTabString("intro.header." + this._onboardingType, [boldSubstr]);
+
+    // Footer links
+    let footerLinkNodes = this._nodes.footer.getElementsByTagName("li");
+    [this._link(TILES_INTRO_LINK, this._newTabString("learn.link2")),
+     this._link(TILES_PRIVACY_LINK, this._newTabString("privacy.link2")),
+    ].forEach((arg, index) => {
+      footerLinkNodes[index].innerHTML = arg;
+    });
   },
 
   _setUpPanel: function() {
     // Build the panel if necessary
     if (this._nodes.panel.childNodes.length == 1) {
       ['<a href="' + TILES_INTRO_LINK + '">' + newTabString("learn.link") + "</a>",
        '<a href="' + TILES_PRIVACY_LINK + '">' + newTabString("privacy.link") + "</a>",
        '<input type="button" class="newtab-customize"/>',
--- a/browser/base/content/newtab/newTab.css
+++ b/browser/base/content/newtab/newTab.css
@@ -50,16 +50,17 @@ input[type=button] {
   right: auto;
 }
 
 #newtab-intro-what {
   cursor: pointer;
   position: absolute;
   right: 70px;
   top: 20px;
+  display: none;
 }
 
 #newtab-intro-what:-moz-locale-dir(rtl) {
   left: 70px;
   right: auto;
 }
 
 #newtab-scrollbox[page-disabled] #newtab-intro-what {
@@ -619,8 +620,220 @@ input[type=button] {
   border-bottom: medium none !important;
   padding: 20px 0px 10px 40px;
 }
 
 .searchSuggestionTable {
   font: message-box;
   font-size: 16px;
 }
+
+/**
+ * Onboarding styling
+ */
+
+ #newtab-intro-mask {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: #424F5A;
+  z-index:102;
+  background-color: rgba(66,79,90,0.95);
+  transition: opacity .5s linear;
+  overflow: auto;
+  display: none;
+}
+
+#newtab-intro-modal {
+  font-family: "Helvetica";
+  width: 700px;
+  height: 500px;
+  position: fixed;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  margin: auto;
+  background: linear-gradient(#FFFFFF, #F9F9F9);
+  box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.7);
+  border-radius: 8px 8px 0px 0px;
+}
+
+#newtab-intro-header {
+  font-size: 28px;
+  color: #737980;
+  text-align: center;
+  top: 50px;
+  position: relative;
+  border-bottom: 2px solid #E0DFE0;
+  padding-bottom: 10px;
+  width: 600px;
+  display: block;
+  margin: 0px auto;
+  font-weight: 100;
+}
+
+#newtab-intro-header .bold {
+  font-weight: 500;
+  color: #343F48;
+}
+
+#newtab-intro-footer {
+  width: 100%;
+  height: 55px;
+  margin: 0px auto;
+  display: block;
+  position: absolute;
+  bottom: 0px;
+  background-color: white;
+  box-shadow: 0 -1px 4px -1px #EBEBEB;
+  text-align: center;
+  vertical-align: middle;
+  line-height: 55px;
+}
+
+#newtab-intro-footer > ul {
+  list-style-type: none;
+  margin: 0px;
+  padding: 0px;
+}
+
+#newtab-intro-footer > ul > li {
+  display: inline;
+  padding-left: 10px;
+  padding-right: 10px;
+}
+
+#newtab-intro-footer > ul > li > a {
+  text-decoration: none;
+  color: #4A90E2;
+}
+
+#newtab-intro-footer > ul > li > a:visited {
+  color: #171F26;
+}
+
+#newtab-intro-footer > ul > :first-child {
+  border-right: solid 1px #C1C1C1;
+}
+
+#newtab-intro-body {
+  height: 300px;
+  position: relative;
+  display: block;
+  top: 50px;
+  margin: 25px 50px 30px;
+}
+
+#newtab-intro-content > * {
+  display: inline-block;
+}
+
+#newtab-intro-content {
+  height: 210px;
+  position: relative;
+}
+
+#newtab-intro-buttons {
+  height: 90px;
+  text-align: center;
+  vertical-align: middle;
+  line-height: 90px;
+}
+
+#newtab-intro-tile {
+  width: 290px;
+  height: 100%;
+}
+
+#newtab-intro-text {
+  height: 100%;
+  width: 270px;
+  right: 0px;
+  position: absolute;
+  font-size: 14px;
+  line-height: 20px;
+}
+
+#newtab-intro-text > p {
+  margin: 0 0 1em 0;
+}
+
+#newtab-intro-text .newtab-control {
+  background-size: 18px auto;
+  height: 18px;
+  width: 18px;
+  vertical-align: middle;
+  opacity: 1;
+  position: inherit;
+}
+
+#newtab-intro-buttons > input {
+  width: 150px;
+  height: 50px;
+  margin: 0px 5px;
+  vertical-align: bottom;
+  border-radius: 2px;
+  border: solid 1px #2C72E2;
+  background-color: #FFFFFF;
+  color: #4A90E2;
+  -moz-user-focus: normal;
+}
+
+#newtab-intro-buttons > input[default] {
+  background-color: #4A90E2;
+  color: #FFFFFF;
+}
+
+#newtab-intro-buttons > input:hover {
+  background-color: #2C72E2;
+  color: #FFFFFF;
+}
+
+#newtab-intro-progress {
+  position: absolute;
+  width: 100%;
+}
+
+#newtab-intro-numerical-progress {
+  text-align: center;
+  top: 15px;
+  position: relative;
+  font-size: 12px;
+  color: #424F5A;
+}
+
+#newtab-intro-graphical-progress {
+  text-align: left;
+  border-radius: 1.5px;
+  overflow: hidden;
+  position: relative;
+  margin: 10px auto 0px;
+  height: 3px;
+  top: 8px;
+  width: 35px;
+  background-color: #DCDCDC;
+}
+
+#indicator {
+  position: absolute;
+  top: 0px;
+  left: 0px;
+  display: inline-block;
+  width: 0%;
+  height: 4px;
+  background: none repeat scroll 0% 0% #FF9500;
+  transition: width 0.3s ease-in-out 0s;
+}
+
+#newtab-intro-numerical-progress[page="0"] + #newtab-intro-graphical-progress > #indicator {
+  width: 33%;
+}
+
+#newtab-intro-numerical-progress[page="1"] + #newtab-intro-graphical-progress > #indicator {
+  width: 66%;
+}
+
+#newtab-intro-numerical-progress[page="2"] + #newtab-intro-graphical-progress > #indicator {
+  width: 100%;
+}
--- a/browser/base/content/newtab/newTab.xul
+++ b/browser/base/content/newtab/newTab.xul
@@ -53,16 +53,45 @@
     <xul:hbox id="newtab-customize-blank" class="newtab-customize-panel-item selectable">
       <xul:label>&newtab.customize.blank2;</xul:label>
     </xul:hbox>
     <xul:hbox id="newtab-customize-learn" class="newtab-customize-panel-item">
       <xul:label>&newtab.customize.cog.learn;</xul:label>
     </xul:hbox>
   </xul:panel>
 
+  <div id="newtab-intro-mask">
+    <div id="newtab-intro-modal">
+      <div id="newtab-intro-progress">
+        <div id="newtab-intro-numerical-progress"/>
+        <div id="newtab-intro-graphical-progress">
+          <span id="indicator"/>
+        </div>
+      </div>
+      <div id="newtab-intro-header"/>
+      <div id="newtab-intro-body">
+        <div id="newtab-intro-content">
+          <div id="newtab-intro-image"/>
+          <div id="newtab-intro-text">
+            <p/><p/>
+          </div>
+        </div>
+        <div id="newtab-intro-buttons">
+          <input type="button" onclick="gIntro._back()"/>
+          <input type="button" default="true" onclick="gIntro._next()"/>
+        </div>
+      </div>
+      <div id="newtab-intro-footer">
+        <ul>
+          <li/><li/>
+        </ul>
+      </div>
+    </div>
+  </div>
+
   <div id="newtab-scrollbox">
 
     <div id="newtab-vertical-margin">
 
       <div id="newtab-margin-top"/>
 
       <div id="newtab-margin-undo-container">
         <div id="newtab-undo-container" undo-disabled="true">
--- a/browser/base/content/test/newtab/browser_newtab_intro.js
+++ b/browser/base/content/test/newtab/browser_newtab_intro.js
@@ -1,51 +1,94 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const INTRO_PREF = "browser.newtabpage.introShown";
+const UPDATE_INTRO_PREF = "browser.newtabpage.updateIntroShown";
 const PRELOAD_PREF = "browser.newtab.preload";
 
 function runTests() {
   let origIntro = Services.prefs.getBoolPref(INTRO_PREF);
+  let origUpdateIntro = Services.prefs.getBoolPref(UPDATE_INTRO_PREF);
   let origPreload = Services.prefs.getBoolPref(PRELOAD_PREF);
   registerCleanupFunction(_ => {
     Services.prefs.setBoolPref(INTRO_PREF, origIntro);
+    Services.prefs.setBoolPref(INTRO_PREF, origUpdateIntro);
     Services.prefs.setBoolPref(PRELOAD_PREF, origPreload);
   });
 
   // Test with preload false
   Services.prefs.setBoolPref(INTRO_PREF, false);
+  Services.prefs.setBoolPref(UPDATE_INTRO_PREF, false);
   Services.prefs.setBoolPref(PRELOAD_PREF, false);
 
-  let panel;
-  function maybeWaitForPanel() {
-    // If already open, no need to wait
-    if (panel.state == "open") {
-      executeSoon(TestRunner.next);
-      return;
-    }
-
-    // We're expecting the panel to open, so wait for it
-    panel.addEventListener("popupshown", TestRunner.next);
-    isnot(panel.state, "open", "intro panel can be slow to show");
-  }
+  let intro;
+  yield addNewTabPageTab();
+  intro = getContentDocument().getElementById("newtab-intro-mask");
+  is(intro.style.opacity, 1, "intro automatically shown on first opening");
+  is(getContentDocument().getElementById("newtab-intro-header").innerHTML,
+     'Welcome to New Tab on <span xmlns="http://www.w3.org/1999/xhtml" class="bold">Firefox!</span>', "we show the first-run intro.");
+  is(Services.prefs.getBoolPref(INTRO_PREF), true, "newtab remembers that the intro was shown");
+  is(Services.prefs.getBoolPref(UPDATE_INTRO_PREF), true, "newtab avoids showing update if intro was shown");
 
   yield addNewTabPageTab();
-  panel = getContentDocument().getElementById("newtab-intro-panel");
-  yield maybeWaitForPanel();
-  is(panel.state, "open", "intro automatically shown on first opening");
-  is(Services.prefs.getBoolPref(INTRO_PREF), true, "newtab remembers that the intro was shown");
-
-  yield addNewTabPageTab();
-  panel = getContentDocument().getElementById("newtab-intro-panel");
-  is(panel.state, "closed", "intro not shown on second opening");
+  intro = getContentDocument().getElementById("newtab-intro-mask");
+  is(intro.style.opacity, 0, "intro not shown on second opening");
 
   // Test with preload true
   Services.prefs.setBoolPref(INTRO_PREF, false);
   Services.prefs.setBoolPref(PRELOAD_PREF, true);
 
   yield addNewTabPageTab();
-  panel = getContentDocument().getElementById("newtab-intro-panel");
-  yield maybeWaitForPanel();
-  is(panel.state, "open", "intro automatically shown on preloaded opening");
+  intro = getContentDocument().getElementById("newtab-intro-mask");
+  is(intro.style.opacity, 1, "intro automatically shown on preloaded opening");
+  is(getContentDocument().getElementById("newtab-intro-header").innerHTML,
+     'Welcome to New Tab on <span xmlns="http://www.w3.org/1999/xhtml" class="bold">Firefox!</span>', "we show the first-run intro.");
   is(Services.prefs.getBoolPref(INTRO_PREF), true, "newtab remembers that the intro was shown");
+  is(Services.prefs.getBoolPref(UPDATE_INTRO_PREF), true, "newtab avoids showing update if intro was shown");
+
+  // Test with first run true but update false
+  Services.prefs.setBoolPref(UPDATE_INTRO_PREF, false);
+
+  yield addNewTabPageTab();
+  intro = getContentDocument().getElementById("newtab-intro-mask");
+  is(intro.style.opacity, 1, "intro automatically shown on preloaded opening");
+  is(getContentDocument().getElementById("newtab-intro-header").innerHTML,
+     "New Tab got an update!", "we show the update intro.");
+  is(Services.prefs.getBoolPref(INTRO_PREF), true, "INTRO_PREF stays true");
+  is(Services.prefs.getBoolPref(UPDATE_INTRO_PREF), true, "newtab remembers that the update intro was show");
+
+  // Test clicking the 'next' and 'back' buttons.
+  let buttons = getContentDocument().getElementById("newtab-intro-buttons").getElementsByTagName("input");
+  let progress = getContentDocument().getElementById("newtab-intro-numerical-progress");
+  let back = buttons[0];
+  let next = buttons[1];
+
+  synthesizeNativeMouseLDown(next);
+  synthesizeNativeMouseLUp(next).then(() => {
+    is(progress.getAttribute("page"), 1, "we get to the 2nd page");
+    is(intro.style.opacity, 1, "intro visible");
+  }, Cu.reportError);
+
+  synthesizeNativeMouseLDown(next);
+  synthesizeNativeMouseLUp(next).then(() => {
+    is(progress.getAttribute("page"), 2, "we get to the 3rd page");
+    is(intro.style.opacity, 1, "intro visible");
+  }, Cu.reportError);
+
+  synthesizeNativeMouseLDown(back);
+  synthesizeNativeMouseLUp(back).then(() => {
+    is(progress.getAttribute("page"), 1, "go back to 2nd page");
+    is(intro.style.opacity, 1, "intro visible");
+  }, Cu.reportError);
+
+  synthesizeNativeMouseLDown(back);
+  synthesizeNativeMouseLUp(back).then(() => {
+    is(progress.getAttribute("page"), 0, "go back to 1st page");
+    is(intro.style.opacity, 1, "intro visible");
+  }, Cu.reportError);
+
+  synthesizeNativeMouseLDown(back);
+  synthesizeNativeMouseLUp(back).then(() => {
+    is(progress.getAttribute("page"), 0, "another back will 'skip tutorial'");
+    is(intro.style.opacity, 0, "intro exited");
+  }, Cu.reportError);
 }