merge fx-team to mozilla-central
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Fri, 18 Oct 2013 11:40:53 +0200
changeset 165064 4e7d1e2c93a639296f661b77898d049040cdcd80
parent 165038 b082b86b2cacb56a7e27c789afbfd764fa447234 (current diff)
parent 165063 84544a4d0e4d3792b888efde6b1fe82a3bb9c8c3 (diff)
child 165065 43b40be4049e849a86582a7943349c70c8e7abc9
child 165073 2e6e5ffe9174a8e24f8186c4eaa90a4e5bfaf4cc
child 165096 5952f0bdecd268cc0858b324bfd5756097aed39c
child 165155 976228f4aef74c718611017c3becb143b1c4490c
child 170451 0b9e36f5ad5de66315e91dd5b3a3c779086c13cb
push id3066
push userakeybl@mozilla.com
push dateMon, 09 Dec 2013 19:58:46 +0000
treeherdermozilla-beta@a31a0dce83aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone27.0a1
first release with
nightly linux32
4e7d1e2c93a6 / 27.0a1 / 20131018030206 / files
nightly linux64
4e7d1e2c93a6 / 27.0a1 / 20131018030206 / files
nightly mac
4e7d1e2c93a6 / 27.0a1 / 20131018030206 / files
nightly win32
4e7d1e2c93a6 / 27.0a1 / 20131018030206 / files
nightly win64
4e7d1e2c93a6 / 27.0a1 / 20131018030206 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge fx-team to mozilla-central
browser/app/profile/firefox.js
browser/base/content/browser.xul
browser/components/sessionstore/src/SessionStore.jsm
browser/metro/profile/metro.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -228,16 +228,22 @@ pref("extensions.dss.switchPending", fal
 pref("extensions.{972ce4c6-7e08-4474-a285-3208198ce6fd}.name", "chrome://browser/locale/browser.properties");
 pref("extensions.{972ce4c6-7e08-4474-a285-3208198ce6fd}.description", "chrome://browser/locale/browser.properties");
 
 pref("xpinstall.whitelist.add", "addons.mozilla.org");
 pref("xpinstall.whitelist.add.180", "marketplace.firefox.com");
 
 pref("lightweightThemes.update.enabled", true);
 
+// UI tour experience.
+pref("browser.uitour.enabled", true);
+pref("browser.uitour.themeOrigin", "https://addons.mozilla.org/%LOCALE%/firefox/themes/");
+pref("browser.uitour.pinnedTabUrl", "https://support.mozilla.org/%LOCALE%/kb/pinned-tabs-keep-favorite-websites-open");
+pref("browser.uitour.whitelist.add.260", "www.mozilla.org,support.mozilla.org");
+
 pref("keyword.enabled", true);
 
 pref("general.useragent.locale", "@AB_CD@");
 pref("general.skins.selectedSkin", "classic/1.0");
 
 pref("general.smoothScroll", true);
 #ifdef UNIX_BUT_NOT_MAC
 pref("general.autoScroll", false);
@@ -655,16 +661,18 @@ pref("pfs.datasource.url", "https://pfs.
 pref("plugins.hide_infobar_for_blocked_plugin", false);
 pref("plugins.hide_infobar_for_outdated_plugin", false);
 
 pref("plugins.update.url", "https://www.mozilla.org/%LOCALE%/plugincheck/");
 pref("plugins.update.notifyUser", false);
 
 pref("plugins.click_to_play", true);
 
+pref("plugins.clickToActivateInfo.url", "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/clicktoplay");
+
 // let all plugins except Flash default to click-to-play
 pref("plugin.default.state", 1);
 pref("plugin.state.flash", 2);
 
 // display door hanger if flash not installed
 pref("plugins.notifyMissingFlash", true);
 
 #ifdef XP_WIN
--- a/browser/base/content/browser-plugins.js
+++ b/browser/base/content/browser-plugins.js
@@ -699,16 +699,19 @@ var gPluginHandler = {
       let url;
       // TODO: allow the blocklist to specify a better link, bug 873093
       if (pluginInfo.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE) {
         url = Services.urlFormatter.formatURLPref("plugins.update.url");
       }
       else if (pluginInfo.blocklistState != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) {
         url = Services.blocklist.getPluginBlocklistURL(pluginInfo.pluginTag);
       }
+      else {
+        url = Services.urlFormatter.formatURLPref("plugins.clickToActivateInfo.url");
+      }
       pluginInfo.detailsLink = url;
 
       centerActions.push(pluginInfo);
     }
     centerActions.sort(function(a, b) {
       return a.pluginName.localeCompare(b.pluginName);
     });
 
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -17,16 +17,17 @@
 
 # All DTD information is stored in a separate file so that it can be shared by
 # hiddenWindow.xul.
 #include browser-doctype.inc
 
 <window id="main-window"
         xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
         xmlns:svg="http://www.w3.org/2000/svg"
+        xmlns:html="http://www.w3.org/1999/xhtml"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         onload="gBrowserInit.onLoad()" onunload="gBrowserInit.onUnload()" onclose="return WindowIsClosing();"
         title="&mainWindow.title;@PRE_RELEASE_SUFFIX@"
         title_normal="&mainWindow.title;@PRE_RELEASE_SUFFIX@"
 #ifdef XP_MACOSX
         title_privatebrowsing="&mainWindow.title;@PRE_RELEASE_SUFFIX@&mainWindow.titlemodifiermenuseparator;&mainWindow.titlePrivateBrowsingSuffix;"
         titledefault="&mainWindow.title;@PRE_RELEASE_SUFFIX@"
         titlemodifier=""
@@ -178,16 +179,32 @@
                 class="editBookmarkPanelBottomButton"
                 label="&editBookmark.done.label;"
                 default="true"
                 oncommand="StarUI.panel.hidePopup();"/>
 #endif
       </hbox>
     </panel>
 
+    <!-- UI tour experience -->
+    <panel id="UITourTooltip"
+           type="arrow"
+           hidden="true"
+           consumeoutsideclicks="false"
+           noautofocus="true"
+           align="start"
+           orient="vertical"
+           role="alert">
+      <label id="UITourTooltipTitle" flex="1"/>
+      <description id="UITourTooltipDescription" flex="1"/>
+    </panel>
+    <html:div id="UITourHighlightContainer" style="position:relative">
+      <html:div id="UITourHighlight"></html:div>
+    </html:div>
+
     <panel id="socialActivatedNotification"
            type="arrow"
            hidden="true"
            consumeoutsideclicks="true"
            align="start"
            orient="horizontal"
            role="alert">
       <image id="social-activation-icon" class="popup-notification-icon"/>
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -10,16 +10,18 @@ let Cu = Components.utils;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this,
   "LoginManagerContent", "resource://gre/modules/LoginManagerContent.jsm");
 XPCOMUtils.defineLazyModuleGetter(this,
   "InsecurePasswordUtils", "resource://gre/modules/InsecurePasswordUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "UITour",
+  "resource:///modules/UITour.jsm");
 
 // Creates a new nsIURI object.
 function makeURI(uri, originCharset, baseURI) {
   return Services.io.newURI(uri, originCharset, baseURI);
 }
 
 addMessageListener("Browser:HideSessionRestoreButton", function (message) {
   // Hide session restore button on about:home
@@ -44,16 +46,25 @@ if (Services.prefs.getBoolPref("browser.
     LoginManagerContent.onFormPassword(event);
   });
   addEventListener("DOMAutoComplete", function(event) {
     LoginManagerContent.onUsernameInput(event);
   });
   addEventListener("blur", function(event) {
     LoginManagerContent.onUsernameInput(event);
   });
+
+  addEventListener("mozUITour", function(event) {
+    if (!Services.prefs.getBoolPref("browser.uitour.enabled"))
+      return;
+
+    let handled = UITour.onPageEvent(event);
+    if (handled)
+      addEventListener("pagehide", UITour);
+  }, false, true);
 }
 
 let AboutHomeListener = {
   init: function(chromeGlobal) {
     chromeGlobal.addEventListener('AboutHomeLoad', () => this.onPageLoad(), false, true);
   },
 
   handleEvent: function(aEvent) {
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -467,16 +467,17 @@ BrowserGlue.prototype = {
     PageThumbs.init();
     NewTabUtils.init();
     BrowserNewTabPreloader.init();
     SignInToWebsiteUX.init();
     PdfJs.init();
     ShumwayUtils.init();
     webrtcUI.init();
     AboutHome.init();
+    SessionStore.init();
 
     if (Services.prefs.getBoolPref("browser.tabs.remote"))
       ContentClick.init();
 
     Services.obs.notifyObservers(null, "browser-ui-startup-complete", "");
   },
 
   _checkForOldBuildUpdates: function () {
@@ -607,17 +608,16 @@ BrowserGlue.prototype = {
     if (WINTASKBAR_CONTRACTID in Cc &&
         Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar).available) {
       let temp = {};
       Cu.import("resource:///modules/WindowsJumpLists.jsm", temp);
       temp.WinTaskbarJumpList.startup();
     }
 #endif
 
-    SessionStore.init(aWindow);
     this._trackSlowStartup();
 
     // Offer to reset a user's profile if it hasn't been used for 60 days.
     const OFFER_PROFILE_RESET_INTERVAL_MS = 60 * 24 * 60 * 60 * 1000;
     let lastUse = Services.appinfo.replacedLockTime;
     if (lastUse &&
         Date.now() - lastUse >= OFFER_PROFILE_RESET_INTERVAL_MS) {
       this._resetUnusedProfileNotification();
--- a/browser/components/sessionstore/src/SessionStore.jsm
+++ b/browser/components/sessionstore/src/SessionStore.jsm
@@ -151,18 +151,18 @@ this.SessionStore = {
   get canRestoreLastSession() {
     return SessionStoreInternal.canRestoreLastSession;
   },
 
   set canRestoreLastSession(val) {
     SessionStoreInternal.canRestoreLastSession = val;
   },
 
-  init: function ss_init(aWindow) {
-    SessionStoreInternal.init(aWindow);
+  init: function ss_init() {
+    SessionStoreInternal.init();
   },
 
   getBrowserState: function ss_getBrowserState() {
     return SessionStoreInternal.getBrowserState();
   },
 
   setBrowserState: function ss_setBrowserState(aState) {
     SessionStoreInternal.setBrowserState(aState);
@@ -363,52 +363,34 @@ let SessionStoreInternal = {
     if (val)
       return;
     this._lastSessionState = null;
   },
 
   /**
    * Initialize the sessionstore service.
    */
-  init: function (aWindow) {
+  init: function () {
     if (this._initialized) {
       throw new Error("SessionStore.init() must only be called once!");
     }
 
-    if (!aWindow) {
-      throw new Error("SessionStore.init() must be called with a valid window.");
-    }
-
     this._disabledForMultiProcess = Services.prefs.getBoolPref("browser.tabs.remote");
     if (this._disabledForMultiProcess) {
       this._deferredInitialized.resolve();
       return;
     }
 
     TelemetryTimestamps.add("sessionRestoreInitialized");
     OBSERVING.forEach(function(aTopic) {
       Services.obs.addObserver(this, aTopic, true);
     }, this);
 
     this._initPrefs();
     this._initialized = true;
-
-    // Wait until nsISessionStartup has finished reading the session data.
-    gSessionStartup.onceInitialized.then(() => {
-      // Parse session data and start restoring.
-      let initialState = this.initSession();
-
-      // Start tracking the given (initial) browser window.
-      if (!aWindow.closed) {
-        this.onLoad(aWindow, initialState);
-      }
-
-      // Let everyone know we're done.
-      this._deferredInitialized.resolve();
-    }, Cu.reportError);
   },
 
   initSession: function ssi_initSession() {
     let state;
     let ss = gSessionStartup;
 
     try {
       if (ss.doRestore() ||
@@ -484,17 +466,16 @@ let SessionStoreInternal = {
 
     // at this point, we've as good as resumed the session, so we can
     // clear the resume_session_once flag, if it's set
     if (this._loadState != STATE_QUITTING &&
         this._prefBranch.getBoolPref("sessionstore.resume_session_once"))
       this._prefBranch.setBoolPref("sessionstore.resume_session_once", false);
 
     this._performUpgradeBackup();
-    this._sessionInitialized = true;
 
     return state;
   },
 
   /**
    * If this is the first time we launc this build of Firefox,
    * backup sessionstore.js.
    */
@@ -871,17 +852,44 @@ let SessionStoreInternal = {
   /**
    * On window open
    * @param aWindow
    *        Window reference
    */
   onOpen: function ssi_onOpen(aWindow) {
     let onload = () => {
       aWindow.removeEventListener("load", onload);
-      this.onLoad(aWindow);
+
+      if (this._sessionInitialized) {
+        this.onLoad(aWindow);
+        return;
+      }
+
+      // We can't call this.onLoad since initialization
+      // hasn't completed, so we'll wait until it is done.
+      // Even if additional windows are opened and wait
+      // for initialization as well, the first opened
+      // window should execute first, and this.onLoad
+      // will be called with the initialState.
+      gSessionStartup.onceInitialized.then(() => {
+        if (aWindow.closed) {
+          return;
+        }
+
+        if (this._sessionInitialized) {
+          this.onLoad(aWindow);
+        } else {
+          let initialState = this.initSession();
+          this._sessionInitialized = true;
+          this.onLoad(aWindow, initialState);
+
+          // Let everyone know we're done.
+          this._deferredInitialized.resolve();
+        }
+      }, Cu.reportError);
     };
 
     aWindow.addEventListener("load", onload);
   },
 
   /**
    * On window close...
    * - remove event listeners from tabs
--- a/browser/devtools/netmonitor/netmonitor-view.js
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -1532,16 +1532,17 @@ NetworkDetailsView.prototype = {
       }));
     this._params = new VariablesView($("#request-params"),
       Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
         emptyText: L10N.getStr("paramsEmptyText"),
         searchPlaceholder: L10N.getStr("paramsFilterText")
       }));
     this._json = new VariablesView($("#response-content-json"),
       Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
+        onlyEnumVisible: true,
         searchPlaceholder: L10N.getStr("jsonFilterText")
       }));
     VariablesViewController.attach(this._json);
 
     this._paramsQueryString = L10N.getStr("paramsQueryString");
     this._paramsFormData = L10N.getStr("paramsFormData");
     this._paramsPostPayload = L10N.getStr("paramsPostPayload");
     this._requestHeaders = L10N.getStr("requestHeaders");
@@ -1869,17 +1870,17 @@ NetworkDetailsView.prototype = {
       // for "json" after any word boundary. This works for the standard
       // "application/json", and also for custom types like "x-bigcorp-json".
       // This should be marginally more reliable than just looking for "json".
       if (/\bjson/.test(mimeType)) {
         let jsonpRegex = /^[a-zA-Z0-9_$]+\(|\)$/g; // JSONP with callback.
         let sanitizedJSON = aString.replace(jsonpRegex, "");
         let callbackPadding = aString.match(jsonpRegex);
 
-        // Make sure this is an valid JSON object first. If so, nicely display
+        // Make sure this is a valid JSON object first. If so, nicely display
         // the parsing results in a variables view. Otherwise, simply show
         // the contents as plain text.
         try {
           var jsonObject = JSON.parse(sanitizedJSON);
         } catch (e) {
           var parsingError = e;
         }
 
--- a/browser/devtools/scratchpad/scratchpad.js
+++ b/browser/devtools/scratchpad/scratchpad.js
@@ -605,76 +605,33 @@ var Scratchpad = {
   writeAsErrorComment: function SP_writeAsErrorComment(aError)
   {
     let deferred = promise.defer();
 
     if (VariablesView.isPrimitive({ value: aError })) {
       deferred.resolve(aError);
     }
     else {
-      let reject = aReason => deferred.reject(aReason);
       let objectClient = new ObjectClient(this.debuggerClient, aError);
-
-      // Because properties on Error objects are lazily added, this roundabout
-      // way of getting all the properties is required, rather than simply
-      // using getPrototypeAndProperties. See bug 724768.
-      let names = ["message", "stack", "fileName", "lineNumber"];
-      let promises = names.map(aName => {
-        let deferred = promise.defer();
-
-        objectClient.getProperty(aName, aResponse => {
-          if (aResponse.error) {
-            deferred.reject(aResponse);
-          }
-          else {
-            deferred.resolve({
-              name: aName,
-              descriptor: aResponse.descriptor
-            });
-          }
-        });
+      objectClient.getPrototypeAndProperties(aResponse => {
+        if (aResponse.error) {
+          deferred.reject(aResponse);
+          return;
+        }
 
-        return deferred.promise;
-      });
-
-      {
-        // We also need to use getPrototypeAndProperties to retrieve any
-        // safeGetterValues in case this is a DOM error.
-        let deferred = promise.defer();
-        objectClient.getPrototypeAndProperties(aResponse => {
-          if (aResponse.error) {
-            deferred.reject(aResponse);
-          }
-          else {
-            deferred.resolve(aResponse);
-          }
-        });
-        promises.push(deferred.promise);
-      }
-
-      promise.all(promises).then(aProperties => {
-        let error = {};
-        let safeGetters;
+        let { ownProperties, safeGetterValues } = aResponse;
+        let error = Object.create(null);
 
         // Combine all the property descriptor/getter values into one object.
-        for (let property of aProperties) {
-          if (property.descriptor) {
-            error[property.name] = property.descriptor.value;
-          }
-          else if (property.safeGetterValues) {
-            safeGetters = property.safeGetterValues;
-          }
+        for (let key of Object.keys(safeGetterValues)) {
+          error[key] = safeGetterValues[key].getterValue;
         }
 
-        if (safeGetters) {
-          for (let key of Object.keys(safeGetters)) {
-            if (!error.hasOwnProperty(key)) {
-              error[key] = safeGetters[key].getterValue;
-            }
-          }
+        for (let key of Object.keys(ownProperties)) {
+          error[key] = ownProperties[key].value;
         }
 
         // Assemble the best possible stack we can given the properties we have.
         let stack;
         if (typeof error.stack == "string") {
           stack = error.stack;
         }
         else if (typeof error.fileName == "number") {
@@ -688,33 +645,33 @@ var Scratchpad = {
         }
 
         stack = stack ? "\n" + stack.replace(/\n$/, "") : "";
 
         if (typeof error.message == "string") {
           deferred.resolve(error.message + stack);
         }
         else {
-          objectClient.getDisplayString(aResult => {
-            if (aResult.error) {
-              deferred.reject(aResult);
+          objectClient.getDisplayString(aResponse => {
+            if (aResponse.error) {
+              deferred.reject(aResponse);
             }
-            else if (aResult.displayString.type == "null") {
+            else if (typeof aResponse.displayString == "string") {
+              deferred.resolve(aResponse.displayString + stack);
+            }
+            else {
               deferred.resolve(stack);
             }
-            else {
-              deferred.resolve(aResult.displayString + stack);
-            }
-          }, reject);
+          });
         }
-      }, reject);
+      });
     }
 
     return deferred.promise.then(aMessage => {
-      console.log(aMessage);
+      console.error(aMessage);
       this.writeAsComment("Exception: " + aMessage);
     });
   },
 
   // Menu Operations
 
   /**
    * Open a new Scratchpad window.
--- a/browser/devtools/shared/test/browser_css_color.js
+++ b/browser/devtools/shared/test/browser_css_color.js
@@ -38,16 +38,17 @@ function createDocument()
 
 function testColorUtils() {
   let data = getTestData();
 
   for (let {authored, name, hex, hsl, rgb} of data) {
     let color = new colorUtils.CssColor(authored);
 
     // Check all values.
+    info("Checking values for " + authored);
     is(color.name, name, "color.name === name");
     is(color.hex, hex, "color.hex === hex");
     is(color.hsl, hsl, "color.hsl === hsl");
     is(color.rgb, rgb, "color.rgb === rgb");
 
     testToString(color, name, hex, hsl, rgb);
     testColorMatch(name, hex, hsl, rgb, color.rgba);
   }
@@ -286,19 +287,23 @@ function getTestData() {
     {authored: "tomato", name: "tomato", hex: "#FF6347", hsl: "hsl(9.13, 100%, 64%)", rgb: "rgb(255, 99, 71)"},
     {authored: "turquoise", name: "turquoise", hex: "#40E0D0", hsl: "hsl(174, 72%, 56%)", rgb: "rgb(64, 224, 208)"},
     {authored: "violet", name: "violet", hex: "#EE82EE", hsl: "hsl(300, 76%, 72%)", rgb: "rgb(238, 130, 238)"},
     {authored: "wheat", name: "wheat", hex: "#F5DEB3", hsl: "hsl(39.091, 77%, 83%)", rgb: "rgb(245, 222, 179)"},
     {authored: "white", name: "white", hex: "#FFF", hsl: "hsl(0, 0%, 100%)", rgb: "rgb(255, 255, 255)"},
     {authored: "whitesmoke", name: "whitesmoke", hex: "#F5F5F5", hsl: "hsl(0, 0%, 96%)", rgb: "rgb(245, 245, 245)"},
     {authored: "yellow", name: "yellow", hex: "#FF0", hsl: "hsl(60, 100%, 50%)", rgb: "rgb(255, 255, 0)"},
     {authored: "yellowgreen", name: "yellowgreen", hex: "#9ACD32", hsl: "hsl(79.742, 61%, 50%)", rgb: "rgb(154, 205, 50)"},
-    {authored: "transparent", name: "transparent", hex: "transparent", hsl: "transparent", rgb: "transparent"},
-    {authored: "rgba(0, 0, 0, 0)", name: "transparent", hex: "transparent", hsl: "transparent", rgb: "transparent"},
-    {authored: "hsla(0, 0%, 0%, 0)", name: "transparent", hex: "transparent", hsl: "transparent", rgb: "transparent"},
+    {authored: "rgba(0, 0, 0, 0)", name: "rgba(0, 0, 0, 0)", hex: "rgba(0, 0, 0, 0)", hsl: "hsla(0, 0%, 0%, 0)", rgb: "rgba(0, 0, 0, 0)"},
+    {authored: "hsla(0, 0%, 0%, 0)", name: "rgba(0, 0, 0, 0)", hex: "rgba(0, 0, 0, 0)", hsl: "hsla(0, 0%, 0%, 0)", rgb: "rgba(0, 0, 0, 0)"},
     {authored: "rgba(50, 60, 70, 0.5)", name: "rgba(50, 60, 70, 0.5)", hex: "rgba(50, 60, 70, 0.5)", hsl: "hsla(210, 17%, 24%, 0.5)", rgb: "rgba(50, 60, 70, 0.5)"},
     {authored: "rgba(0, 0, 0, 0.3)", name: "rgba(0, 0, 0, 0.3)", hex: "rgba(0, 0, 0, 0.3)", hsl: "hsla(0, 0%, 0%, 0.3)", rgb: "rgba(0, 0, 0, 0.3)"},
     {authored: "rgba(255, 255, 255, 0.6)", name: "rgba(255, 255, 255, 0.6)", hex: "rgba(255, 255, 255, 0.6)", hsl: "hsla(0, 0%, 100%, 0.6)", rgb: "rgba(255, 255, 255, 0.6)"},
     {authored: "rgba(127, 89, 45, 1)", name: "#7F592D", hex: "#7F592D", hsl: "hsl(32.195, 48%, 34%)", rgb: "rgb(127, 89, 45)"},
     {authored: "hsla(19.304, 56%, 40%, 1)", name: "#9F512C", hex: "#9F512C", hsl: "hsl(19.304, 57%, 40%)", rgb: "rgb(159, 81, 44)"},
-    {authored: "invalidColor", name: "", hex: "", hsl: "", rgb: ""}
+    {authored: "currentcolor", name: "currentcolor", hex: "currentcolor", hsl: "currentcolor", rgb: "currentcolor"},
+    {authored: "inherit", name: "inherit", hex: "inherit", hsl: "inherit", rgb: "inherit"},
+    {authored: "initial", name: "initial", hex: "initial", hsl: "initial", rgb: "initial"},
+    {authored: "invalidColor", name: "", hex: "", hsl: "", rgb: ""},
+    {authored: "transparent", name: "transparent", hex: "transparent", hsl: "transparent", rgb: "transparent"},
+    {authored: "unset", name: "unset", hex: "unset", hsl: "unset", rgb: "unset"}
   ];
 }
--- a/browser/devtools/shared/widgets/VariablesViewController.jsm
+++ b/browser/devtools/shared/widgets/VariablesViewController.jsm
@@ -456,17 +456,17 @@ VariablesViewController.prototype = {
    *         - variable: the created Variable.
    *         - expanded: the Promise that resolves when the variable expands.
    */
   setSingleVariable: function(aOptions) {
     let scope = this.view.addScope(aOptions.label);
     scope.expanded = true;
     scope.locked = true;
 
-    let variable = scope.addItem();
+    let variable = scope.addItem("", { enumerable: true });
     let expanded;
 
     if (aOptions.objectActor) {
       expanded = this.expand(variable, aOptions.objectActor);
     } else if (aOptions.rawObject) {
       variable.populate(aOptions.rawObject, { expanded: true });
       expanded = promise.resolve();
     }
new file mode 100644
--- /dev/null
+++ b/browser/modules/UITour.jsm
@@ -0,0 +1,426 @@
+// 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/.
+
+this.EXPORTED_SYMBOLS = ["UITour"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
+  "resource://gre/modules/LightweightThemeManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils",
+  "resource://gre/modules/PermissionsUtils.jsm");
+
+
+const UITOUR_PERMISSION   = "uitour";
+const PREF_PERM_BRANCH    = "browser.uitour.";
+
+
+this.UITour = {
+  originTabs: new WeakMap(),
+  pinnedTabs: new WeakMap(),
+  urlbarCapture: new WeakMap(),
+
+  highlightEffects: ["wobble", "zoom", "color"],
+  targets: new Map([
+    ["backforward", "#unified-back-forward-button"],
+    ["appmenu", "#appmenu-button"],
+    ["home", "#home-button"],
+    ["urlbar", "#urlbar"],
+    ["bookmarks", "#bookmarks-menu-button"],
+    ["search", "#searchbar"],
+    ["searchprovider", function UITour_target_searchprovider(aDocument) {
+      let searchbar = aDocument.getElementById("searchbar");
+      return aDocument.getAnonymousElementByAttribute(searchbar,
+                                                     "anonid",
+                                                     "searchbar-engine-button");
+    }],
+  ]),
+
+  onPageEvent: function(aEvent) {
+    let contentDocument = null;
+    if (aEvent.target instanceof Ci.nsIDOMHTMLDocument)
+      contentDocument = aEvent.target;
+    else if (aEvent.target instanceof Ci.nsIDOMHTMLElement)
+      contentDocument = aEvent.target.ownerDocument;
+    else
+      return false;
+
+    // Ignore events if they're not from a trusted origin.
+    if (!this.ensureTrustedOrigin(contentDocument))
+      return false;
+
+    if (typeof aEvent.detail != "object")
+      return false;
+
+    let action = aEvent.detail.action;
+    if (typeof action != "string" || !action)
+      return false;
+
+    let data = aEvent.detail.data;
+    if (typeof data != "object")
+      return false;
+
+    let window = this.getChromeWindow(contentDocument);
+
+    switch (action) {
+      case "showHighlight": {
+        let target = this.getTarget(window, data.target);
+        if (!target)
+          return false;
+        this.showHighlight(target);
+        break;
+      }
+
+      case "hideHighlight": {
+        this.hideHighlight(window);
+        break;
+      }
+
+      case "showInfo": {
+        let target = this.getTarget(window, data.target, true);
+        if (!target)
+          return false;
+        this.showInfo(target, data.title, data.text);
+        break;
+      }
+
+      case "hideInfo": {
+        this.hideInfo(window);
+        break;
+      }
+
+      case "previewTheme": {
+        this.previewTheme(data.theme);
+        break;
+      }
+
+      case "resetTheme": {
+        this.resetTheme();
+        break;
+      }
+
+      case "addPinnedTab": {
+        this.ensurePinnedTab(window, true);
+        break;
+      }
+
+      case "removePinnedTab": {
+        this.removePinnedTab(window);
+        break;
+      }
+
+      case "showMenu": {
+        this.showMenu(window, data.name);
+        break;
+      }
+
+      case "startUrlbarCapture": {
+        if (typeof data.text != "string" || !data.text ||
+            typeof data.url != "string" || !data.url) {
+          return false;
+        }
+
+        let uri = null;
+        try {
+          uri = Services.io.newURI(data.url, null, null);
+        } catch (e) {
+          return false;
+        }
+
+        let secman = Services.scriptSecurityManager;
+        let principal = contentDocument.nodePrincipal;
+        let flags = secman.DISALLOW_INHERIT_PRINCIPAL;
+        try {
+          secman.checkLoadURIWithPrincipal(principal, uri, flags);
+        } catch (e) {
+          return false;
+        }
+
+        this.startUrlbarCapture(window, data.text, data.url);
+        break;
+      }
+
+      case "endUrlbarCapture": {
+        this.endUrlbarCapture(window);
+        break;
+      }
+    }
+
+    let tab = window.gBrowser._getTabForContentWindow(contentDocument.defaultView);
+    if (!this.originTabs.has(window))
+      this.originTabs.set(window, new Set());
+    this.originTabs.get(window).add(tab);
+
+    tab.addEventListener("TabClose", this);
+    window.gBrowser.tabContainer.addEventListener("TabSelect", this);
+    window.addEventListener("SSWindowClosing", this);
+
+    return true;
+  },
+
+  handleEvent: function(aEvent) {
+    switch (aEvent.type) {
+      case "pagehide": {
+        let window = this.getChromeWindow(aEvent.target);
+        this.teardownTour(window);
+        break;
+      }
+
+      case "TabClose": {
+        let window = aEvent.target.ownerDocument.defaultView;
+        this.teardownTour(window);
+        break;
+      }
+
+      case "TabSelect": {
+        let window = aEvent.target.ownerDocument.defaultView;
+        let pinnedTab = this.pinnedTabs.get(window);
+        if (pinnedTab && pinnedTab.tab == window.gBrowser.selectedTab)
+          break;
+        let originTabs = this.originTabs.get(window);
+        if (originTabs && originTabs.has(window.gBrowser.selectedTab))
+          break;
+
+        this.teardownTour(window);
+        break;
+      }
+
+      case "SSWindowClosing": {
+        let window = aEvent.target;
+        this.teardownTour(window, true);
+        break;
+      }
+
+      case "input": {
+        if (aEvent.target.id == "urlbar") {
+          let window = aEvent.target.ownerDocument.defaultView;
+          this.handleUrlbarInput(window);
+        }
+        break;
+      }
+    }
+  },
+
+  teardownTour: function(aWindow, aWindowClosing = false) {
+    aWindow.gBrowser.tabContainer.removeEventListener("TabSelect", this);
+    aWindow.removeEventListener("SSWindowClosing", this);
+
+    let originTabs = this.originTabs.get(aWindow);
+    if (originTabs) {
+      for (let tab of originTabs)
+        tab.removeEventListener("TabClose", this);
+    }
+    this.originTabs.delete(aWindow);
+
+    if (!aWindowClosing) {
+      this.hideHighlight(aWindow);
+      this.hideInfo(aWindow);
+    }
+
+    this.endUrlbarCapture(aWindow);
+    this.removePinnedTab(aWindow);
+    this.resetTheme();
+  },
+
+  getChromeWindow: function(aContentDocument) {
+    return aContentDocument.defaultView
+                           .window
+                           .QueryInterface(Ci.nsIInterfaceRequestor)
+                           .getInterface(Ci.nsIWebNavigation)
+                           .QueryInterface(Ci.nsIDocShellTreeItem)
+                           .rootTreeItem
+                           .QueryInterface(Ci.nsIInterfaceRequestor)
+                           .getInterface(Ci.nsIDOMWindow)
+                           .wrappedJSObject;
+  },
+
+  importPermissions: function() {
+    try {
+      PermissionsUtils.importFromPrefs(PREF_PERM_BRANCH, UITOUR_PERMISSION);
+    } catch (e) {
+      Cu.reportError(e);
+    }
+  },
+
+  ensureTrustedOrigin: function(aDocument) {
+    if (aDocument.defaultView.top != aDocument.defaultView)
+      return false;
+
+    let uri = aDocument.documentURIObject;
+
+    if (uri.schemeIs("chrome"))
+      return true;
+
+    if (!uri.schemeIs("https"))
+      return false;
+
+    this.importPermissions();
+    let permission = Services.perms.testPermission(uri, UITOUR_PERMISSION);
+    return permission == Services.perms.ALLOW_ACTION;
+  },
+
+  getTarget: function(aWindow, aTargetName, aSticky = false) {
+    if (typeof aTargetName != "string" || !aTargetName)
+      return null;
+
+    if (aTargetName == "pinnedtab")
+      return this.ensurePinnedTab(aWindow, aSticky);
+
+    let targetQuery = this.targets.get(aTargetName);
+    if (!targetQuery)
+      return null;
+
+    if (typeof targetQuery == "function")
+      return targetQuery(aWindow.document);
+
+    return aWindow.document.querySelector(targetQuery);
+  },
+
+  previewTheme: function(aTheme) {
+    let origin = Services.prefs.getCharPref("browser.uitour.themeOrigin");
+    let data = LightweightThemeManager.parseTheme(aTheme, origin);
+    if (data)
+      LightweightThemeManager.previewTheme(data);
+  },
+
+  resetTheme: function() {
+    LightweightThemeManager.resetPreview();
+  },
+
+  ensurePinnedTab: function(aWindow, aSticky = false) {
+    let tabInfo = this.pinnedTabs.get(aWindow);
+
+    if (tabInfo) {
+      tabInfo.sticky = tabInfo.sticky || aSticky;
+    } else {
+      let url = Services.urlFormatter.formatURLPref("browser.uitour.pinnedTabUrl");
+
+      let tab = aWindow.gBrowser.addTab(url);
+      aWindow.gBrowser.pinTab(tab);
+      tab.addEventListener("TabClose", () => {
+        this.pinnedTabs.delete(aWindow);
+      });
+
+      tabInfo = {
+        tab: tab,
+        sticky: aSticky
+      };
+      this.pinnedTabs.set(aWindow, tabInfo);
+    }
+
+    return tabInfo.tab;
+  },
+
+  removePinnedTab: function(aWindow) {
+    let tabInfo = this.pinnedTabs.get(aWindow);
+    if (tabInfo)
+      aWindow.gBrowser.removeTab(tabInfo.tab);
+  },
+
+  showHighlight: function(aTarget) {
+    let highlighter = aTarget.ownerDocument.getElementById("UITourHighlight");
+
+    let randomEffect = Math.floor(Math.random() * this.highlightEffects.length);
+    if (randomEffect == this.highlightEffects.length)
+      randomEffect--; // On the order of 1 in 2^62 chance of this happening.
+    highlighter.setAttribute("active", this.highlightEffects[randomEffect]);
+
+    let targetRect = aTarget.getBoundingClientRect();
+
+    highlighter.style.height = targetRect.height + "px";
+    highlighter.style.width = targetRect.width + "px";
+
+    let highlighterRect = highlighter.getBoundingClientRect();
+
+    let top = targetRect.top + (targetRect.height / 2) - (highlighterRect.height / 2);
+    highlighter.style.top = top + "px";
+    let left = targetRect.left + (targetRect.width / 2) - (highlighterRect.width / 2);
+    highlighter.style.left = left + "px";
+  },
+
+  hideHighlight: function(aWindow) {
+    let tabData = this.pinnedTabs.get(aWindow);
+    if (tabData && !tabData.sticky)
+      this.removePinnedTab(aWindow);
+
+    let highlighter = aWindow.document.getElementById("UITourHighlight");
+    highlighter.removeAttribute("active");
+  },
+
+  showInfo: function(aAnchor, aTitle, aDescription) {
+    aAnchor.focus();
+
+    let document = aAnchor.ownerDocument;
+    let tooltip = document.getElementById("UITourTooltip");
+    let tooltipTitle = document.getElementById("UITourTooltipTitle");
+    let tooltipDesc = document.getElementById("UITourTooltipDescription");
+
+    tooltip.hidePopup();
+
+    tooltipTitle.textContent = aTitle;
+    tooltipDesc.textContent = aDescription;
+
+    let alignment = "bottomcenter topright";
+    let anchorRect = aAnchor.getBoundingClientRect();
+
+    tooltip.hidden = false;
+    tooltip.openPopup(aAnchor, alignment);
+  },
+
+  hideInfo: function(aWindow) {
+    let tooltip = aWindow.document.getElementById("UITourTooltip");
+    tooltip.hidePopup();
+  },
+
+  showMenu: function(aWindow, aMenuName) {
+    function openMenuButton(aId) {
+      let menuBtn = aWindow.document.getElementById(aId);
+      if (menuBtn && menuBtn.boxObject)
+        menuBtn.boxObject.QueryInterface(Ci.nsIMenuBoxObject).openMenu(true);
+    }
+
+    if (aMenuName == "appmenu")
+      openMenuButton("appmenu-button");
+    else if (aMenuName == "bookmarks")
+      openMenuButton("bookmarks-menu-button");
+  },
+
+  startUrlbarCapture: function(aWindow, aExpectedText, aUrl) {
+    let urlbar = aWindow.document.getElementById("urlbar");
+    this.urlbarCapture.set(aWindow, {
+      expected: aExpectedText.toLocaleLowerCase(),
+      url: aUrl
+    });
+    urlbar.addEventListener("input", this);
+  },
+
+  endUrlbarCapture: function(aWindow) {
+    let urlbar = aWindow.document.getElementById("urlbar");
+    urlbar.removeEventListener("input", this);
+    this.urlbarCapture.delete(aWindow);
+  },
+
+  handleUrlbarInput: function(aWindow) {
+    if (!this.urlbarCapture.has(aWindow))
+      return;
+
+    let urlbar = aWindow.document.getElementById("urlbar");
+
+    let {expected, url} = this.urlbarCapture.get(aWindow);
+
+    if (urlbar.value.toLocaleLowerCase().localeCompare(expected) != 0)
+      return;
+
+    urlbar.handleRevert();
+
+    let tab = aWindow.gBrowser.addTab(url, {
+      owner: aWindow.gBrowser.selectedTab,
+      relatedToCurrent: true
+    });
+    aWindow.gBrowser.selectedTab = tab;
+  },
+};
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -10,16 +10,17 @@ EXTRA_JS_MODULES += [
     'BrowserNewTabPreloader.jsm',
     'ContentClick.jsm',
     'NetworkPrioritizer.jsm',
     'SharedFrame.jsm',
     'SignInToWebsite.jsm',
     'SitePermissions.jsm',
     'Social.jsm',
     'TabCrashReporter.jsm',
+    'UITour.jsm',
     'offlineAppCache.jsm',
     'openLocationLastURL.jsm',
     'webappsUI.jsm',
     'webrtcUI.jsm',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
     EXTRA_JS_MODULES += [
--- a/browser/modules/test/browser.ini
+++ b/browser/modules/test/browser.ini
@@ -1,3 +1,5 @@
 [DEFAULT]
 
 [browser_NetworkPrioritizer.js]
+[browser_UITour.js]
+support-files = uitour.*
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser_UITour.js
@@ -0,0 +1,212 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let gTestTab;
+let gContentAPI;
+
+Components.utils.import("resource:///modules/UITour.jsm");
+
+function is_hidden(element) {
+  var style = element.ownerDocument.defaultView.getComputedStyle(element, "");
+  if (style.display == "none")
+    return true;
+  if (style.visibility != "visible")
+    return true;
+
+  // Hiding a parent element will hide all its children
+  if (element.parentNode != element.ownerDocument)
+    return is_hidden(element.parentNode);
+
+  return false;
+}
+
+function is_element_visible(element, msg) {
+  isnot(element, null, "Element should not be null, when checking visibility");
+  ok(!is_hidden(element), msg);
+}
+
+function is_element_hidden(element, msg) {
+  isnot(element, null, "Element should not be null, when checking visibility");
+  ok(is_hidden(element), msg);
+}
+
+function loadTestPage(callback, untrustedHost = false) {
+   if (gTestTab)
+    gBrowser.removeTab(gTestTab);
+
+  let url = getRootDirectory(gTestPath) + "uitour.html";
+  if (untrustedHost)
+    url = url.replace("chrome://mochitests/content/", "http://example.com/");
+
+  gTestTab = gBrowser.addTab(url);
+  gBrowser.selectedTab = gTestTab;
+
+  gTestTab.linkedBrowser.addEventListener("load", function onLoad() {
+    gTestTab.linkedBrowser.removeEventListener("load", onLoad);
+
+    let contentWindow = Components.utils.waiveXrays(gTestTab.linkedBrowser.contentDocument.defaultView);
+    gContentAPI = contentWindow.Mozilla.UITour;
+
+    waitForFocus(callback, contentWindow);
+  }, true);
+}
+
+function test() {
+  Services.prefs.setBoolPref("browser.uitour.enabled", true);
+
+  waitForExplicitFinish();
+
+  registerCleanupFunction(function() {
+    delete window.UITour;
+    delete window.gContentAPI;
+    if (gTestTab)
+      gBrowser.removeTab(gTestTab);
+    delete window.gTestTab;
+    Services.prefs.clearUserPref("browser.uitour.enabled", true);
+  });
+
+  function done() {
+    if (gTestTab)
+      gBrowser.removeTab(gTestTab);
+    gTestTab = null;
+
+    let highlight = document.getElementById("UITourHighlight");
+    is_element_hidden(highlight, "Highlight should be hidden after UITour tab is closed");
+
+    let popup = document.getElementById("UITourTooltip");
+    isnot(["hidding","closed"].indexOf(popup.state), -1, "Popup should be closed/hidding after UITour tab is closed");
+
+    is(UITour.pinnedTabs.get(window), null, "Any pinned tab should be closed after UITour tab is closed");
+
+    executeSoon(nextTest);
+  }
+
+  function nextTest() {
+    if (tests.length == 0) {
+      finish();
+      return;
+    }
+    let test = tests.shift();
+
+    loadTestPage(function() {
+      test(done);
+    });
+  }
+  nextTest();
+}
+
+let tests = [
+  function test_disabled(done) {
+    Services.prefs.setBoolPref("browser.uitour.enabled", false);
+
+    let highlight = document.getElementById("UITourHighlight");
+    is_element_hidden(highlight, "Highlight should initially be hidden");
+
+    gContentAPI.showHighlight("urlbar");
+    is_element_hidden(highlight, "Highlight should not be shown when feature is disabled");
+
+    Services.prefs.setBoolPref("browser.uitour.enabled", true);
+    done();
+  },
+  function test_untrusted_host(done) {
+    loadTestPage(function() {
+      let highlight = document.getElementById("UITourHighlight");
+      is_element_hidden(highlight, "Highlight should initially be hidden");
+
+      gContentAPI.showHighlight("urlbar");
+      is_element_hidden(highlight, "Highlight should not be shown on a untrusted domain");
+
+      done();
+    }, true);
+  },
+  function test_highlight(done) {
+    let highlight = document.getElementById("UITourHighlight");
+    is_element_hidden(highlight, "Highlight should initially be hidden");
+
+    gContentAPI.showHighlight("urlbar");
+    is_element_visible(highlight, "Highlight should be shown after showHighlight()");
+
+    gContentAPI.hideHighlight();
+    is_element_hidden(highlight, "Highlight should be hidden after hideHighlight()");
+
+    gContentAPI.showHighlight("urlbar");
+    is_element_visible(highlight, "Highlight should be shown after showHighlight()");
+    gContentAPI.showHighlight("backforward");
+    is_element_visible(highlight, "Highlight should be shown after showHighlight()");
+
+    done();
+  },
+  function test_info_1(done) {
+    let popup = document.getElementById("UITourTooltip");
+    let title = document.getElementById("UITourTooltipTitle");
+    let desc = document.getElementById("UITourTooltipDescription");
+    popup.addEventListener("popupshown", function onPopupShown() {
+      popup.removeEventListener("popupshown", onPopupShown);
+      is(popup.popupBoxObject.anchorNode, document.getElementById("urlbar"), "Popup should be anchored to the urlbar");
+      is(title.textContent, "test title", "Popup should have correct title");
+      is(desc.textContent, "test text", "Popup should have correct description text");
+
+      popup.addEventListener("popuphidden", function onPopupHidden() {
+        popup.removeEventListener("popuphidden", onPopupHidden);
+
+        popup.addEventListener("popupshown", function onPopupShown() {
+          popup.removeEventListener("popupshown", onPopupShown);
+          done();
+        });
+
+        gContentAPI.showInfo("urlbar", "test title", "test text");
+
+      });
+      gContentAPI.hideInfo();
+    });
+
+    gContentAPI.showInfo("urlbar", "test title", "test text");
+  },
+  function test_info_2(done) {
+    let popup = document.getElementById("UITourTooltip");
+    let title = document.getElementById("UITourTooltipTitle");
+    let desc = document.getElementById("UITourTooltipDescription");
+    popup.addEventListener("popupshown", function onPopupShown() {
+      popup.removeEventListener("popupshown", onPopupShown);
+      is(popup.popupBoxObject.anchorNode, document.getElementById("urlbar"), "Popup should be anchored to the urlbar");
+      is(title.textContent, "urlbar title", "Popup should have correct title");
+      is(desc.textContent, "urlbar text", "Popup should have correct description text");
+
+      gContentAPI.showInfo("search", "search title", "search text");
+      executeSoon(function() {
+        is(popup.popupBoxObject.anchorNode, document.getElementById("searchbar"), "Popup should be anchored to the searchbar");
+        is(title.textContent, "search title", "Popup should have correct title");
+        is(desc.textContent, "search text", "Popup should have correct description text");
+
+        done();
+      });
+    });
+
+    gContentAPI.showInfo("urlbar", "urlbar title", "urlbar text");
+  },
+  function test_pinnedTab(done) {
+    is(UITour.pinnedTabs.get(window), null, "Should not already have a pinned tab");
+
+    gContentAPI.addPinnedTab();
+    let tabInfo = UITour.pinnedTabs.get(window);
+    isnot(tabInfo, null, "Should have recorded data about a pinned tab after addPinnedTab()");
+    isnot(tabInfo.tab, null, "Should have added a pinned tab after addPinnedTab()");
+    is(tabInfo.tab.pinned, true, "Tab should be marked as pinned");
+
+    let tab = tabInfo.tab;
+
+    gContentAPI.removePinnedTab();
+    isnot(gBrowser.tabs[0], tab, "First tab should not be the pinned tab");
+    let tabInfo = UITour.pinnedTabs.get(window);
+    is(tabInfo, null, "Should not have any data about the removed pinned tab after removePinnedTab()");
+
+    gContentAPI.addPinnedTab();
+    gContentAPI.addPinnedTab();
+    gContentAPI.addPinnedTab();
+    is(gBrowser.tabs[1].pinned, false, "After multiple calls of addPinnedTab, should still only have one pinned tab");
+
+    done();
+  },
+];
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/uitour.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8" />
+    <title>UITour test</title>
+    <script type="application/javascript" src="uitour.js">
+    </script>
+  </head>
+  <body>
+    <h1>UITour tests</h1>
+    <p>Because Firefox is...</p>
+    <p>Never gonna let you down</p>
+    <p>Never gonna give you up</p>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/uitour.js
@@ -0,0 +1,115 @@
+/* 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/. */
+
+// Copied from the proposed JS library for Bedrock (ie, www.mozilla.org).
+
+// create namespace
+if (typeof Mozilla == 'undefined') {
+	var Mozilla = {};
+}
+
+(function($) {
+  'use strict';
+
+	// create namespace
+	if (typeof Mozilla.UITour == 'undefined') {
+		Mozilla.UITour = {};
+	}
+
+	var themeIntervalId = null;
+	function _stopCyclingThemes() {
+		if (themeIntervalId) {
+			clearInterval(themeIntervalId);
+			themeIntervalId = null;
+		}
+	}
+
+
+	function _sendEvent(action, data) {
+		var event = new CustomEvent('mozUITour', {
+			bubbles: true,
+			detail: {
+				action: action,
+				data: data || {}
+			}
+		});
+		console.log("Sending mozUITour event: ", event);
+		document.dispatchEvent(event);
+	}
+
+	Mozilla.UITour.DEFAULT_THEME_CYCLE_DELAY = 10 * 1000;
+
+	Mozilla.UITour.showHighlight = function(target) {
+		_sendEvent('showHighlight', {
+			target: target
+		});
+	};
+
+	Mozilla.UITour.hideHighlight = function() {
+		_sendEvent('hideHighlight');
+	};
+
+	Mozilla.UITour.showInfo = function(target, title, text) {
+		_sendEvent('showInfo', {
+			target: target,
+			title: title,
+			text: text
+		});
+	};
+
+	Mozilla.UITour.hideInfo = function() {
+		_sendEvent('hideInfo');
+	};
+
+	Mozilla.UITour.previewTheme = function(theme) {
+		_stopCyclingThemes();
+
+		_sendEvent('previewTheme', {
+			theme: JSON.stringify(theme)
+		});
+	};
+
+	Mozilla.UITour.resetTheme = function() {
+		_stopCyclingThemes();
+
+		_sendEvent('resetTheme');
+	};
+
+	Mozilla.UITour.cycleThemes = function(themes, delay, callback) {
+		_stopCyclingThemes();
+
+		if (!delay) {
+			delay = Mozilla.UITour.DEFAULT_THEME_CYCLE_DELAY;
+		}
+
+		function nextTheme() {
+			var theme = themes.shift();
+			themes.push(theme);
+
+			_sendEvent('previewTheme', {
+				theme: JSON.stringify(theme),
+				state: true
+			});
+
+			callback(theme);
+		}
+
+		themeIntervalId = setInterval(nextTheme, delay);
+		nextTheme();
+	};
+
+	Mozilla.UITour.addPinnedTab = function() {
+		_sendEvent('addPinnedTab');
+	};
+
+	Mozilla.UITour.removePinnedTab = function() {
+		_sendEvent('removePinnedTab');
+	};
+
+	Mozilla.UITour.showMenu = function(name) {
+		_sendEvent('showMenu', {
+			name: name
+		});
+	};
+})();
\ No newline at end of file
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -2108,16 +2108,100 @@ toolbar[mode="text"] toolbarbutton.chevr
   color: #FDF3DE;
   min-width: 16px;
   text-shadow: none;
   background-image: linear-gradient(#B4211B, #8A1915);
   border-radius: 1px;
   -moz-margin-end: 2px;
 }
 
+/* UI Tour */
+
+@keyframes uitour-wobble {
+  from {
+    transform: rotate(0deg) translateX(2px) rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg) translateX(2px) rotate(-360deg);
+  }
+}
+
+@keyframes uitour-zoom {
+  from {
+    transform: scale(0.9);
+  }
+  50% {
+    transform: scale(1.1);
+  }
+  to {
+    transform: scale(0.9);
+  }
+}
+
+@keyframes uitour-color {
+  from {
+    border-color: #5B9CD9;
+  }
+  50% {
+    border-color: #FF0000;
+  }
+  to {
+    border-color: #5B9CD9;
+  }
+}
+
+html|div#UITourHighlight {
+  display: none;
+  position: absolute;
+  min-height: 32px;
+  min-width: 32px;
+  display: none;
+  border: 2px #5B9CD9 solid;
+  box-shadow: 0 0 2px #5B9CD9, inset 0 0 1px #5B9CD9;
+  border-radius: 20px;
+  z-index: 10000000000;
+}
+
+html|div#UITourHighlight[active] {
+  display: block;
+  animation-delay: 2s;
+  animation-timing-function: linear;
+  animation-iteration-count: infinite;
+  animation-fill-mode: forwards;
+}
+
+html|div#UITourHighlight[active="wobble"] {
+  animation-name: uitour-wobble;
+  animation-duration: 1s;
+}
+html|div#UITourHighlight[active="zoom"] {
+  animation-name: uitour-zoom;
+  animation-duration: 1s;
+}
+html|div#UITourHighlight[active="color"] {
+  animation-name: uitour-color;
+  animation-duration: 2s;
+}
+
+#UITourTooltip {
+  max-width: 20em;
+}
+
+#UITourTooltipTitle {
+  font-weight: bold;
+  font-size: 130%;
+  margin: 0 0 5px 0;
+}
+
+#UITourTooltipDescription {
+  max-width: 20em;
+}
+
+/* Social toolbar item */
+
 #social-provider-button {
   -moz-image-region: rect(0, 16px, 16px, 0);
   list-style-image: url(chrome://browser/skin/social/services-16.png);
 }
 
 #social-provider-button > .toolbarbutton-menu-dropmarker {
   display: none;
 }
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -3723,16 +3723,98 @@ toolbarbutton.chevron > .toolbarbutton-m
 #developer-toolbar-toolbox-button[error-count]:before {
   color: #FDF3DE;
   min-width: 16px;
   text-shadow: none;
   background-image: linear-gradient(#B4211B, #8A1915);
   border-radius: 1px;
 }
 
+/* UI Tour */
+
+@keyframes uitour-wobble {
+  from {
+    transform: rotate(0deg) translateX(2px) rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg) translateX(2px) rotate(-360deg);
+  }
+}
+
+@keyframes uitour-zoom {
+  from {
+    transform: scale(0.9);
+  }
+  50% {
+    transform: scale(1.1);
+  }
+  to {
+    transform: scale(0.9);
+  }
+}
+
+@keyframes uitour-color {
+  from {
+    border-color: #5B9CD9;
+  }
+  50% {
+    border-color: #FF0000;
+  }
+  to {
+    border-color: #5B9CD9;
+  }
+}
+
+html|div#UITourHighlight {
+  display: none;
+  position: absolute;
+  min-height: 32px;
+  min-width: 32px;
+  display: none;
+  border: 2px #5B9CD9 solid;
+  box-shadow: 0 0 2px #5B9CD9, inset 0 0 1px #5B9CD9;
+  border-radius: 20px;
+  z-index: 10000000000;
+}
+
+html|div#UITourHighlight[active] {
+  display: block;
+  animation-delay: 2s;
+  animation-timing-function: linear;
+  animation-iteration-count: infinite;
+  animation-fill-mode: forwards;
+}
+
+html|div#UITourHighlight[active="wobble"] {
+  animation-name: uitour-wobble;
+  animation-duration: 1s;
+}
+html|div#UITourHighlight[active="zoom"] {
+  animation-name: uitour-zoom;
+  animation-duration: 1s;
+}
+html|div#UITourHighlight[active="color"] {
+  animation-name: uitour-color;
+  animation-duration: 2s;
+}
+
+#UITourTooltip {
+  max-width: 20em;
+}
+
+#UITourTooltipTitle {
+  font-weight: bold;
+  font-size: 130%;
+  margin: 0 0 5px 0;
+}
+
+#UITourTooltipDescription {
+  max-width: 20em;
+}
+
 /* === social toolbar button === */
 
 #social-toolbar-item > .toolbarbutton-1 {
   margin-left: 0;
   margin-right: 0;
   border-top-left-radius: 0;
   border-bottom-left-radius: 0;
   border-top-right-radius: 0;
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -2850,16 +2850,97 @@ toolbarbutton.bookmark-item[dragover="tr
   color: #FDF3DE;
   min-width: 16px;
   text-shadow: none;
   background-image: linear-gradient(#B4211B, #8A1915);
   border-radius: 1px;
   -moz-margin-end: 5px;
 }
 
+/* UI Tour */
+
+@keyframes uitour-wobble {
+  from {
+    transform: rotate(0deg) translateX(2px) rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg) translateX(2px) rotate(-360deg);
+  }
+}
+
+@keyframes uitour-zoom {
+  from {
+    transform: scale(0.9);
+  }
+  50% {
+    transform: scale(1.1);
+  }
+  to {
+    transform: scale(0.9);
+  }
+}
+
+@keyframes uitour-color {
+  from {
+    border-color: #5B9CD9;
+  }
+  50% {
+    border-color: #FF0000;
+  }
+  to {
+    border-color: #5B9CD9;
+  }
+}
+
+html|div#UITourHighlight {
+  display: none;
+  position: absolute;
+  min-height: 32px;
+  min-width: 32px;
+  display: none;
+  border: 2px #5B9CD9 solid;
+  box-shadow: 0 0 2px #5B9CD9, inset 0 0 1px #5B9CD9;
+  border-radius: 20px;
+  z-index: 10000000000;
+}
+
+html|div#UITourHighlight[active] {
+  display: block;
+  animation-delay: 2s;
+  animation-timing-function: linear;
+  animation-iteration-count: infinite;
+  animation-fill-mode: forwards;
+}
+
+html|div#UITourHighlight[active="wobble"] {
+  animation-name: uitour-wobble;
+  animation-duration: 1s;
+}
+html|div#UITourHighlight[active="zoom"] {
+  animation-name: uitour-zoom;
+  animation-duration: 1s;
+}
+html|div#UITourHighlight[active="color"] {
+  animation-name: uitour-color;
+  animation-duration: 2s;
+}
+
+#UITourTooltip {
+}
+
+#UITourTooltipTitle {
+  font-weight: bold;
+  font-size: 130%;
+  margin: 0 0 5px 0;
+}
+
+#UITourTooltipDescription {
+  max-width: 20em;
+}
+
 /* Social toolbar item */
 
 #social-provider-button {
   -moz-image-region: rect(0, 16px, 16px, 0);
   list-style-image: url(chrome://browser/skin/social/services-16.png);
 }
 
 #social-provider-button > .toolbarbutton-menu-dropmarker {
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -1771,31 +1771,33 @@ abstract public class BrowserApp extends
         if (menuItem != null)
             mMenu.removeItem(id);
     }
 
     private void updateAddonMenuItem(int id, JSONObject options) {
         // Set attribute for the menu item in cache, if available
         if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
             for (MenuItemInfo item : mAddonMenuItemsCache) {
-                 if (item.id == id) {
-                     item.checkable = options.optBoolean("checkable", item.checkable);
-                     item.checked = options.optBoolean("checked", item.checked);
-                     item.enabled = options.optBoolean("enabled", item.enabled);
-                     item.visible = options.optBoolean("visible", item.visible);
-                     break;
-                 }
+                if (item.id == id) {
+                    item.label = options.optString("name", item.label);
+                    item.checkable = options.optBoolean("checkable", item.checkable);
+                    item.checked = options.optBoolean("checked", item.checked);
+                    item.enabled = options.optBoolean("enabled", item.enabled);
+                    item.visible = options.optBoolean("visible", item.visible);
+                    break;
+                }
             }
         }
 
         if (mMenu == null)
             return;
 
         MenuItem menuItem = mMenu.findItem(id);
         if (menuItem != null) {
+            menuItem.setTitle(options.optString("name", menuItem.getTitle().toString()));
             menuItem.setCheckable(options.optBoolean("checkable", menuItem.isCheckable()));
             menuItem.setChecked(options.optBoolean("checked", menuItem.isChecked()));
             menuItem.setEnabled(options.optBoolean("enabled", menuItem.isEnabled()));
             menuItem.setVisible(options.optBoolean("visible", menuItem.isVisible()));
         }
     }
 
     @Override
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -4165,17 +4165,17 @@ var BrowserEventHandler = {
       // If we've pressed a scrollable element, let Java know that we may
       // want to override the scroll behaviour (for document sub-frames)
       this._scrollableElement = this._findScrollableElement(closest, true);
       this._firstScrollEvent = true;
 
       if (this._scrollableElement != null) {
         // Discard if it's the top-level scrollable, we let Java handle this
         let doc = BrowserApp.selectedBrowser.contentDocument;
-        if (this._scrollableElement != doc.documentElement)
+        if (this._scrollableElement != doc.body && this._scrollableElement != doc.documentElement)
           sendMessageToJava({ type: "Panning:Override" });
       }
     }
 
     if (!ElementTouchHelper.isElementClickable(closest, null, false))
       closest = ElementTouchHelper.elementFromPoint(aEvent.changedTouches[0].screenX,
                                                     aEvent.changedTouches[0].screenY);
     if (!closest)
@@ -4252,16 +4252,17 @@ var BrowserEventHandler = {
 
         if (this._firstScrollEvent) {
           while (this._scrollableElement != null &&
                  !this._elementCanScroll(this._scrollableElement, x, y))
             this._scrollableElement = this._findScrollableElement(this._scrollableElement, false);
 
           let doc = BrowserApp.selectedBrowser.contentDocument;
           if (this._scrollableElement == null ||
+              this._scrollableElement == doc.body ||
               this._scrollableElement == doc.documentElement) {
             sendMessageToJava({ type: "Panning:CancelOverride" });
             return;
           }
 
           this._firstScrollEvent = false;
         }
 
@@ -4622,24 +4623,25 @@ var BrowserEventHandler = {
         || computedStyle.overflowY == 'auto' || computedStyle.overflowY == 'scroll';
   },
 
   _findScrollableElement: function(elem, checkElem) {
     // Walk the DOM tree until we find a scrollable element
     let scrollable = false;
     while (elem) {
       /* Element is scrollable if its scroll-size exceeds its client size, and:
-       * - It has overflow 'auto' or 'scroll', or
-       * - It's a textarea or HTML node, or
+       * - It has overflow 'auto' or 'scroll'
+       * - It's a textarea
+       * - It's an HTML/BODY node
        * - It's a select element showing multiple rows
        */
       if (checkElem) {
         if ((elem.scrollTopMax > 0 || elem.scrollLeftMax > 0) &&
             (this._hasScrollableOverflow(elem) ||
-             elem.mozMatchesSelector("html, textarea")) ||
+             elem.mozMatchesSelector("html, body, textarea")) ||
             (elem instanceof HTMLSelectElement && (elem.size > 1 || elem.multiple))) {
           scrollable = true;
           break;
         }
       } else {
         checkElem = true;
       }
 
--- a/mobile/android/chrome/content/downloads.js
+++ b/mobile/android/chrome/content/downloads.js
@@ -257,19 +257,19 @@ AlertDownloadProgressListener.prototype 
         break;
       case Ci.nsIDownloadManager.DOWNLOAD_FAILED:
       case Ci.nsIDownloadManager.DOWNLOAD_CANCELED:
       case Ci.nsIDownloadManager.DOWNLOAD_BLOCKED_PARENTAL:
       case Ci.nsIDownloadManager.DOWNLOAD_DIRTY:
       case Ci.nsIDownloadManager.DOWNLOAD_FINISHED: {
         Downloads.removeNotification(aDownload);
         if (aDownload.isPrivate) {
-          let index = this._privateDownloads.indexOf(aDownload);
+          let index = Downloads._privateDownloads.indexOf(aDownload);
           if (index != -1) {
-            this._privateDownloads.splice(index, 1);
+            Downloads._privateDownloads.splice(index, 1);
           }
         }
 
         if (state == Ci.nsIDownloadManager.DOWNLOAD_FINISHED) {
           Downloads.showNotification(aDownload, Strings.browser.GetStringFromName("alertDownloadsDone2"),
                               aDownload.displayName);
         }
         break;
--- a/toolkit/content/widgets/videocontrols.xml
+++ b/toolkit/content/widgets/videocontrols.xml
@@ -951,17 +951,17 @@
                     this.startFade(element, true, immediate);
                 },
 
                 startFadeOut : function (element, immediate) {
                     this.startFade(element, false, immediate);
                 },
 
                 startFade : function (element, fadeIn, immediate) {
-                    if (element.className == "controlBar" && fadeIn) {
+                    if (element.classList.contains("controlBar") && fadeIn) {
                         // Bug 493523, the scrubber doesn't call valueChanged while hidden,
                         // so our dependent state (eg, timestamp in the thumb) will be stale.
                         // As a workaround, update it manually when it first becomes unhidden.
                         if (element.hidden)
                             this.scrubber.valueChanged("curpos", this.video.currentTime * 1000, false);
                     }
 
                     if (immediate)
@@ -970,21 +970,21 @@
                         element.removeAttribute("immediate");
 
                     if (fadeIn) {
                         element.hidden = false;
                         // force style resolution, so that transition begins
                         // when we remove the attribute.
                         element.clientTop;
                         element.removeAttribute("fadeout");
-                        if (element.className == "controlBar")
+                        if (element.classList.contains("controlBar"))
                             this.controlsSpacer.removeAttribute("hideCursor");
                     } else {
                         element.setAttribute("fadeout", true);
-                        if (element.className == "controlBar" && !this.hasError() &&
+                        if (element.classList.contains("controlBar") && !this.hasError() &&
                             document.mozFullScreenElement == this.video)
                             this.controlsSpacer.setAttribute("hideCursor", true);
 
                     }
                 },
 
                 onTransitionEnd : function (event) {
                     // Ignore events for things other than opacity changes.
--- a/toolkit/devtools/css-color.js
+++ b/toolkit/devtools/css-color.js
@@ -23,16 +23,24 @@ const REGEX_HSL_3_TUPLE  = /^\bhsl\(([\d
  *  - red
  *
  *  It also matches css keywords e.g. "background-color" otherwise
  *  "background" would be replaced with #6363CE ("background" is a platform
  *  color).
  */
 const REGEX_ALL_COLORS = /#[0-9a-fA-F]{3}\b|#[0-9a-fA-F]{6}\b|hsl\(.*?\)|hsla\(.*?\)|rgba?\(.*?\)|\b[a-zA-Z-]+\b/g;
 
+const SPECIALVALUES = new Set([
+  "currentcolor",
+  "initial",
+  "inherit",
+  "transparent",
+  "unset"
+]);
+
 let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
 
 /**
  * This module is used to convert between various color types.
  *
  * Usage:
  *   let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
  *   let {colorUtils} = devtools.require("devtools/css-color");
@@ -84,152 +92,159 @@ CssColor.COLORUNIT = {
   "rgb": "rgb",
   "hsl": "hsl"
 };
 
 CssColor.prototype = {
   authored: null,
 
   get hasAlpha() {
-    if (!this.valid || this.transparent) {
+    if (!this.valid) {
       return false;
     }
     return this._getRGBATuple().a !== 1;
   },
 
   get valid() {
     return this._validateColor(this.authored);
   },
 
+  /**
+   * Return true for all transparent values e.g. rgba(0, 0, 0, 0).
+   */
   get transparent() {
     try {
       let tuple = this._getRGBATuple();
-      return tuple === "transparent";
+      return !(tuple.r || tuple.g || tuple.b || tuple.a);
     } catch(e) {
       return false;
     }
   },
 
+  get specialValue() {
+    if (SPECIALVALUES.has(this.authored)) {
+      return this.authored;
+    }
+  },
+
   get name() {
     if (!this.valid) {
       return "";
     }
-    if (this.authored === "transparent") {
-      return "transparent";
+    if (this.specialValue) {
+      return this.specialValue;
     }
+
     try {
       let tuple = this._getRGBATuple();
 
-      if (tuple === "transparent") {
-        return "transparent";
-      }
       if (tuple.a !== 1) {
         return this.rgb;
       }
       let {r, g, b} = tuple;
       return DOMUtils.rgbToColorName(r, g, b);
     } catch(e) {
       return this.hex;
     }
   },
 
   get hex() {
     if (!this.valid) {
       return "";
     }
+    if (this.specialValue) {
+      return this.specialValue;
+    }
     if (this.hasAlpha) {
       return this.rgba;
     }
-    if (this.transparent) {
-      return "transparent";
-    }
 
     let hex = this.longHex;
     if (hex.charAt(1) == hex.charAt(2) &&
         hex.charAt(3) == hex.charAt(4) &&
         hex.charAt(5) == hex.charAt(6)) {
       hex = "#" + hex.charAt(1) + hex.charAt(3) + hex.charAt(5);
     }
     return hex;
   },
 
   get longHex() {
     if (!this.valid) {
       return "";
     }
+    if (this.specialValue) {
+      return this.specialValue;
+    }
     if (this.hasAlpha) {
       return this.rgba;
     }
-    if (this.transparent) {
-      return "transparent";
-    }
     return this.rgb.replace(/\brgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)/gi, function(_, r, g, b) {
       return "#" + ((1 << 24) + (r << 16) + (g << 8) + (b << 0)).toString(16).substr(-6).toUpperCase();
     });
   },
 
   get rgb() {
     if (!this.valid) {
       return "";
     }
-    if (this.transparent) {
-      return "transparent";
+    if (this.specialValue) {
+      return this.specialValue;
     }
     if (!this.hasAlpha) {
       if (this.authored.startsWith("rgb(")) {
         // The color is valid and begins with rgb(. Return the authored value.
         return this.authored;
       }
       let tuple = this._getRGBATuple();
       return "rgb(" + tuple.r + ", " + tuple.g + ", " + tuple.b + ")";
     }
     return this.rgba;
   },
 
   get rgba() {
     if (!this.valid) {
       return "";
     }
-    if (this.transparent) {
-      return "transparent";
+    if (this.specialValue) {
+      return this.specialValue;
     }
     if (this.authored.startsWith("rgba(")) {
       // The color is valid and begins with rgba(. Return the authored value.
         return this.authored;
     }
     let components = this._getRGBATuple();
     return "rgba(" + components.r + ", " +
                      components.g + ", " +
                      components.b + ", " +
                      components.a + ")";
   },
 
   get hsl() {
     if (!this.valid) {
       return "";
     }
-    if (this.transparent) {
-      return "transparent";
+    if (this.specialValue) {
+      return this.specialValue;
     }
     if (this.authored.startsWith("hsl(")) {
       // The color is valid and begins with hsl(. Return the authored value.
       return this.authored;
     }
     if (this.hasAlpha) {
       return this.hsla;
     }
     return this._hslNoAlpha();
   },
 
   get hsla() {
     if (!this.valid) {
       return "";
     }
-    if (this.transparent) {
-      return "transparent";
+    if (this.specialValue) {
+      return this.specialValue;
     }
     if (this.authored.startsWith("hsla(")) {
       // The color is valid and begins with hsla(. Return the authored value.
       return this.authored;
     }
     if (this.hasAlpha) {
       let a = this._getRGBATuple().a;
       return this._hslNoAlpha().replace("hsl", "hsla").replace(")", ", " + a + ")");
@@ -285,17 +300,17 @@ CssColor.prototype = {
   _getRGBATuple: function() {
     let win = Services.appShell.hiddenDOMWindow;
     let doc = win.document;
     let span = doc.createElement("span");
     span.style.color = this.authored;
     let computed = win.getComputedStyle(span).color;
 
     if (computed === "transparent") {
-      return "transparent";
+      return {r: 0, g: 0, b: 0, a: 0};
     }
 
     let rgba = computed.match(REGEX_RGBA_4_TUPLE);
 
     if (rgba) {
       let [, r, g, b, a] = rgba;
       return {r: r, g: g, b: b, a: a};
     } else {