Merge fx-team to m-c.
authorRyan VanderMeulen <ryanvm@gmail.com>
Fri, 06 Dec 2013 16:03:56 -0500
changeset 174927 b3806ae5399d5b1a23ad5f14ecb28406c2ad59a2
parent 174895 79d1a5a93b64fe467beb7a4897cc680d2f6b50d4 (current diff)
parent 174926 7bda9296f32bbf13574bc84e65a4834301c0a085 (diff)
child 174928 a69b97c58e0fa61713447378548e79c1d53df07c
child 174935 5aecc00ca4dd90cce91076fe5a061351527bfb69
child 174978 7229b03abd0f4a72c4e32fcc61018412c1f64d8f
child 174999 f9e31850d15e7d86c90d704c915fc5ca5022601d
push id445
push userffxbld
push dateMon, 10 Mar 2014 22:05:19 +0000
treeherdermozilla-release@dc38b741b04e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone28.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
Merge fx-team to m-c.
mobile/android/base/RobocopAPI.java
toolkit/mozapps/installer/packager.mk
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -864,48 +864,51 @@ var PlacesMenuDNDHandler = {
  */
 let PlacesToolbarHelper = {
   _place: "place:folder=TOOLBAR",
 
   get _viewElt() {
     return document.getElementById("PlacesToolbar");
   },
 
-  init: function PTH_init() {
+  init: function PTH_init(forceToolbarOverflowCheck) {
     let viewElt = this._viewElt;
     if (!viewElt || viewElt._placesView)
       return;
 
     // If the bookmarks toolbar item is:
     // - not in a toolbar, or;
     // - the toolbar is collapsed, or;
     // - the toolbar is hidden some other way:
     // don't initialize.  Also, there is no need to initialize the toolbar if
     // customizing, because that will happen when the customization is done.
     let toolbar = this._getParentToolbar(viewElt);
     if (!toolbar || toolbar.collapsed || this._isCustomizing ||
         getComputedStyle(toolbar, "").display == "none")
       return;
 
     new PlacesToolbar(this._place);
+    if (forceToolbarOverflowCheck) {
+      viewElt._placesView.updateOverflowStatus();
+    }
   },
 
   customizeStart: function PTH_customizeStart() {
     try {
       let viewElt = this._viewElt;
       if (viewElt && viewElt._placesView)
         viewElt._placesView.uninit();
     } finally {
       this._isCustomizing = true;
     }
   },
 
   customizeDone: function PTH_customizeDone() {
     this._isCustomizing = false;
-    this.init();
+    this.init(true);
   },
 
   onPlaceholderCommand: function () {
     let widgetGroup = CustomizableUI.getWidget("personal-bookmarks");
     let widget = widgetGroup.forWindow(window);
     if (widget.overflowed ||
         widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
       PlacesCommandHook.showPlacesOrganizer("BookmarksToolbar");
--- a/browser/components/places/content/browserPlacesViews.js
+++ b/browser/components/places/content/browserPlacesViews.js
@@ -1032,42 +1032,24 @@ PlacesToolbar.prototype = {
         break;
       case "resize":
         // This handler updates nodes visibility in both the toolbar
         // and the chevron popup when a window resize does not change
         // the overflow status of the toolbar.
         this.updateChevron();
         break;
       case "overflow":
-        if (aEvent.target != aEvent.currentTarget)
-          return;
-
-        // Ignore purely vertical overflows.
-        if (aEvent.detail == 0)
+        if (!this._isOverflowStateEventRelevant(aEvent))
           return;
-
-        // Attach the popup binding to the chevron popup if it has not yet
-        // been initialized.
-        if (!this._chevronPopup.hasAttribute("type")) {
-          this._chevronPopup.setAttribute("place", this.place);
-          this._chevronPopup.setAttribute("type", "places");
-        }
-        this._chevron.collapsed = false;
-        this.updateChevron();
+        this._onOverflow();
         break;
       case "underflow":
-        if (aEvent.target != aEvent.currentTarget)
+        if (!this._isOverflowStateEventRelevant(aEvent))
           return;
-
-        // Ignore purely vertical underflows.
-        if (aEvent.detail == 0)
-          return;
-
-        this.updateChevron();
-        this._chevron.collapsed = true;
+        this._onUnderflow();
         break;
       case "TabOpen":
       case "TabClose":
         this.updateChevron();
         break;
       case "dragstart":
         this._onDragStart(aEvent);
         break;
@@ -1098,16 +1080,45 @@ PlacesToolbar.prototype = {
       case "popuphidden":
         this._onPopupHidden(aEvent);
         break;
       default:
         throw "Trying to handle unexpected event.";
     }
   },
 
+  updateOverflowStatus: function() {
+    if (this._rootElt.scrollLeftMax > 0) {
+      this._onOverflow();
+    } else {
+      this._onUnderflow();
+    }
+  },
+
+  _isOverflowStateEventRelevant: function PT_isOverflowStateEventRelevant(aEvent) {
+    // Ignore events not aimed at ourselves, as well as purely vertical ones:
+    return aEvent.target == aEvent.currentTarget && aEvent.detail > 0;
+  },
+
+  _onOverflow: function PT_onOverflow() {
+    // Attach the popup binding to the chevron popup if it has not yet
+    // been initialized.
+    if (!this._chevronPopup.hasAttribute("type")) {
+      this._chevronPopup.setAttribute("place", this.place);
+      this._chevronPopup.setAttribute("type", "places");
+    }
+    this._chevron.collapsed = false;
+    this.updateChevron();
+  },
+
+  _onUnderflow: function PT_onUnderflow() {
+    this.updateChevron();
+    this._chevron.collapsed = true;
+  },
+
   updateChevron: function PT_updateChevron() {
     // If the chevron is collapsed there's nothing to update.
     if (this._chevron.collapsed)
       return;
 
     // Update the chevron on a timer.  This will avoid repeated work when
     // lot of changes happen in a small timeframe.
     if (this._updateChevronTimer)
--- a/browser/components/sessionstore/src/SessionStore.jsm
+++ b/browser/components/sessionstore/src/SessionStore.jsm
@@ -1503,22 +1503,44 @@ let SessionStoreInternal = {
     if (DyingWindowCache.has(aWindow)) {
       let data = DyingWindowCache.get(aWindow);
       return this._toJSONString({ windows: [data] });
     }
 
     throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
   },
 
+  /**
+   * Restores the given state |aState| for a given window |aWindow|.
+   *
+   * @param aWindow (xul window)
+   *        The window that the given state will be restored to.
+   * @param aState (string)
+   *        The state that will be applied to the given window.
+   * @param aOverwrite (bool)
+   *        When true, existing tabs in the given window will be re-used or
+   *        removed. When false, only new tabs will be added, no existing ones
+   8        will be removed or overwritten.
+   */
   setWindowState: function ssi_setWindowState(aWindow, aState, aOverwrite) {
     if (!aWindow.__SSi) {
       throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
     }
 
-    this.restoreWindow(aWindow, aState, {overwriteTabs: aOverwrite});
+    let winState = JSON.parse(aState);
+    if (!winState) {
+      throw Components.Exception("Invalid state string: not JSON", Cr.NS_ERROR_INVALID_ARG);
+    }
+
+    if (!winState.windows || !winState.windows[0]) {
+      throw Components.Exception("Invalid window state passed", Cr.NS_ERROR_INVALID_ARG);
+    }
+
+    let state = {windows: [winState.windows[0]]};
+    this.restoreWindow(aWindow, state, {overwriteTabs: aOverwrite});
   },
 
   getTabState: function ssi_getTabState(aTab) {
     if (!aTab.ownerDocument) {
       throw Components.Exception("Invalid tab object: no ownerDocument", Cr.NS_ERROR_INVALID_ARG);
     }
     if (!aTab.ownerDocument.defaultView.__SSi) {
       throw Components.Exception("Default view is not tracked", Cr.NS_ERROR_INVALID_ARG);
@@ -2275,17 +2297,17 @@ let SessionStoreInternal = {
 
   /* ........ Restoring Functionality .............. */
 
   /**
    * restore features to a single window
    * @param aWindow
    *        Window reference
    * @param aState
-   *        JS object or its eval'able source
+   *        JS object
    * @param aOptions
    *        {overwriteTabs: true} to overwrite existing tabs w/ new ones
    *        {isFollowUp: true} if this is not the restoration of the 1st window
    *        {firstWindow: true} if this is the first non-private window we're
    *                            restoring in this session, that might open an
    *                            external link as well
    */
   restoreWindow: function ssi_restoreWindow(aWindow, aState, aOptions = {}) {
@@ -2295,27 +2317,20 @@ let SessionStoreInternal = {
 
     if (isFollowUp) {
       this.windowToFocus = aWindow;
     }
     // initialize window if necessary
     if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi]))
       this.onLoad(aWindow);
 
-    try {
-      var root = typeof aState == "string" ? JSON.parse(aState) : aState;
-      if (!root.windows[0]) {
-        this._sendRestoreCompletedNotifications();
-        return; // nothing to restore
-      }
-    }
-    catch (ex) { // invalid state object - don't restore anything
-      debug(ex);
+    var root = aState;
+    if (!root.windows[0]) {
       this._sendRestoreCompletedNotifications();
-      return;
+      return; // nothing to restore
     }
 
     TelemetryStopwatch.start("FX_SESSION_RESTORE_RESTORE_WINDOW_MS");
 
     // We're not returning from this before we end up calling restoreTabs
     // for this window, so make sure we send the SSWindowStateBusy event.
     this._setWindowStateBusy(aWindow);
 
--- a/browser/components/sessionstore/test/browser_491577.js
+++ b/browser/components/sessionstore/test/browser_491577.js
@@ -73,49 +73,43 @@ function test() {
       aFunction();
       return false;
     }
     catch (ex) {
       return ex.name == "NS_ERROR_ILLEGAL_VALUE";
     }
   }
 
-  // open a window and add the above closed window list
-  let newWin = openDialog(location, "_blank", "chrome,all,dialog=no");
-  newWin.addEventListener("load", function(aEvent) {
-    this.removeEventListener("load", arguments.callee, false);
-    gPrefService.setIntPref("browser.sessionstore.max_windows_undo",
-                            test_state._closedWindows.length);
-    ss.setWindowState(newWin, JSON.stringify(test_state), true);
+  gPrefService.setIntPref("browser.sessionstore.max_windows_undo",
+                          test_state._closedWindows.length);
+  ss.setBrowserState(JSON.stringify(test_state), true);
 
-    let closedWindows = JSON.parse(ss.getClosedWindowData());
-    is(closedWindows.length, test_state._closedWindows.length,
-       "Closed window list has the expected length");
-    is(countByTitle(closedWindows, FORGET),
-       test_state._closedWindows.length - remember_count,
-       "The correct amount of windows are to be forgotten");
-    is(countByTitle(closedWindows, REMEMBER), remember_count,
-       "Everything is set up.");
+  let closedWindows = JSON.parse(ss.getClosedWindowData());
+  is(closedWindows.length, test_state._closedWindows.length,
+     "Closed window list has the expected length");
+  is(countByTitle(closedWindows, FORGET),
+     test_state._closedWindows.length - remember_count,
+     "The correct amount of windows are to be forgotten");
+  is(countByTitle(closedWindows, REMEMBER), remember_count,
+     "Everything is set up.");
 
-    // all of the following calls with illegal arguments should throw NS_ERROR_ILLEGAL_VALUE
-    ok(testForError(function() ss.forgetClosedWindow(-1)),
-       "Invalid window for forgetClosedWindow throws");
-    ok(testForError(function() ss.forgetClosedWindow(test_state._closedWindows.length + 1)),
-       "Invalid window for forgetClosedWindow throws");
+  // all of the following calls with illegal arguments should throw NS_ERROR_ILLEGAL_VALUE
+  ok(testForError(function() ss.forgetClosedWindow(-1)),
+     "Invalid window for forgetClosedWindow throws");
+  ok(testForError(function() ss.forgetClosedWindow(test_state._closedWindows.length + 1)),
+     "Invalid window for forgetClosedWindow throws");
 
-    // Remove third window, then first window
-    ss.forgetClosedWindow(2);
-    ss.forgetClosedWindow(null);
+  // Remove third window, then first window
+  ss.forgetClosedWindow(2);
+  ss.forgetClosedWindow(null);
 
-    closedWindows = JSON.parse(ss.getClosedWindowData());
-    is(closedWindows.length, remember_count,
-       "The correct amount of windows were removed");
-    is(countByTitle(closedWindows, FORGET), 0,
-       "All windows specifically forgotten were indeed removed");
-    is(countByTitle(closedWindows, REMEMBER), remember_count,
-       "... and windows not specifically forgetten weren't.");
+  closedWindows = JSON.parse(ss.getClosedWindowData());
+  is(closedWindows.length, remember_count,
+     "The correct amount of windows were removed");
+  is(countByTitle(closedWindows, FORGET), 0,
+     "All windows specifically forgotten were indeed removed");
+  is(countByTitle(closedWindows, REMEMBER), remember_count,
+     "... and windows not specifically forgetten weren't.");
 
-    // clean up
-    newWin.close();
-    gPrefService.clearUserPref("browser.sessionstore.max_windows_undo");
-    finish();
-  }, false);
+  // clean up
+  gPrefService.clearUserPref("browser.sessionstore.max_windows_undo");
+  finish();
 }
--- a/browser/devtools/debugger/debugger-panes.js
+++ b/browser/devtools/debugger/debugger-panes.js
@@ -681,19 +681,19 @@ SourcesView.prototype = Heritage.extend(
    */
   _onSourceSelect: function({ detail: sourceItem }) {
     if (!sourceItem) {
       return;
     }
     // The container is not empty and an actual item was selected.
     DebuggerView.setEditorLocation(sourceItem.value);
 
-    // Set window title.
-    let script = sourceItem.value.split(" -> ").pop();
-    document.title = L10N.getFormatStr("DebuggerWindowScriptTitle", script);
+    // Set window title. No need to split the url by " -> " here, because it was
+    // already sanitized when the source was added.
+    document.title = L10N.getFormatStr("DebuggerWindowScriptTitle", sourceItem.value);
 
     DebuggerView.maybeShowBlackBoxMessage();
     this.updateToolbarButtonsState();
   },
 
   /**
    * Update the checked/unchecked and enabled/disabled states of the buttons in
    * the sources toolbar based on the currently selected source's state.
@@ -1080,48 +1080,24 @@ let SourceUtils = {
   getSourceGroup: function(aUrl) {
     let cachedGroup = this._groupsCache.get(aUrl);
     if (cachedGroup) {
       return cachedGroup;
     }
 
     try {
       // Use an nsIURL to parse all the url path parts.
-      var uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
+      let url = aUrl.split(" -> ").pop();
+      var uri = Services.io.newURI(url, null, null).QueryInterface(Ci.nsIURL);
     } catch (e) {
       // This doesn't look like a url, or nsIURL can't handle it.
       return "";
     }
 
-    let { scheme, directory, fileName } = uri;
-    let hostPort;
-    // Add-on SDK jar: URLs will cause accessing hostPort to throw.
-    if (scheme != "jar") {
-      hostPort = uri.hostPort;
-    }
-    let lastDir = directory.split("/").reverse()[1];
-    let group = [];
-
-    // Only show interesting schemes, http is implicit.
-    if (scheme != "http") {
-      group.push(scheme);
-    }
-    // Hostnames don't always exist for files or some resource urls.
-    // e.g. file://foo/bar.js or resource:///foo/bar.js don't have a host.
-    if (hostPort) {
-      // If the hostname is a dot-separated identifier, show the first 2 parts.
-      group.push(hostPort.split(".").slice(0, 2).join("."));
-    }
-    // Append the last directory if the path leads to an actual file.
-    // e.g. http://foo.org/bar/ should only show "foo.org", not "foo.org bar"
-    if (fileName) {
-      group.push(lastDir);
-    }
-
-    let groupLabel = group.join(" ");
+    let groupLabel = uri.prePath;
     let unicodeLabel = NetworkHelper.convertToUnicode(unescape(groupLabel));
     this._groupsCache.set(aUrl, unicodeLabel)
     return unicodeLabel;
   },
 
   /**
    * Trims the url by shortening it if it exceeds a certain length, adding an
    * ellipsis at the end.
--- a/browser/devtools/debugger/debugger-toolbar.js
+++ b/browser/devtools/debugger/debugger-toolbar.js
@@ -394,31 +394,26 @@ function StackFramesView() {
 
 StackFramesView.prototype = Heritage.extend(WidgetMethods, {
   /**
    * Initialization function, called when the debugger is started.
    */
   initialize: function() {
     dumpn("Initializing the StackFramesView");
 
-    let commandset = this._commandset = document.createElement("commandset");
-    let menupopup = this._menupopup = document.createElement("menupopup");
-    commandset.id = "stackframesCommandset";
-    menupopup.id = "stackframesMenupopup";
-
-    document.getElementById("debuggerPopupset").appendChild(menupopup);
-    document.getElementById("debuggerCommands").appendChild(commandset);
-
     this.widget = new BreadcrumbsWidget(document.getElementById("stackframes"));
     this.widget.addEventListener("select", this._onSelect, false);
     this.widget.addEventListener("scroll", this._onScroll, true);
     window.addEventListener("resize", this._onScroll, true);
 
     this.autoFocusOnFirstItem = false;
     this.autoFocusOnSelection = false;
+
+    // This view's contents are also mirrored in a different container.
+    this._mirror = DebuggerView.StackFramesClassicList;
   },
 
   /**
    * Destruction function, called when the debugger is closed.
    */
   destroy: function() {
     dumpn("Destroying the StackFramesView");
 
@@ -448,34 +443,32 @@ StackFramesView.prototype = Heritage.ext
       if (this._prevBlackBoxedUrl == aUrl) {
         return;
       }
       this._prevBlackBoxedUrl = aUrl;
     } else {
       this._prevBlackBoxedUrl = null;
     }
 
-    // Create the element node and menu entry for the stack frame item.
+    // Create the element node for the stack frame item.
     let frameView = this._createFrameView.apply(this, arguments);
-    let menuEntry = this._createMenuEntry.apply(this, arguments);
 
     // Append a stack frame item to this container.
     this.push([frameView, aTitle, aUrl], {
       index: 0, /* specifies on which position should the item be appended */
       attachment: {
-        popup: menuEntry,
         depth: aDepth
       },
-      attributes: [
-        ["contextmenu", "stackframesMenupopup"]
-      ],
       // Make sure that when the stack frame item is removed, the corresponding
-      // menuitem and command are also destroyed.
+      // mirrored item in the classic list is also removed.
       finalize: this._onStackframeRemoved
     });
+
+    // Mirror this newly inserted item inside the "Call Stack" tab.
+    this._mirror.addFrame(aTitle, aUrl, aLine, aDepth);
   },
 
   /**
    * Selects the frame at the specified depth in this container.
    * @param number aDepth
    */
   set selectedDepth(aDepth) {
     this.selectedItem = aItem => aItem.attachment.depth == aDepth;
@@ -538,110 +531,46 @@ StackFramesView.prototype = Heritage.ext
     frameDetailsNode.className = "plain dbg-stackframe-details breadcrumbs-widget-item-id";
     frameDetailsNode.setAttribute("value", frameDetails);
     container.appendChild(frameDetailsNode);
 
     return container;
   },
 
   /**
-   * Customization function for populating an item's context menu.
-   *
-   * @param string aTitle
-   *        The frame title to be displayed in the list.
-   * @param string aUrl
-   *        The frame source url.
-   * @param string aLine
-   *        The frame line number.
-   * @param number aDepth
-   *        The frame depth in the stack.
-   * @param boolean aIsBlackBoxed
-   *        Whether or not the frame is black boxed.
-   * @return object
-   *         An object containing the stack frame command and menu item.
-   */
-  _createMenuEntry: function(aTitle, aUrl, aLine, aDepth, aIsBlackBoxed) {
-    let frameDescription = SourceUtils.trimUrlLength(
-      SourceUtils.getSourceLabel(aUrl),
-      STACK_FRAMES_POPUP_SOURCE_URL_MAX_LENGTH,
-      STACK_FRAMES_POPUP_SOURCE_URL_TRIM_SECTION) +
-      SEARCH_LINE_FLAG + aLine;
-
-    let prefix = "sf-cMenu-"; // "stackframes context menu"
-    let commandId = prefix + aDepth + "-" + "-command";
-    let menuitemId = prefix + aDepth + "-" + "-menuitem";
-
-    let command = document.createElement("command");
-    command.id = commandId;
-    command.addEventListener("command", () => this.selectedDepth = aDepth, false);
-
-    let menuitem = document.createElement("menuitem");
-    menuitem.id = menuitemId;
-    menuitem.className = "dbg-stackframe-menuitem";
-    menuitem.setAttribute("type", "checkbox");
-    menuitem.setAttribute("command", commandId);
-    menuitem.setAttribute("tooltiptext", aUrl);
-
-    let labelNode = document.createElement("label");
-    labelNode.className = "plain dbg-stackframe-menuitem-title";
-    labelNode.setAttribute("value", aTitle);
-    labelNode.setAttribute("flex", "1");
-
-    let descriptionNode = document.createElement("label");
-    descriptionNode.className = "plain dbg-stackframe-menuitem-details";
-    descriptionNode.setAttribute("value", frameDescription);
-
-    menuitem.appendChild(labelNode);
-    menuitem.appendChild(descriptionNode);
-
-    this._commandset.appendChild(command);
-    this._menupopup.appendChild(menuitem);
-
-    return {
-      command: command,
-      menuitem: menuitem
-    };
-  },
-
-  /**
    * Function called each time a stack frame item is removed.
    *
    * @param object aItem
    *        The corresponding item.
    */
   _onStackframeRemoved: function(aItem) {
     dumpn("Finalizing stackframe item: " + aItem);
 
-    // Destroy the context menu item for the stack frame.
-    let contextItem = aItem.attachment.popup;
-    contextItem.command.remove();
-    contextItem.menuitem.remove();
+    // Remove the mirrored item in the classic list.
+    let depth = aItem.attachment.depth;
+    this._mirror.remove(this._mirror.getItemForAttachment(e => e.depth == depth));
 
     // Forget the previously blackboxed stack frame url.
     this._prevBlackBoxedUrl = null;
   },
 
   /**
    * The select listener for the stackframes container.
    */
   _onSelect: function(e) {
     let stackframeItem = this.selectedItem;
     if (stackframeItem) {
       // The container is not empty and an actual item was selected.
-      DebuggerController.StackFrames.selectFrame(stackframeItem.attachment.depth);
+      let depth = stackframeItem.attachment.depth;
+      DebuggerController.StackFrames.selectFrame(depth);
 
-      // Update the context menu to show the currently selected stackframe item
-      // as a checked entry.
-      for (let otherItem of this) {
-        if (otherItem != stackframeItem) {
-          otherItem.attachment.popup.menuitem.removeAttribute("checked");
-        } else {
-          otherItem.attachment.popup.menuitem.setAttribute("checked", "");
-        }
-      }
+      // Mirror the selected item in the classic list.
+      this.suppressSelectionEvents = true;
+      this._mirror.selectedItem = e => e.attachment.depth == depth;
+      this.suppressSelectionEvents = false;
     }
   },
 
   /**
    * The scroll listener for the stackframes container.
    */
   _onScroll: function() {
     // Update the stackframes container only if we have to.
@@ -668,21 +597,152 @@ StackFramesView.prototype = Heritage.ext
       list.ensureElementIsVisible(this.getItemAtIndex(CALL_STACK_PAGE_SIZE - 1).target);
       this.dirty = false;
 
       // Loads more stack frames from the debugger server cache.
       DebuggerController.StackFrames.addMoreFrames();
     }
   },
 
-  _commandset: null,
-  _menupopup: null,
+  _mirror: null,
   _prevBlackBoxedUrl: null
 });
 
+/*
+ * Functions handling the stackframes classic list UI.
+ * Controlled by the DebuggerView.StackFrames isntance.
+ */
+function StackFramesClassicListView() {
+  dumpn("StackFramesClassicListView was instantiated");
+
+  this._onSelect = this._onSelect.bind(this);
+}
+
+StackFramesClassicListView.prototype = Heritage.extend(WidgetMethods, {
+  /**
+   * Initialization function, called when the debugger is started.
+   */
+  initialize: function() {
+    dumpn("Initializing the StackFramesClassicListView");
+
+    this.widget = new SideMenuWidget(document.getElementById("callstack-list"), {
+      theme: "light"
+    });
+    this.widget.addEventListener("select", this._onSelect, false);
+
+    this.emptyText = L10N.getStr("noStackFramesText");
+    this.autoFocusOnFirstItem = false;
+    this.autoFocusOnSelection = false;
+
+    // This view's contents are also mirrored in a different container.
+    this._mirror = DebuggerView.StackFrames;
+
+    // Show an empty label by default.
+    this.empty();
+  },
+
+  /**
+   * Destruction function, called when the debugger is closed.
+   */
+  destroy: function() {
+    dumpn("Destroying the StackFramesClassicListView");
+
+    this.widget.removeEventListener("select", this._onSelect, false);
+  },
+
+  /**
+   * Adds a frame in this stackframes container.
+   *
+   * @param string aTitle
+   *        The frame title (function name).
+   * @param string aUrl
+   *        The frame source url.
+   * @param string aLine
+   *        The frame line number.
+   * @param number aDepth
+   *        The frame depth in the stack.
+   */
+  addFrame: function(aTitle, aUrl, aLine, aDepth) {
+    // Create the element node for the stack frame item.
+    let frameView = this._createFrameView.apply(this, arguments);
+
+    // Append a stack frame item to this container.
+    this.push([frameView, aUrl], {
+      attachment: {
+        depth: aDepth
+      }
+    });
+  },
+
+  /**
+   * Customization function for creating an item's UI.
+   *
+   * @param string aTitle
+   *        The frame title to be displayed in the list.
+   * @param string aUrl
+   *        The frame source url.
+   * @param string aLine
+   *        The frame line number.
+   * @param number aDepth
+   *        The frame depth in the stack.
+   * @return nsIDOMNode
+   *         The stack frame view.
+   */
+  _createFrameView: function(aTitle, aUrl, aLine, aDepth) {
+    let container = document.createElement("hbox");
+    container.id = "classic-stackframe-" + aDepth;
+    container.className = "dbg-classic-stackframe";
+    container.setAttribute("flex", "1");
+
+    let frameTitleNode = document.createElement("label");
+    frameTitleNode.className = "plain dbg-classic-stackframe-title";
+    frameTitleNode.setAttribute("value", aTitle);
+    frameTitleNode.setAttribute("crop", "center");
+
+    let frameDetailsNode = document.createElement("hbox");
+    frameDetailsNode.className = "plain dbg-classic-stackframe-details";
+
+    let frameUrlNode = document.createElement("label");
+    frameUrlNode.className = "plain dbg-classic-stackframe-details-url";
+    frameUrlNode.setAttribute("value", SourceUtils.getSourceLabel(aUrl));
+    frameUrlNode.setAttribute("crop", "center");
+    frameDetailsNode.appendChild(frameUrlNode);
+
+    let frameDetailsSeparator = document.createElement("label");
+    frameDetailsSeparator.className = "plain dbg-classic-stackframe-details-sep";
+    frameDetailsSeparator.setAttribute("value", SEARCH_LINE_FLAG);
+    frameDetailsNode.appendChild(frameDetailsSeparator);
+
+    let frameLineNode = document.createElement("label");
+    frameLineNode.className = "plain dbg-classic-stackframe-details-line";
+    frameLineNode.setAttribute("value", aLine);
+    frameDetailsNode.appendChild(frameLineNode);
+
+    container.appendChild(frameTitleNode);
+    container.appendChild(frameDetailsNode);
+
+    return container;
+  },
+
+  /**
+   * The select listener for the stackframes container.
+   */
+  _onSelect: function(e) {
+    let stackframeItem = this.selectedItem;
+    if (stackframeItem) {
+      // The container is not empty and an actual item was selected.
+      // Mirror the selected item in the breadcrumbs list.
+      let depth = stackframeItem.attachment.depth;
+      this._mirror.selectedItem = e => e.attachment.depth == depth;
+    }
+  },
+
+  _mirror: null
+});
+
 /**
  * Functions handling the filtering UI.
  */
 function FilterView() {
   dumpn("FilterView was instantiated");
 
   this._onClick = this._onClick.bind(this);
   this._onInput = this._onInput.bind(this);
@@ -1528,8 +1588,9 @@ FilteredFunctionsView.prototype = Herita
  */
 DebuggerView.Toolbar = new ToolbarView();
 DebuggerView.Options = new OptionsView();
 DebuggerView.Filtering = new FilterView();
 DebuggerView.FilteredSources = new FilteredSourcesView();
 DebuggerView.FilteredFunctions = new FilteredFunctionsView();
 DebuggerView.ChromeGlobals = new ChromeGlobalsView();
 DebuggerView.StackFrames = new StackFramesView();
+DebuggerView.StackFramesClassicList = new StackFramesClassicListView();
--- a/browser/devtools/debugger/debugger-view.js
+++ b/browser/devtools/debugger/debugger-view.js
@@ -4,18 +4,16 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 1048576; // 1 MB in bytes
 const SOURCE_URL_DEFAULT_MAX_LENGTH = 64; // chars
 const STACK_FRAMES_SOURCE_URL_MAX_LENGTH = 15; // chars
 const STACK_FRAMES_SOURCE_URL_TRIM_SECTION = "center";
-const STACK_FRAMES_POPUP_SOURCE_URL_MAX_LENGTH = 32; // chars
-const STACK_FRAMES_POPUP_SOURCE_URL_TRIM_SECTION = "center";
 const STACK_FRAMES_SCROLL_DELAY = 100; // ms
 const BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH = 1000; // chars
 const BREAKPOINT_CONDITIONAL_POPUP_POSITION = "before_start";
 const BREAKPOINT_CONDITIONAL_POPUP_OFFSET_X = 7; // px
 const BREAKPOINT_CONDITIONAL_POPUP_OFFSET_Y = -3; // px
 const RESULTS_PANEL_POPUP_POSITION = "before_end";
 const RESULTS_PANEL_MAX_RESULTS = 10;
 const FILE_SEARCH_ACTION_MAX_DELAY = 300; // ms
@@ -54,16 +52,17 @@ let DebuggerView = {
     this._initializePanes();
     this.Toolbar.initialize();
     this.Options.initialize();
     this.Filtering.initialize();
     this.FilteredSources.initialize();
     this.FilteredFunctions.initialize();
     this.ChromeGlobals.initialize();
     this.StackFrames.initialize();
+    this.StackFramesClassicList.initialize();
     this.Sources.initialize();
     this.VariableBubble.initialize();
     this.WatchExpressions.initialize();
     this.EventListeners.initialize();
     this.GlobalSearch.initialize();
     this._initializeVariablesView();
     this._initializeEditor(deferred.resolve);
 
@@ -88,16 +87,17 @@ let DebuggerView = {
 
     this.Toolbar.destroy();
     this.Options.destroy();
     this.Filtering.destroy();
     this.FilteredSources.destroy();
     this.FilteredFunctions.destroy();
     this.ChromeGlobals.destroy();
     this.StackFrames.destroy();
+    this.StackFramesClassicList.destroy();
     this.Sources.destroy();
     this.VariableBubble.destroy();
     this.WatchExpressions.destroy();
     this.EventListeners.destroy();
     this.GlobalSearch.destroy();
     this._destroyPanes();
     this._destroyEditor(deferred.resolve);
 
@@ -526,17 +526,17 @@ let DebuggerView = {
    *        A function to invoke when the toggle finishes.
    */
   showInstrumentsPane: function(aCallback) {
     DebuggerView.toggleInstrumentsPane({
       visible: true,
       animated: true,
       delayed: true,
       callback: aCallback
-    }, 0);
+    });
   },
 
   /**
    * Handles a tab selection event on the instruments pane.
    */
   _onInstrumentsPaneTabSelect: function() {
     if (this._instrumentsPane.selectedTab.id == "events-tab") {
       DebuggerController.Breakpoints.DOM.scheduleEventListenersFetch();
--- a/browser/devtools/debugger/debugger.css
+++ b/browser/devtools/debugger/debugger.css
@@ -1,31 +1,30 @@
 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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/. */
 
-/* Sources search view */
+/* Side pane views */
 
-#globalsearch {
-  overflow: auto;
-}
-
-/* Instruments pane view (watch expressions, variables, events...) */
-
+#sources-pane > tabpanels > tabpanel,
 #instruments-pane > tabpanels > tabpanel {
   -moz-box-orient: vertical;
 }
 
 #expressions {
   overflow-x: hidden;
   overflow-y: auto;
 }
 
+#globalsearch {
+  overflow: auto;
+}
+
 /* Toolbar controls */
 
 .devtools-toolbarbutton:not([label]) > .toolbarbutton-text {
   display: none;
 }
 
 /* Horizontal vs. vertical layout */
 
--- a/browser/devtools/debugger/debugger.xul
+++ b/browser/devtools/debugger/debugger.xul
@@ -316,33 +316,45 @@
                      class="devtools-option-toolbarbutton"
                      tooltiptext="&debuggerUI.optsButton.tooltip;"
                      popup="debuggerPrefsContextMenu"
                      tabindex="0"/>
     </toolbar>
     <scrollbox id="globalsearch" orient="vertical" hidden="true"/>
     <splitter class="devtools-horizontal-splitter" hidden="true"/>
     <hbox id="debugger-widgets" flex="1">
-      <vbox id="sources-pane">
-        <vbox id="sources" flex="1"/>
-        <toolbar id="sources-toolbar" class="devtools-toolbar">
-          <hbox id="sources-controls">
-            <toolbarbutton id="black-box"
-                           tooltiptext="&debuggerUI.sources.blackBoxTooltip;"
-                           command="blackBoxCommand"
-                           class="devtools-toolbarbutton"/>
-            <toolbarbutton id="pretty-print"
-                           label="{}"
-                           tooltiptext="&debuggerUI.sources.prettyPrint;"
-                           class="devtools-toolbarbutton devtools-monospace"
-                           command="prettyPrintCommand"
-                           hidden="true"/>
-          </hbox>
-        </toolbar>
-      </vbox>
+      <tabbox id="sources-pane"
+              class="devtools-sidebar-tabs">
+        <tabs>
+          <tab id="sources-tab" label="&debuggerUI.tabs.sources;"/>
+          <tab id="callstack-tab" label="&debuggerUI.tabs.callstack;"/>
+        </tabs>
+        <tabpanels flex="1">
+          <tabpanel id="sources-tabpanel">
+            <vbox id="sources" flex="1"/>
+            <toolbar id="sources-toolbar" class="devtools-toolbar">
+              <hbox id="sources-controls">
+                <toolbarbutton id="black-box"
+                               tooltiptext="&debuggerUI.sources.blackBoxTooltip;"
+                               command="blackBoxCommand"
+                               class="devtools-toolbarbutton"/>
+                <toolbarbutton id="pretty-print"
+                               label="{}"
+                               tooltiptext="&debuggerUI.sources.prettyPrint;"
+                               class="devtools-toolbarbutton devtools-monospace"
+                               command="prettyPrintCommand"
+                               hidden="true"/>
+              </hbox>
+            </toolbar>
+          </tabpanel>
+          <tabpanel id="callstack-tabpanel">
+            <vbox id="callstack-list" flex="1"/>
+          </tabpanel>
+        </tabpanels>
+      </tabbox>
       <splitter id="sources-and-editor-splitter"
                 class="devtools-side-splitter"/>
       <deck id="editor-deck" flex="4">
         <vbox id="editor"/>
         <vbox id="black-boxed-message" align="center" pack="center">
           <label id="black-boxed-message-label">
             &debuggerUI.blackBoxMessage.label;
           </label>
@@ -362,22 +374,22 @@
       <tabbox id="instruments-pane"
               class="devtools-sidebar-tabs"
               hidden="true">
         <tabs>
           <tab id="variables-tab" label="&debuggerUI.tabs.variables;"/>
           <tab id="events-tab" label="&debuggerUI.tabs.events;"/>
         </tabs>
         <tabpanels flex="1">
-          <tabpanel id="variables-tabpanel">
+          <tabpanel id="variables-tabpanel" class="theme-body">
             <vbox id="expressions"/>
             <splitter class="devtools-horizontal-splitter"/>
             <vbox id="variables" flex="1"/>
           </tabpanel>
-          <tabpanel id="events-tabpanel">
+          <tabpanel id="events-tabpanel" class="theme-body">
             <vbox id="event-listeners" flex="1"/>
           </tabpanel>
         </tabpanels>
       </tabbox>
       <splitter id="vertical-layout-splitter"
                 class="devtools-horizontal-splitter"/>
       <hbox id="vertical-layout-panes-container">
         <splitter id="sources-and-instruments-splitter"
--- a/browser/devtools/debugger/test/browser_dbg_breadcrumbs-access.js
+++ b/browser/devtools/debugger/test/browser_dbg_breadcrumbs-access.js
@@ -43,44 +43,50 @@ function test() {
 
   function focusCurrentStackFrame() {
     EventUtils.sendMouseEvent({ type: "mousedown" },
       gFrames.selectedItem.target,
       gDebugger);
   }
 
   function checkNavigationWhileFocused() {
-    let deferred = promise.defer();
+    return Task.spawn(function() {
+      yield promise.all([
+        waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES),
+        EventUtils.sendKey("UP", gDebugger)
+      ]);
+      checkState({ frame: 2, source: 1, line: 6 });
 
-    EventUtils.sendKey("UP", gDebugger);
-    checkState({ frame: 2, source: 1, line: 6 });
-
-    waitForSourceAndCaret(gPanel, "-01.js", 5).then(() => {
+      yield promise.all([
+        waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES),
+        waitForSourceAndCaret(gPanel, "-01.js", 5),
+        EventUtils.sendKey("UP", gDebugger)
+      ]);
       checkState({ frame: 1, source: 0, line: 5 });
 
-      EventUtils.sendKey("UP", gDebugger);
+      yield promise.all([
+        waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES),
+        EventUtils.sendKey("UP", gDebugger)
+      ]);
       checkState({ frame: 0, source: 0, line: 5 });
 
-      waitForSourceAndCaret(gPanel, "-02.js", 6).then(() => {
-        checkState({ frame: 3, source: 1, line: 6 });
+      yield promise.all([
+        waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES),
+        waitForSourceAndCaret(gPanel, "-02.js", 6),
+        EventUtils.sendKey("END", gDebugger)
+      ]);
+      checkState({ frame: 3, source: 1, line: 6 });
 
-        waitForSourceAndCaret(gPanel, "-01.js", 5).then(() => {
-          checkState({ frame: 0, source: 0, line: 5 });
-          deferred.resolve();
-        });
-
+      yield promise.all([
+        waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES),
+        waitForSourceAndCaret(gPanel, "-01.js", 5),
         EventUtils.sendKey("HOME", gDebugger)
-      });
-
-      EventUtils.sendKey("END", gDebugger)
+      ]);
+      checkState({ frame: 0, source: 0, line: 5 });
     });
-
-    EventUtils.sendKey("UP", gDebugger)
-
-    return deferred.promise;
   }
 
   function checkState({ frame, source, line }) {
     is(gFrames.selectedIndex, frame,
       "The currently selected stackframe is incorrect.");
     is(gSources.selectedIndex, source,
       "The currently selected source is incorrect.");
     ok(isCaretPos(gPanel, line),
--- a/browser/devtools/debugger/test/browser_dbg_location-changes-01-simple.js
+++ b/browser/devtools/debugger/test/browser_dbg_location-changes-01-simple.js
@@ -41,17 +41,17 @@ function performTest() {
     "There should be a selected source value.");
   isnot(gEditor.getText().length, 0,
     "The source editor should have some text displayed.");
   isnot(gEditor.getText(), gDebugger.L10N.getStr("loadingText"),
     "The source editor text should not be 'Loading...'");
 
   is(gSources.widget.getAttribute("label"), "doc_recursion-stack.html",
     "The sources widget should have a correct label attribute.");
-  is(gSources.widget.getAttribute("tooltiptext"), "example.com test",
+  is(gSources.widget.getAttribute("tooltiptext"), "http://example.com",
     "The sources widget should have a correct tooltip text attribute.");
 
   is(gDebugger.document.querySelectorAll("#sources .side-menu-widget-empty-notice-container").length, 0,
     "The sources widget should not display any notice at this point (1).");
   is(gDebugger.document.querySelectorAll("#sources .side-menu-widget-empty-notice").length, 0,
     "The sources widget should not display any notice at this point (2).");
   is(gDebugger.document.querySelector("#sources .side-menu-widget-empty-notice > label"), null,
     "The sources widget should not display a notice at this point (3).");
--- a/browser/devtools/debugger/test/browser_dbg_location-changes-03-new.js
+++ b/browser/devtools/debugger/test/browser_dbg_location-changes-03-new.js
@@ -43,17 +43,17 @@ function testLocationChange() {
       "There should be a selected source value.");
     isnot(gEditor.getText().length, 0,
       "The source editor should have some text displayed.");
     is(gEditor.getText(), gDebugger.L10N.getStr("loadingText"),
       "The source editor text should not be 'Loading...'");
 
     is(gSources.widget.getAttribute("label"), "doc_inline-debugger-statement.html",
       "The sources widget should have a correct label attribute.");
-    is(gSources.widget.getAttribute("tooltiptext"), "example.com test",
+    is(gSources.widget.getAttribute("tooltiptext"), "http://example.com",
       "The sources widget should have a correct tooltip text attribute.");
 
     is(gDebugger.document.querySelectorAll("#sources .side-menu-widget-empty-notice-container").length, 0,
       "The sources widget should not display any notice at this point (1).");
     is(gDebugger.document.querySelectorAll("#sources .side-menu-widget-empty-notice").length, 0,
       "The sources widget should not display any notice at this point (2).");
     is(gDebugger.document.querySelector("#sources .side-menu-widget-empty-notice > label"), null,
       "The sources widget should not display a notice at this point (3).");
--- a/browser/devtools/debugger/test/browser_dbg_stack-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_stack-01.js
@@ -3,39 +3,43 @@
 
 /**
  * Test that stackframes are added when debugger is paused.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
 
 let gTab, gDebuggee, gPanel, gDebugger;
-let gFrames;
+let gFrames, gClassicFrames;
 
 function test() {
   initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gFrames = gDebugger.DebuggerView.StackFrames;
+    gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList;
 
     waitForSourceAndCaretAndScopes(gPanel, ".html", 14).then(performTest);
     gDebuggee.simpleCall();
   });
 }
 
 function performTest() {
   is(gDebugger.gThreadClient.state, "paused",
     "Should only be getting stack frames while paused.");
   is(gFrames.itemCount, 1,
     "Should have only one frame.");
+  is(gClassicFrames.itemCount, 1,
+    "Should also have only one frame in the mirrored view.");
 
   resumeDebuggerThenCloseAndFinish(gPanel);
 }
 
 registerCleanupFunction(function() {
   gTab = null;
   gDebuggee = null;
   gPanel = null;
   gDebugger = null;
   gFrames = null;
+  gClassicFrames = null;
 });
--- a/browser/devtools/debugger/test/browser_dbg_stack-02.js
+++ b/browser/devtools/debugger/test/browser_dbg_stack-02.js
@@ -3,81 +3,109 @@
 
 /**
  * Test that stackframes are added when debugger is paused in eval calls.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
 
 let gTab, gDebuggee, gPanel, gDebugger;
-let gFrames;
+let gFrames, gClassicFrames;
 
 function test() {
   initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gFrames = gDebugger.DebuggerView.StackFrames;
+    gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList;
 
     waitForSourceAndCaretAndScopes(gPanel, ".html", 18).then(performTest);
     gDebuggee.evalCall();
   });
 }
 
 function performTest() {
   is(gDebugger.gThreadClient.state, "paused",
     "Should only be getting stack frames while paused.");
   is(gFrames.itemCount, 2,
     "Should have two frames.");
+  is(gClassicFrames.itemCount, 2,
+    "Should also have only two in the mirrored view.");
 
   is(gFrames.getItemAtIndex(0).value,
     "evalCall", "Oldest frame name should be correct.");
   is(gFrames.getItemAtIndex(0).description,
     TAB_URL, "Oldest frame url should be correct.");
+  is(gClassicFrames.getItemAtIndex(0).value,
+    TAB_URL, "Oldest frame name is mirrored correctly.");
 
   is(gFrames.getItemAtIndex(1).value,
     "(eval)", "Newest frame name should be correct.");
   is(gFrames.getItemAtIndex(1).description,
     TAB_URL, "Newest frame url should be correct.");
+  is(gClassicFrames.getItemAtIndex(1).value,
+    TAB_URL, "Newest frame name is mirrored correctly.");
 
   is(gFrames.selectedIndex, 1,
     "Newest frame should be selected by default.");
+  is(gClassicFrames.selectedIndex, 0,
+    "Newest frame should be selected by default in the mirrored view.");
+
   isnot(gFrames.selectedIndex, 0,
     "Oldest frame should not be selected.");
+  isnot(gClassicFrames.selectedIndex, 1,
+    "Oldest frame should not be selected in the mirrored view.");
 
   EventUtils.sendMouseEvent({ type: "mousedown" },
     gFrames.getItemAtIndex(0).target,
     gDebugger);
 
   isnot(gFrames.selectedIndex, 1,
     "Newest frame should not be selected after click.");
+  isnot(gClassicFrames.selectedIndex, 0,
+    "Newest frame in the mirrored view should not be selected.");
+
   is(gFrames.selectedIndex, 0,
     "Oldest frame should be selected after click.");
+  is(gClassicFrames.selectedIndex, 1,
+    "Oldest frame in the mirrored view should be selected.");
 
   EventUtils.sendMouseEvent({ type: "mousedown" },
     gFrames.getItemAtIndex(1).target.querySelector(".dbg-stackframe-title"),
     gDebugger);
 
   is(gFrames.selectedIndex, 1,
     "Newest frame should be selected after click inside the newest frame.");
+  is(gClassicFrames.selectedIndex, 0,
+    "Newest frame in the mirrored view should be selected.");
+
   isnot(gFrames.selectedIndex, 0,
     "Oldest frame should not be selected after click inside the newest frame.");
+  isnot(gClassicFrames.selectedIndex, 1,
+    "Oldest frame in the mirrored view should not be selected.");
 
   EventUtils.sendMouseEvent({ type: "mousedown" },
     gFrames.getItemAtIndex(0).target.querySelector(".dbg-stackframe-details"),
     gDebugger);
 
   isnot(gFrames.selectedIndex, 1,
     "Newest frame should not be selected after click inside the oldest frame.");
+  isnot(gClassicFrames.selectedIndex, 0,
+    "Newest frame in the mirrored view should not be selected.");
+
   is(gFrames.selectedIndex, 0,
     "Oldest frame should be selected after click inside the oldest frame.");
+  is(gClassicFrames.selectedIndex, 1,
+    "Oldest frame in the mirrored view should be selected.");
 
   resumeDebuggerThenCloseAndFinish(gPanel);
 }
 
 registerCleanupFunction(function() {
   gTab = null;
   gDebuggee = null;
   gPanel = null;
   gDebugger = null;
   gFrames = null;
+  gClassicFrames = null;
 });
--- a/browser/devtools/debugger/test/browser_dbg_stack-03.js
+++ b/browser/devtools/debugger/test/browser_dbg_stack-03.js
@@ -3,46 +3,53 @@
 
 /**
  * Test that stackframes are scrollable.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
 
 let gTab, gDebuggee, gPanel, gDebugger;
-let gFrames, gFramesScrollingInterval;
+let gFrames, gClassicFrames, gFramesScrollingInterval;
 
 function test() {
   initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gFrames = gDebugger.DebuggerView.StackFrames;
+    gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList;
 
     waitForSourceAndCaretAndScopes(gPanel, ".html", 26).then(performTest);
 
     gDebuggee.gRecurseLimit = (gDebugger.gCallStackPageSize * 2) + 1;
     gDebuggee.recurse();
   });
 }
 
 function performTest() {
   is(gDebugger.gThreadClient.state, "paused",
     "Should only be getting stack frames while paused.");
   is(gFrames.itemCount, gDebugger.gCallStackPageSize,
     "Should have only the max limit of frames.");
+  is(gClassicFrames.itemCount, gDebugger.gCallStackPageSize,
+    "Should have only the max limit of frames in the mirrored view as well.")
 
   gDebugger.gThreadClient.addOneTimeListener("framesadded", () => {
     is(gFrames.itemCount, gDebugger.gCallStackPageSize * 2,
       "Should now have twice the max limit of frames.");
+    is(gClassicFrames.itemCount, gDebugger.gCallStackPageSize * 2,
+      "Should now have twice the max limit of frames in the mirrored view as well.");
 
     gDebugger.gThreadClient.addOneTimeListener("framesadded", () => {
       is(gFrames.itemCount, gDebuggee.gRecurseLimit,
         "Should have reached the recurse limit.");
+      is(gClassicFrames.itemCount, gDebuggee.gRecurseLimit,
+        "Should have reached the recurse limit in the mirrored view as well.");
 
       gDebugger.gThreadClient.resume(() => {
         window.clearInterval(gFramesScrollingInterval);
         closeDebuggerAndFinish(gPanel);
       });
     });
   });
 
@@ -55,9 +62,10 @@ registerCleanupFunction(function() {
   window.clearInterval(gFramesScrollingInterval);
   gFramesScrollingInterval = null;
 
   gTab = null;
   gDebuggee = null;
   gPanel = null;
   gDebugger = null;
   gFrames = null;
+  gClassicFrames = null;
 });
--- a/browser/devtools/debugger/test/browser_dbg_stack-04.js
+++ b/browser/devtools/debugger/test/browser_dbg_stack-04.js
@@ -3,46 +3,52 @@
 
 /**
  * Test that stackframes are cleared after resume.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
 
 let gTab, gDebuggee, gPanel, gDebugger;
-let gFrames;
+let gFrames, gClassicFrames;
 
 function test() {
   initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gFrames = gDebugger.DebuggerView.StackFrames;
+    gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList;
 
     waitForSourceAndCaretAndScopes(gPanel, ".html", 18).then(performTest);
     gDebuggee.evalCall();
   });
 }
 
 function performTest() {
   is(gDebugger.gThreadClient.state, "paused",
     "Should only be getting stack frames while paused.");
   is(gFrames.itemCount, 2,
     "Should have two frames.");
+  is(gClassicFrames.itemCount, 2,
+    "Should also have two frames in the mirrored view.");
 
   gDebugger.once(gDebugger.EVENTS.AFTER_FRAMES_CLEARED, () => {
     is(gFrames.itemCount, 0,
       "Should have no frames after resume.");
+    is(gClassicFrames.itemCount, 0,
+      "Should also have no frames in the mirrored view after resume.");
 
     closeDebuggerAndFinish(gPanel);
   }, true);
 
   gDebugger.gThreadClient.resume();
 }
 
 registerCleanupFunction(function() {
   gTab = null;
   gDebuggee = null;
   gPanel = null;
   gDebugger = null;
   gFrames = null;
+  gClassicFrames = null;
 });
--- a/browser/devtools/debugger/test/browser_dbg_stack-05.js
+++ b/browser/devtools/debugger/test/browser_dbg_stack-05.js
@@ -4,27 +4,28 @@
 /**
  * Test that switching between stack frames properly sets the current debugger
  * location in the source editor.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 let gTab, gDebuggee, gPanel, gDebugger;
-let gEditor, gSources, gFrames;
+let gEditor, gSources, gFrames, gClassicFrames;
 
 function test() {
   initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gEditor = gDebugger.DebuggerView.editor;
     gSources = gDebugger.DebuggerView.Sources;
     gFrames = gDebugger.DebuggerView.StackFrames;
+    gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList;
 
     waitForSourceAndCaretAndScopes(gPanel, "-02.js", 6)
       .then(initialChecks)
       .then(testNewestTwoFrames)
       .then(testOldestTwoFrames)
       .then(testAfterResume)
       .then(() => closeDebuggerAndFinish(gPanel))
       .then(null, aError => {
@@ -35,39 +36,45 @@ function test() {
   });
 }
 
 function initialChecks() {
   is(gDebugger.gThreadClient.state, "paused",
     "Should only be getting stack frames while paused.");
   is(gFrames.itemCount, 4,
     "Should have four frames.");
+  is(gClassicFrames.itemCount, 4,
+    "Should also have four frames in the mirrored view.");
 }
 
 function testNewestTwoFrames() {
   let deferred = promise.defer();
 
   is(gFrames.selectedIndex, 3,
     "Newest frame should be selected by default.");
+  is(gClassicFrames.selectedIndex, 0,
+    "Newest frame should be selected in the mirrored view as well.");
   is(gSources.selectedIndex, 1,
     "The second source is selected in the widget.");
   ok(isCaretPos(gPanel, 6),
     "Editor caret location is correct.");
 
   // The editor's debug location takes a tick to update.
   executeSoon(() => {
     is(gEditor.getDebugLocation(), 5,
       "Editor debug location is correct.");
 
     EventUtils.sendMouseEvent({ type: "mousedown" },
       gFrames.getItemAtIndex(2).target,
       gDebugger);
 
     is(gFrames.selectedIndex, 2,
       "Third frame should be selected after click.");
+    is(gClassicFrames.selectedIndex, 1,
+      "Third frame should be selected in the mirrored view as well.");
     is(gSources.selectedIndex, 1,
       "The second source is still selected in the widget.");
     ok(isCaretPos(gPanel, 6),
       "Editor caret location is correct.");
 
     // The editor's debug location takes a tick to update.
     executeSoon(() => {
       is(gEditor.getDebugLocation(), 5,
@@ -78,63 +85,69 @@ function testNewestTwoFrames() {
   });
 
   return deferred.promise;
 }
 
 function testOldestTwoFrames() {
   let deferred = promise.defer();
 
-  waitForSourceAndCaret(gPanel, "-01.js", 5).then(() => {
+  waitForSourceAndCaret(gPanel, "-01.js", 5).then(waitForTick).then(() => {
     is(gFrames.selectedIndex, 1,
       "Second frame should be selected after click.");
+    is(gClassicFrames.selectedIndex, 2,
+      "Second frame should be selected in the mirrored view as well.");
     is(gSources.selectedIndex, 0,
       "The first source is now selected in the widget.");
     ok(isCaretPos(gPanel, 5),
       "Editor caret location is correct.");
 
     // The editor's debug location takes a tick to update.
     executeSoon(() => {
       is(gEditor.getDebugLocation(), 4,
         "Editor debug location is correct.");
 
       EventUtils.sendMouseEvent({ type: "mousedown" },
         gFrames.getItemAtIndex(0).target,
         gDebugger);
 
       is(gFrames.selectedIndex, 0,
         "Oldest frame should be selected after click.");
+      is(gClassicFrames.selectedIndex, 3,
+        "Oldest frame should be selected in the mirrored view as well.");
       is(gSources.selectedIndex, 0,
         "The first source is still selected in the widget.");
       ok(isCaretPos(gPanel, 5),
         "Editor caret location is correct.");
 
       // The editor's debug location takes a tick to update.
       executeSoon(() => {
         is(gEditor.getDebugLocation(), 4,
           "Editor debug location is correct.");
 
         deferred.resolve();
       });
     });
   });
 
   EventUtils.sendMouseEvent({ type: "mousedown" },
-    gFrames.getItemAtIndex(1).target,
+    gDebugger.document.querySelector("#stackframe-2"),
     gDebugger);
 
   return deferred.promise;
 }
 
 function testAfterResume() {
   let deferred = promise.defer();
 
   gDebugger.once(gDebugger.EVENTS.AFTER_FRAMES_CLEARED, () => {
     is(gFrames.itemCount, 0,
       "Should have no frames after resume.");
+    is(gClassicFrames.itemCount, 0,
+      "Should have no frames in the mirrored view as well.");
     ok(isCaretPos(gPanel, 5),
       "Editor caret location is correct after resume.");
     is(gEditor.getDebugLocation(), null,
       "Editor debug location is correct after resume.");
 
     deferred.resolve();
   }, true);
 
@@ -145,10 +158,11 @@ function testAfterResume() {
 
 registerCleanupFunction(function() {
   gTab = null;
   gDebuggee = null;
   gPanel = null;
   gDebugger = null;
   gEditor = null;
   gFrames = null;
+  gClassicFrames = null;
 });
 
--- a/browser/devtools/debugger/test/browser_dbg_stack-06.js
+++ b/browser/devtools/debugger/test/browser_dbg_stack-06.js
@@ -4,62 +4,85 @@
 /**
  * Make sure that selecting a stack frame loads the right source in the editor
  * pane and highlights the proper line.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 let gTab, gDebuggee, gPanel, gDebugger;
-let gEditor, gSources, gFrames;
+let gEditor, gSources, gFrames, gClassicFrames;
 
 function test() {
   initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gEditor = gDebugger.DebuggerView.editor;
     gSources = gDebugger.DebuggerView.Sources;
     gFrames = gDebugger.DebuggerView.StackFrames;
+    gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList;
 
     waitForSourceAndCaretAndScopes(gPanel, "-02.js", 6).then(performTest);
     gDebuggee.firstCall();
   });
 }
 
 function performTest() {
   is(gFrames.selectedIndex, 3,
     "Newest frame should be selected by default.");
+  is(gClassicFrames.selectedIndex, 0,
+    "Newest frame should also be selected in the mirrored view.");
   is(gSources.selectedIndex, 1,
     "The second source is selected in the widget.");
   is(gEditor.getText().search(/firstCall/), -1,
     "The first source is not displayed.");
   is(gEditor.getText().search(/debugger/), 172,
-    "The second source is displayed.")
+    "The second source is displayed.");
 
-  waitForSourceAndCaret(gPanel, "-01.js", 6).then(() => {
+  waitForSourceAndCaret(gPanel, "-01.js", 6).then(waitForTick).then(() => {
     is(gFrames.selectedIndex, 0,
       "Oldest frame should be selected after click.");
+    is(gClassicFrames.selectedIndex, 3,
+      "Oldest frame should also be selected in the mirrored view.");
     is(gSources.selectedIndex, 0,
       "The first source is now selected in the widget.");
     is(gEditor.getText().search(/firstCall/), 118,
       "The first source is displayed.");
     is(gEditor.getText().search(/debugger/), -1,
       "The second source is not displayed.");
 
-    resumeDebuggerThenCloseAndFinish(gPanel);
+    waitForSourceAndCaret(gPanel, "-02.js", 6).then(waitForTick).then(() => {
+      is(gFrames.selectedIndex, 3,
+        "Newest frame should be selected again after click.");
+      is(gClassicFrames.selectedIndex, 0,
+        "Newest frame should also be selected again in the mirrored view.");
+      is(gSources.selectedIndex, 1,
+        "The second source is selected in the widget.");
+      is(gEditor.getText().search(/firstCall/), -1,
+        "The first source is not displayed.");
+      is(gEditor.getText().search(/debugger/), 172,
+        "The second source is displayed.");
+
+      resumeDebuggerThenCloseAndFinish(gPanel);
+    });
+
+    EventUtils.sendMouseEvent({ type: "mousedown" },
+      gDebugger.document.querySelector("#classic-stackframe-0"),
+      gDebugger);
   });
 
   EventUtils.sendMouseEvent({ type: "mousedown" },
     gDebugger.document.querySelector("#stackframe-3"),
     gDebugger);
 }
 
 registerCleanupFunction(function() {
   gTab = null;
   gDebuggee = null;
   gPanel = null;
   gDebugger = null;
   gEditor = null;
   gSources = null;
   gFrames = null;
+  gClassicFrames = null;
 });
--- a/browser/devtools/debugger/test/browser_dbg_stack-07.js
+++ b/browser/devtools/debugger/test/browser_dbg_stack-07.js
@@ -5,27 +5,28 @@
  * Make sure that after selecting a different stack frame, resuming reselects
  * the topmost stackframe, loads the right source in the editor pane and
  * highlights the proper line.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 let gTab, gDebuggee, gPanel, gDebugger;
-let gEditor, gSources, gFrames, gToolbar;
+let gEditor, gSources, gFrames, gClassicFrames, gToolbar;
 
 function test() {
   initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gEditor = gDebugger.DebuggerView.editor;
     gSources = gDebugger.DebuggerView.Sources;
     gFrames = gDebugger.DebuggerView.StackFrames;
+    gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList;
     gToolbar = gDebugger.DebuggerView.Toolbar;
 
     waitForSourceAndCaretAndScopes(gPanel, "-02.js", 6).then(performTest);
     gDebuggee.firstCall();
   });
 }
 
 function performTest() {
@@ -48,23 +49,25 @@ function performTest() {
     yield performStep("StepOut");
     testTopFrame(2);
 
     yield resumeDebuggerThenCloseAndFinish(gPanel);
   });
 
   function selectBottomFrame() {
     let updated = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES);
-    gFrames.selectedIndex = 0;
+    gClassicFrames.selectedIndex = gClassicFrames.itemCount - 1;
     return updated.then(waitForTick);
   }
 
   function testBottomFrame(debugLocation) {
     is(gFrames.selectedIndex, 0,
       "Oldest frame should be selected after click.");
+    is(gClassicFrames.selectedIndex, gFrames.itemCount - 1,
+      "Oldest frame should also be selected in the mirrored view.");
     is(gSources.selectedIndex, 0,
       "The first source is now selected in the widget.");
     is(gEditor.getText().search(/firstCall/), 118,
       "The first source is displayed.");
     is(gEditor.getText().search(/debugger/), -1,
       "The second source is not displayed.");
 
     is(gEditor.getDebugLocation(), debugLocation,
@@ -77,16 +80,18 @@ function performTest() {
     let updated = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES);
     gToolbar["_on" + type + "Pressed"]();
     return updated.then(waitForTick);
   }
 
   function testTopFrame(frameIndex) {
     is(gFrames.selectedIndex, frameIndex,
       "Topmost frame should be selected after click.");
+    is(gClassicFrames.selectedIndex, gFrames.itemCount - frameIndex - 1,
+      "Topmost frame should also be selected in the mirrored view.");
     is(gSources.selectedIndex, 1,
       "The second source is now selected in the widget.");
     is(gEditor.getText().search(/firstCall/), -1,
       "The second source is displayed.");
     is(gEditor.getText().search(/debugger/), 172,
       "The first source is not displayed.");
   }
 }
@@ -94,10 +99,11 @@ function performTest() {
 registerCleanupFunction(function() {
   gTab = null;
   gDebuggee = null;
   gPanel = null;
   gDebugger = null;
   gEditor = null;
   gSources = null;
   gFrames = null;
+  gClassicFrames = null;
   gToolbar = null;
 });
--- a/browser/devtools/shadereditor/panel.js
+++ b/browser/devtools/shadereditor/panel.js
@@ -1,17 +1,17 @@
 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const { Cc, Ci, Cu, Cr } = require("chrome");
-const promise = require("sdk/core/promise");
+const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
 const EventEmitter = require("devtools/shared/event-emitter");
 const { WebGLFront } = require("devtools/server/actors/webgl");
 
 function ShaderEditorPanel(iframeWindow, toolbox) {
   this.panelWin = iframeWindow;
   this._toolbox = toolbox;
   this._destroyer = null;
 
--- a/browser/devtools/shadereditor/shadereditor.js
+++ b/browser/devtools/shadereditor/shadereditor.js
@@ -8,32 +8,38 @@ const { classes: Cc, interfaces: Ci, uti
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/devtools/Loader.jsm");
 Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 
 const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
-const promise = require("sdk/core/promise");
+const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
 const EventEmitter = require("devtools/shared/event-emitter");
 const {Tooltip} = require("devtools/shared/widgets/Tooltip");
 const Editor = require("devtools/sourceeditor/editor");
 
 // The panel's window global is an EventEmitter firing the following events:
 const EVENTS = {
   // When new programs are received from the server.
   NEW_PROGRAM: "ShaderEditor:NewProgram",
   PROGRAMS_ADDED: "ShaderEditor:ProgramsAdded",
 
   // When the vertex and fragment sources were shown in the editor.
   SOURCES_SHOWN: "ShaderEditor:SourcesShown",
 
   // When a shader's source was edited and compiled via the editor.
-  SHADER_COMPILED: "ShaderEditor:ShaderCompiled"
+  SHADER_COMPILED: "ShaderEditor:ShaderCompiled",
+
+  // When the UI is reset from tab navigation
+  UI_RESET: "ShaderEditor:UIReset",
+
+  // When the editor's error markers are all removed
+  EDITOR_ERROR_MARKERS_REMOVED: "ShaderEditor:EditorCleaned"
 };
 
 const STRINGS_URI = "chrome://browser/locale/devtools/shadereditor.properties"
 const HIGHLIGHT_TINT = [1, 0, 0.25, 1]; // rgba
 const TYPING_MAX_DELAY = 500; // ms
 const SHADERS_AUTOGROW_ITEMS = 4;
 const GUTTER_ERROR_PANEL_OFFSET_X = 7; // px
 const GUTTER_ERROR_PANEL_DELAY = 100; // ms
@@ -109,25 +115,27 @@ let EventsHandler = {
   },
 
   /**
    * Called for each location change in the debugged tab.
    */
   _onTabNavigated: function(event) {
     switch (event) {
       case "will-navigate": {
-        // Make sure the backend is prepared to handle WebGL contexts.
-        gFront.setup({ reload: false });
+        Task.spawn(function() {
+          // Make sure the backend is prepared to handle WebGL contexts.
+          gFront.setup({ reload: false });
 
-        // Reset UI.
-        ShadersListView.empty();
-        ShadersEditorsView.setText({ vs: "", fs: "" });
-        $("#reload-notice").hidden = true;
-        $("#waiting-notice").hidden = false;
-        $("#content").hidden = true;
+          // Reset UI.
+          ShadersListView.empty();
+          $("#reload-notice").hidden = true;
+          $("#waiting-notice").hidden = false;
+          yield ShadersEditorsView.setText({ vs: "", fs: "" });
+          $("#content").hidden = true;
+        }).then(() => window.emit(EVENTS.UI_RESET));
         break;
       }
       case "navigate": {
         // Manually retrieve the list of program actors known to the server,
         // because the backend won't emit "program-linked" notifications
         // in the case of a bfcache navigation (since no new programs are
         // actually linked).
         gFront.getPrograms().then(this._onProgramsAdded);
@@ -267,23 +275,26 @@ let ShadersListView = Heritage.extend(Wi
     }
     function getSources([vertexShaderActor, fragmentShaderActor]) {
       return promise.all([
         vertexShaderActor.getText(),
         fragmentShaderActor.getText()
       ]);
     }
     function showSources([vertexShaderText, fragmentShaderText]) {
-      ShadersEditorsView.setText({
+      return ShadersEditorsView.setText({
         vs: vertexShaderText,
         fs: fragmentShaderText
       });
     }
 
-    getShaders().then(getSources).then(showSources).then(null, Cu.reportError);
+    getShaders()
+      .then(getSources)
+      .then(showSources)
+      .then(null, Cu.reportError);
   },
 
   /**
    * The check listener for the programs container.
    */
   _onProgramCheck: function({ detail: { checked }, target }) {
     let sourceItem = this.getItemForElement(target);
     let attachment = sourceItem.attachment;
@@ -346,41 +357,48 @@ let ShadersEditorsView = {
 
   /**
    * Sets the text displayed in the vertex and fragment shader editors.
    *
    * @param object sources
    *        An object containing the following properties
    *          - vs: the vertex shader source code
    *          - fs: the fragment shader source code
+   * @return object
+   *        A promise resolving upon completion of text setting.
    */
   setText: function(sources) {
+    let view = this;
     function setTextAndClearHistory(editor, text) {
       editor.setText(text);
       editor.clearHistory();
     }
 
-    this._toggleListeners("off");
-    this._getEditor("vs").then(e => setTextAndClearHistory(e, sources.vs));
-    this._getEditor("fs").then(e => setTextAndClearHistory(e, sources.fs));
-    this._toggleListeners("on");
-
-    window.emit(EVENTS.SOURCES_SHOWN, sources);
+    return Task.spawn(function() {
+      yield view._toggleListeners("off");
+      yield promise.all([
+        view._getEditor("vs").then(e => setTextAndClearHistory(e, sources.vs)),
+        view._getEditor("fs").then(e => setTextAndClearHistory(e, sources.fs))
+      ]);
+      yield view._toggleListeners("on");
+    }).then(() => window.emit(EVENTS.SOURCES_SHOWN, sources));
   },
 
   /**
    * Lazily initializes and returns a promise for an Editor instance.
    *
    * @param string type
    *        Specifies for which shader type should an editor be retrieved,
    *        either are "vs" for a vertex, or "fs" for a fragment shader.
+   * @return object
+   *        Returns a promise that resolves to an editor instance
    */
   _getEditor: function(type) {
     if ($("#content").hidden) {
-      return promise.reject(null);
+      return promise.reject(new Error("Shader Editor is still waiting for a WebGL context to be created."));
     }
     if (this._editorPromises.has(type)) {
       return this._editorPromises.get(type);
     }
 
     let deferred = promise.defer();
     this._editorPromises.set(type, deferred.promise);
 
@@ -394,24 +412,26 @@ let ShadersEditorsView = {
     return deferred.promise;
   },
 
   /**
    * Toggles all the event listeners for the editors either on or off.
    *
    * @param string flag
    *        Either "on" to enable the event listeners, "off" to disable them.
+   * @return object
+   *        A promise resolving upon completion of toggling the listeners.
    */
   _toggleListeners: function(flag) {
-    ["vs", "fs"].forEach(type => {
-      this._getEditor(type).then(editor => {
+    return promise.all(["vs", "fs"].map(type => {
+      return this._getEditor(type).then(editor => {
         editor[flag]("focus", this["_" + type + "Focused"]);
         editor[flag]("change", this["_" + type + "Changed"]);
       });
-    });
+    }));
   },
 
   /**
    * The focus listener for a source editor.
    *
    * @param string focused
    *        The corresponding shader type for the focused editor (e.g. "vs").
    * @param string focused
@@ -481,17 +501,17 @@ let ShadersEditorsView = {
       };
     }
     function discardInvalidMatches(e) {
       // Discard empty line and text matches.
       return e.lineMatch && e.textMatch;
     }
     function sanitizeValidMatches(e) {
       return {
-        // Drivers might yield retarded line numbers under some obscure
+        // Drivers might yield confusing line numbers under some obscure
         // circumstances. Don't throw the errors away in those cases,
         // just display them on the currently edited line.
         line: e.lineMatch[0] > lineCount ? currentLine : e.lineMatch[0] - 1,
         // Trim whitespace from the beginning and the end of the message,
         // and replace all other occurences of double spaces to a single space.
         text: e.textMatch[0].trim().replace(/\s{2,}/g, " ")
       };
     }
@@ -549,16 +569,17 @@ let ShadersEditorsView = {
   /**
    * Removes all the gutter markers and line classes from the editor.
    */
   _cleanEditor: function(type) {
     this._getEditor(type).then(editor => {
       editor.removeAllMarkers("errors");
       this._errors[type].forEach(e => editor.removeLineClass(e.line));
       this._errors[type].length = 0;
+      window.emit(EVENTS.EDITOR_ERROR_MARKERS_REMOVED);
     });
   },
 
   _errors: {
     vs: [],
     fs: []
   }
 };
--- a/browser/devtools/shadereditor/test/browser_se_bfcache.js
+++ b/browser/devtools/shadereditor/test/browser_se_bfcache.js
@@ -9,18 +9,17 @@ function ifWebGLSupported() {
   let [target, debuggee, panel] = yield initShaderEditor(SIMPLE_CANVAS_URL);
   let { gFront, $, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
 
   let reloaded = reload(target);
   let firstProgram = yield once(gFront, "program-linked");
   yield reloaded;
 
   let navigated = navigate(target, MULTIPLE_CONTEXTS_URL);
-  let secondProgram = yield once(gFront, "program-linked");
-  let thirdProgram = yield once(gFront, "program-linked");
+  let [secondProgram, thirdProgram] = yield getPrograms(gFront, 2);
   yield navigated;
 
   let vsEditor = yield ShadersEditorsView._getEditor("vs");
   let fsEditor = yield ShadersEditorsView._getEditor("fs");
 
   yield navigateInHistory(target, "back", "will-navigate");
   yield once(panel.panelWin, EVENTS.PROGRAMS_ADDED);
   yield once(panel.panelWin, EVENTS.SOURCES_SHOWN);
@@ -51,14 +50,8 @@ function ifWebGLSupported() {
   is(vsEditor.getText().indexOf("gl_Position"), 100,
     "The vertex shader editor contains the correct text.");
   is(fsEditor.getText().indexOf("gl_FragColor"), 89,
     "The fragment shader editor contains the correct text.");
 
   yield teardown(panel);
   finish();
 }
-
-function once(aTarget, aEvent) {
-  let deferred = promise.defer();
-  aTarget.once(aEvent, deferred.resolve);
-  return deferred.promise;
-}
--- a/browser/devtools/shadereditor/test/browser_se_editors-contents.js
+++ b/browser/devtools/shadereditor/test/browser_se_editors-contents.js
@@ -3,24 +3,28 @@
 
 /**
  * Tests if the editors contain the correct text when a program
  * becomes available.
  */
 
 function ifWebGLSupported() {
   let [target, debuggee, panel] = yield initShaderEditor(SIMPLE_CANVAS_URL);
-  let { gFront, ShadersEditorsView } = panel.panelWin;
+  let { gFront, ShadersEditorsView, EVENTS } = panel.panelWin;
 
   reload(target);
-  yield once(gFront, "program-linked");
+  yield promise.all([
+    once(gFront, "program-linked"),
+    once(panel.panelWin, EVENTS.SOURCES_SHOWN)
+  ]);
 
   let vsEditor = yield ShadersEditorsView._getEditor("vs");
   let fsEditor = yield ShadersEditorsView._getEditor("fs");
 
+
   is(vsEditor.getText().indexOf("gl_Position"), 170,
     "The vertex shader editor contains the correct text.");
   is(fsEditor.getText().indexOf("gl_FragColor"), 97,
     "The fragment shader editor contains the correct text.");
 
   yield teardown(panel);
   finish();
 }
--- a/browser/devtools/shadereditor/test/browser_se_editors-error-gutter.js
+++ b/browser/devtools/shadereditor/test/browser_se_editors-error-gutter.js
@@ -6,65 +6,71 @@
  * when there's a shader compilation error.
  */
 
 function ifWebGLSupported() {
   let [target, debuggee, panel] = yield initShaderEditor(SIMPLE_CANVAS_URL);
   let { gFront, EVENTS, ShadersEditorsView } = panel.panelWin;
 
   reload(target);
-  yield once(gFront, "program-linked");
+  yield promise.all([
+    once(gFront, "program-linked"),
+    once(panel.panelWin, EVENTS.SOURCES_SHOWN)
+  ]);
 
   let vsEditor = yield ShadersEditorsView._getEditor("vs");
   let fsEditor = yield ShadersEditorsView._getEditor("fs");
 
   vsEditor.replaceText("vec3", { line: 7, ch: 22 }, { line: 7, ch: 26 });
-  let vertError = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+  let [, vertError] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
   checkHasVertFirstError(true, vertError);
   checkHasVertSecondError(false, vertError);
   info("Error marks added in the vertex shader editor.");
 
   vsEditor.insertText(" ", { line: 1, ch: 0 });
+  yield once(panel.panelWin, EVENTS.EDITOR_ERROR_MARKERS_REMOVED);
   is(vsEditor.getText(1), "       precision lowp float;", "Typed space.");
   checkHasVertFirstError(false, vertError);
   checkHasVertSecondError(false, vertError);
   info("Error marks removed while typing in the vertex shader editor.");
 
-  let vertError = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+  [, vertError] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
   checkHasVertFirstError(true, vertError);
   checkHasVertSecondError(false, vertError);
   info("Error marks were re-added after recompiling the vertex shader.");
 
   fsEditor.replaceText("vec4", { line: 2, ch: 14 }, { line: 2, ch: 18 });
-  let fragError = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+  let [, fragError] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
   checkHasVertFirstError(true, vertError);
   checkHasVertSecondError(false, vertError);
   checkHasFragError(true, fragError);
   info("Error marks added in the fragment shader editor.");
 
   fsEditor.insertText(" ", { line: 1, ch: 0 });
+  yield once(panel.panelWin, EVENTS.EDITOR_ERROR_MARKERS_REMOVED);
   is(fsEditor.getText(1), "       precision lowp float;", "Typed space.");
   checkHasVertFirstError(true, vertError);
   checkHasVertSecondError(false, vertError);
   checkHasFragError(false, fragError);
   info("Error marks removed while typing in the fragment shader editor.");
 
-  let fragError = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+  [, fragError] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
   checkHasVertFirstError(true, vertError);
   checkHasVertSecondError(false, vertError);
   checkHasFragError(true, fragError);
   info("Error marks were re-added after recompiling the fragment shader.");
 
   vsEditor.replaceText("2", { line: 3, ch: 19 }, { line: 3, ch: 20 });
+  yield once(panel.panelWin, EVENTS.EDITOR_ERROR_MARKERS_REMOVED);
   checkHasVertFirstError(false, vertError);
   checkHasVertSecondError(false, vertError);
   checkHasFragError(true, fragError);
   info("Error marks removed while typing in the vertex shader editor again.");
 
-  let vertError = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+  [, vertError] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
   checkHasVertFirstError(true, vertError);
   checkHasVertSecondError(true, vertError);
   checkHasFragError(true, fragError);
   info("Error marks were re-added after recompiling the fragment shader again.");
 
   yield teardown(panel);
   finish();
 
@@ -143,14 +149,8 @@ function ifWebGLSupported() {
         "The correct line was parsed.");
       is(parsed[0].messages.length, 1,
         "There is 1 parsed message.");
       ok(parsed[0].messages[0].contains("'constructor' : too many arguments"),
         "The correct message was parsed.");
     }
   }
 }
-
-function once(aTarget, aEvent) {
-  let deferred = promise.defer();
-  aTarget.once(aEvent, (aName, aData) => deferred.resolve(aData));
-  return deferred.promise;
-}
--- a/browser/devtools/shadereditor/test/browser_se_editors-error-tooltip.js
+++ b/browser/devtools/shadereditor/test/browser_se_editors-error-tooltip.js
@@ -6,17 +6,20 @@
  * a shader compilation error.
  */
 
 function ifWebGLSupported() {
   let [target, debuggee, panel] = yield initShaderEditor(SIMPLE_CANVAS_URL);
   let { gFront, EVENTS, ShadersEditorsView } = panel.panelWin;
 
   reload(target);
-  yield once(gFront, "program-linked");
+  yield promise.all([
+    once(gFront, "program-linked"),
+    once(panel.panelWin, EVENTS.SOURCES_SHOWN)
+  ]);
 
   let vsEditor = yield ShadersEditorsView._getEditor("vs");
   let fsEditor = yield ShadersEditorsView._getEditor("fs");
 
   vsEditor.replaceText("vec3", { line: 7, ch: 22 }, { line: 7, ch: 26 });
   yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
 
   // Synthesizing 'mouseenter' events doesn't work, hack around this by
@@ -46,14 +49,8 @@ function ifWebGLSupported() {
   ok(messages[0].textContent.contains("'constructor' : too many arguments"),
     "The first message contains the correct text.");
   ok(messages[1].textContent.contains("'assign' : cannot convert"),
     "The second message contains the correct text.");
 
   yield teardown(panel);
   finish();
 }
-
-function once(aTarget, aEvent) {
-  let deferred = promise.defer();
-  aTarget.once(aEvent, (aName, aData) => deferred.resolve(aData));
-  return deferred.promise;
-}
--- a/browser/devtools/shadereditor/test/browser_se_navigation.js
+++ b/browser/devtools/shadereditor/test/browser_se_navigation.js
@@ -2,20 +2,23 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests target navigations are handled correctly in the UI.
  */
 
 function ifWebGLSupported() {
   let [target, debuggee, panel] = yield initShaderEditor(SIMPLE_CANVAS_URL);
-  let { gFront, $, ShadersListView, ShadersEditorsView } = panel.panelWin;
+  let { gFront, $, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
 
   reload(target);
-  yield once(gFront, "program-linked");
+  yield promise.all([
+    once(gFront, "program-linked"),
+    once(panel.panelWin, EVENTS.SOURCES_SHOWN)
+  ]);
 
   is($("#reload-notice").hidden, true,
     "The 'reload this page' notice should be hidden after linking.");
   is($("#waiting-notice").hidden, true,
     "The 'waiting for a WebGL context' notice should be visible after linking.");
   is($("#content").hidden, false,
     "The tool's content should not be hidden anymore.");
 
@@ -34,44 +37,43 @@ function ifWebGLSupported() {
   is(fsEditor.getText().indexOf("gl_FragColor"), 97,
     "The fragment shader editor contains the correct text.");
 
   let navigating = once(target, "will-navigate");
   let navigated = once(target, "will-navigate");
   navigate(target, "about:blank");
 
   yield navigating;
+  yield once(panel.panelWin, EVENTS.UI_RESET);
 
   is($("#reload-notice").hidden, true,
     "The 'reload this page' notice should be hidden while navigating.");
   is($("#waiting-notice").hidden, false,
     "The 'waiting for a WebGL context' notice should be visible while navigating.");
   is($("#content").hidden, true,
     "The tool's content should be hidden now that there's no WebGL content.");
 
   is(ShadersListView.itemCount, 0,
     "The shaders list should be empty.");
   is(ShadersListView.selectedItem, null,
     "The shaders list has no correct item.");
   is(ShadersListView.selectedIndex, -1,
     "The shaders list has a negative index.");
 
-  try {
-    yield ShadersEditorsView._getEditor("vs");
+  yield ShadersEditorsView._getEditor("vs").then(() => {
     ok(false, "The promise for a vertex shader editor should be rejected.");
-  } catch (e) {
+  }, () => {
     ok(true, "The vertex shader editors wasn't initialized.");
-  }
+  });
 
-  try {
-    yield ShadersEditorsView._getEditor("fs");
+  yield ShadersEditorsView._getEditor("fs").then(() => {
     ok(false, "The promise for a fragment shader editor should be rejected.");
-  } catch (e) {
+  }, () => {
     ok(true, "The fragment shader editors wasn't initialized.");
-  }
+  });
 
   yield navigated;
 
   is($("#reload-notice").hidden, true,
     "The 'reload this page' notice should still be hidden after navigating.");
   is($("#waiting-notice").hidden, false,
     "The 'waiting for a WebGL context' notice should still be visible after navigating.");
   is($("#content").hidden, true,
--- a/browser/devtools/shadereditor/test/browser_se_programs-blackbox-01.js
+++ b/browser/devtools/shadereditor/test/browser_se_programs-blackbox-01.js
@@ -9,18 +9,20 @@ function ifWebGLSupported() {
   let [target, debuggee, panel] = yield initShaderEditor(MULTIPLE_CONTEXTS_URL);
   let { gFront, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
 
   once(panel.panelWin, EVENTS.SHADER_COMPILED).then(() => {
     ok(false, "No shaders should be publicly compiled during this test.");
   });
 
   reload(target);
-  let firstProgramActor = yield once(gFront, "program-linked");
-  let secondProgramActor = yield once(gFront, "program-linked");
+  let [firstProgramActor, secondProgramActor] = yield promise.all([
+    getPrograms(gFront, 2),
+    once(panel.panelWin, EVENTS.SOURCES_SHOWN)
+  ]).then(([programs]) => programs);
 
   let vsEditor = yield ShadersEditorsView._getEditor("vs");
   let fsEditor = yield ShadersEditorsView._getEditor("fs");
 
   vsEditor.once("change", () => {
     ok(false, "The vertex shader source was unexpectedly changed.");
   });
   fsEditor.once("change", () => {
--- a/browser/devtools/shadereditor/test/browser_se_programs-blackbox-02.js
+++ b/browser/devtools/shadereditor/test/browser_se_programs-blackbox-02.js
@@ -6,18 +6,20 @@
  * overlapping geometry.
  */
 
 function ifWebGLSupported() {
   let [target, debuggee, panel] = yield initShaderEditor(BLENDED_GEOMETRY_CANVAS_URL);
   let { gFront, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
 
   reload(target);
-  let firstProgramActor = yield once(gFront, "program-linked");
-  let secondProgramActor = yield once(gFront, "program-linked");
+  let [firstProgramActor, secondProgramActor] = yield promise.all([
+    getPrograms(gFront, 2),
+    once(panel.panelWin, EVENTS.SOURCES_SHOWN)
+  ]).then(([programs]) => programs);
 
   yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 127, g: 127, b: 127, a: 255 }, true);
   yield ensurePixelIs(debuggee, { x: 64, y: 64 }, { r: 0, g: 127, b: 127, a: 127 }, true);
   ok(true, "The canvas was correctly drawn.");
 
   getBlackBoxCheckbox(panel, 0).click();
 
   yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
@@ -54,14 +56,8 @@ function ifWebGLSupported() {
   yield teardown(panel);
   finish();
 }
 
 function getBlackBoxCheckbox(aPanel, aIndex) {
   return aPanel.panelWin.document.querySelectorAll(
     ".side-menu-widget-item-checkbox")[aIndex];
 }
-
-function once(aTarget, aEvent) {
-  let deferred = promise.defer();
-  aTarget.once(aEvent, deferred.resolve);
-  return deferred.promise;
-}
--- a/browser/devtools/shadereditor/test/browser_se_programs-cache.js
+++ b/browser/devtools/shadereditor/test/browser_se_programs-cache.js
@@ -2,20 +2,24 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests that program and shader actors are cached in the frontend.
  */
 
 function ifWebGLSupported() {
   let [target, debuggee, panel] = yield initShaderEditor(MULTIPLE_CONTEXTS_URL);
-  let { gFront, ShadersListView, ShadersEditorsView } = panel.panelWin;
+  let { EVENTS, gFront, ShadersListView, ShadersEditorsView } = panel.panelWin;
 
   reload(target);
-  let programActor = yield once(gFront, "program-linked");
+  let [programActor] = yield promise.all([
+    getPrograms(gFront, 1),
+    once(panel.panelWin, EVENTS.SOURCES_SHOWN)
+  ]).then(([programs]) => programs);
+
   let programItem = ShadersListView.selectedItem;
 
   is(programItem.attachment.programActor, programActor,
     "The correct program actor is cached for the selected item.");
 
   is((yield programActor.getVertexShader()),
      (yield programItem.attachment.vs),
     "The cached vertex shader promise returns the correct actor.");
--- a/browser/devtools/shadereditor/test/browser_se_programs-highlight-01.js
+++ b/browser/devtools/shadereditor/test/browser_se_programs-highlight-01.js
@@ -9,18 +9,20 @@ function ifWebGLSupported() {
   let [target, debuggee, panel] = yield initShaderEditor(MULTIPLE_CONTEXTS_URL);
   let { gFront, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
 
   once(panel.panelWin, EVENTS.SHADER_COMPILED).then(() => {
     ok(false, "No shaders should be publicly compiled during this test.");
   });
 
   reload(target);
-  let firstProgramActor = yield once(gFront, "program-linked");
-  let secondProgramActor = yield once(gFront, "program-linked");
+  let [firstProgramActor, secondProgramActor] = yield promise.all([
+    getPrograms(gFront, 2),
+    once(panel.panelWin, EVENTS.SOURCES_SHOWN)
+  ]).then(([programs]) => programs);
 
   let vsEditor = yield ShadersEditorsView._getEditor("vs");
   let fsEditor = yield ShadersEditorsView._getEditor("fs");
 
   vsEditor.once("change", () => {
     ok(false, "The vertex shader source was unexpectedly changed.");
   });
   fsEditor.once("change", () => {
@@ -84,14 +86,8 @@ function getItemLabel(aPanel, aIndex) {
   return aPanel.panelWin.document.querySelectorAll(
     ".side-menu-widget-item-label")[aIndex];
 }
 
 function getBlackBoxCheckbox(aPanel, aIndex) {
   return aPanel.panelWin.document.querySelectorAll(
     ".side-menu-widget-item-checkbox")[aIndex];
 }
-
-function once(aTarget, aEvent) {
-  let deferred = promise.defer();
-  aTarget.once(aEvent, deferred.resolve);
-  return deferred.promise;
-}
--- a/browser/devtools/shadereditor/test/browser_se_programs-highlight-02.js
+++ b/browser/devtools/shadereditor/test/browser_se_programs-highlight-02.js
@@ -6,18 +6,20 @@
  * overlapping geometry.
  */
 
 function ifWebGLSupported() {
   let [target, debuggee, panel] = yield initShaderEditor(BLENDED_GEOMETRY_CANVAS_URL);
   let { gFront, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
 
   reload(target);
-  let firstProgramActor = yield once(gFront, "program-linked");
-  let secondProgramActor = yield once(gFront, "program-linked");
+  let [firstProgramActor, secondProgramActor] = yield promise.all([
+    getPrograms(gFront, 2),
+    once(panel.panelWin, EVENTS.SOURCES_SHOWN)
+  ]).then(([programs]) => programs);
 
   yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 127, g: 127, b: 127, a: 255 }, true);
   yield ensurePixelIs(debuggee, { x: 64, y: 64 }, { r: 0, g: 127, b: 127, a: 127 }, true);
   ok(true, "The canvas was correctly drawn.");
 
   ShadersListView._onProgramMouseEnter({ target: getItemLabel(panel, 0) });
 
   yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 127, g: 0, b: 32, a: 255 }, true);
@@ -40,14 +42,8 @@ function ifWebGLSupported() {
   yield teardown(panel);
   finish();
 }
 
 function getItemLabel(aPanel, aIndex) {
   return aPanel.panelWin.document.querySelectorAll(
     ".side-menu-widget-item-label")[aIndex];
 }
-
-function once(aTarget, aEvent) {
-  let deferred = promise.defer();
-  aTarget.once(aEvent, deferred.resolve);
-  return deferred.promise;
-}
--- a/browser/devtools/shadereditor/test/browser_se_programs-list.js
+++ b/browser/devtools/shadereditor/test/browser_se_programs-list.js
@@ -14,33 +14,27 @@ function ifWebGLSupported() {
     "The shaders list should initially be empty.");
   is(ShadersListView.selectedItem, null,
     "The shaders list has no selected item.");
   is(ShadersListView.selectedIndex, -1,
     "The shaders list has a negative index.");
 
   reload(target);
 
-  let firstProgramActor = yield once(gFront, "program-linked");
-
-  is(ShadersListView.itemCount, 1,
-    "The shaders list contains one entry.");
-  is(ShadersListView.selectedItem, ShadersListView.items[0],
-    "The shaders list has a correct item selected.");
-  is(ShadersListView.selectedIndex, 0,
-    "The shaders list has a correct index selected.");
-
-  let secondProgramActor = yield once(gFront, "program-linked");
-
-  is(ShadersListView.itemCount, 2,
-    "The shaders list contains two entries.");
-  is(ShadersListView.selectedItem, ShadersListView.items[0],
-    "The shaders list has a correct item selected.");
-  is(ShadersListView.selectedIndex, 0,
-    "The shaders list has a correct index selected.");
+  let [firstProgramActor, secondProgramActor] = yield promise.all([
+    getPrograms(gFront, 2, (actors) => {
+      // Fired upon each actor addition, we want to check only
+      // after the first actor has been added so we can test state
+      if (actors.length === 1)
+        checkFirstProgram();
+      if (actors.length === 2)
+        checkSecondProgram();
+    }),
+    once(panel.panelWin, EVENTS.SOURCES_SHOWN)
+  ]).then(([programs, ]) => programs);
 
   is(ShadersListView.labels[0], L10N.getFormatStr("shadersList.programLabel", 0),
     "The correct first label is shown in the shaders list.");
   is(ShadersListView.labels[1], L10N.getFormatStr("shadersList.programLabel", 1),
     "The correct second label is shown in the shaders list.");
 
   let vertexShader = yield firstProgramActor.getVertexShader();
   let fragmentShader = yield firstProgramActor.getFragmentShader();
@@ -68,15 +62,26 @@ function ifWebGLSupported() {
 
   is(ShadersListView.selectedItem, ShadersListView.items[1],
     "The shaders list has a correct item selected.");
   is(ShadersListView.selectedIndex, 1,
     "The shaders list has a correct index selected.");
 
   yield teardown(panel);
   finish();
+
+  function checkFirstProgram () {
+    is(ShadersListView.itemCount, 1,
+      "The shaders list contains one entry.");
+    is(ShadersListView.selectedItem, ShadersListView.items[0],
+      "The shaders list has a correct item selected.");
+    is(ShadersListView.selectedIndex, 0,
+      "The shaders list has a correct index selected.");
+  }
+  function checkSecondProgram () {
+    is(ShadersListView.itemCount, 2,
+      "The shaders list contains two entries.");
+    is(ShadersListView.selectedItem, ShadersListView.items[0],
+      "The shaders list has a correct item selected.");
+    is(ShadersListView.selectedIndex, 0,
+      "The shaders list has a correct index selected.");
+  }
 }
-
-function once(aTarget, aEvent) {
-  let deferred = promise.defer();
-  aTarget.once(aEvent, deferred.resolve);
-  return deferred.promise;
-}
--- a/browser/devtools/shadereditor/test/browser_se_shaders-edit-01.js
+++ b/browser/devtools/shadereditor/test/browser_se_shaders-edit-01.js
@@ -5,17 +5,20 @@
  * Tests if editing a vertex and a fragment shader works properly.
  */
 
 function ifWebGLSupported() {
   let [target, debuggee, panel] = yield initShaderEditor(SIMPLE_CANVAS_URL);
   let { gFront, $, EVENTS, ShadersEditorsView } = panel.panelWin;
 
   reload(target);
-  yield once(gFront, "program-linked");
+  yield promise.all([
+    once(gFront, "program-linked"),
+    once(panel.panelWin, EVENTS.SOURCES_SHOWN)
+  ]);
 
   let vsEditor = yield ShadersEditorsView._getEditor("vs");
   let fsEditor = yield ShadersEditorsView._getEditor("fs");
 
   is(vsEditor.getText().indexOf("gl_Position"), 170,
     "The vertex shader editor contains the correct text.");
   is(fsEditor.getText().indexOf("gl_FragColor"), 97,
     "The fragment shader editor contains the correct text.");
@@ -63,14 +66,8 @@ function ifWebGLSupported() {
   yield ensurePixelIs(debuggee, { x: 128, y: 128 }, { r: 255, g: 0, b: 0, a: 127 }, true);
   yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 0, b: 0, a: 255 }, true);
 
   ok(true, "The fragment shader was recompiled successfully.");
 
   yield teardown(panel);
   finish();
 }
-
-function once(aTarget, aEvent) {
-  let deferred = promise.defer();
-  aTarget.once(aEvent, deferred.resolve);
-  return deferred.promise;
-}
--- a/browser/devtools/shadereditor/test/browser_se_shaders-edit-02.js
+++ b/browser/devtools/shadereditor/test/browser_se_shaders-edit-02.js
@@ -6,66 +6,63 @@
  * gets malformed after being edited.
  */
 
 function ifWebGLSupported() {
   let [target, debuggee, panel] = yield initShaderEditor(SIMPLE_CANVAS_URL);
   let { gFront, EVENTS, ShadersEditorsView } = panel.panelWin;
 
   reload(target);
-  yield once(gFront, "program-linked");
+  yield promise.all([
+    once(gFront, "program-linked"),
+    once(panel.panelWin, EVENTS.SOURCES_SHOWN)
+  ]);
 
   let vsEditor = yield ShadersEditorsView._getEditor("vs");
   let fsEditor = yield ShadersEditorsView._getEditor("fs");
 
   vsEditor.replaceText("vec3", { line: 7, ch: 22 }, { line: 7, ch: 26 });
-  let error = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+  let [, error] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
 
   ok(error,
     "The new vertex shader source was compiled with errors.");
   is(error.compile, "",
     "The compilation status should be empty.");
   isnot(error.link, "",
     "The linkage status should not be empty.");
   is(error.link.split("ERROR").length - 1, 2,
     "The linkage status contains two errors.");
   ok(error.link.contains("ERROR: 0:8: 'constructor'"),
     "A constructor error is contained in the linkage status.");
   ok(error.link.contains("ERROR: 0:8: 'assign'"),
     "An assignment error is contained in the linkage status.");
 
   fsEditor.replaceText("vec4", { line: 2, ch: 14 }, { line: 2, ch: 18 });
-  let error = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+  let [, error] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
 
   ok(error,
     "The new fragment shader source was compiled with errors.");
   is(error.compile, "",
     "The compilation status should be empty.");
   isnot(error.link, "",
     "The linkage status should not be empty.");
   is(error.link.split("ERROR").length - 1, 1,
     "The linkage status contains one error.");
   ok(error.link.contains("ERROR: 0:6: 'constructor'"),
     "A constructor error is contained in the linkage status.");
 
   yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
   yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
 
   vsEditor.replaceText("vec4", { line: 7, ch: 22 }, { line: 7, ch: 26 });
-  let error = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+  let [, error] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
   ok(!error, "The new vertex shader source was compiled successfully.");
 
   fsEditor.replaceText("vec3", { line: 2, ch: 14 }, { line: 2, ch: 18 });
-  let error = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+  let [, error] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
   ok(!error, "The new fragment shader source was compiled successfully.");
 
   yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
   yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
 
   yield teardown(panel);
   finish();
 }
-
-function once(aTarget, aEvent) {
-  let deferred = promise.defer();
-  aTarget.once(aEvent, (aName, aData) => deferred.resolve(aData));
-  return deferred.promise;
-}
--- a/browser/devtools/shadereditor/test/browser_se_shaders-edit-03.js
+++ b/browser/devtools/shadereditor/test/browser_se_shaders-edit-03.js
@@ -6,18 +6,23 @@
  * their new source on the backend and reshow it in the frontend when required.
  */
 
 function ifWebGLSupported() {
   let [target, debuggee, panel] = yield initShaderEditor(MULTIPLE_CONTEXTS_URL);
   let { gFront, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
 
   reload(target);
-  let firstProgramActor = yield once(gFront, "program-linked");
-  let secondProgramActor = yield once(gFront, "program-linked");
+
+  yield promise.all([
+    once(gFront, "program-linked"),
+    once(gFront, "program-linked")
+  ]);
+
+  yield once(panel.panelWin, EVENTS.SOURCES_SHOWN)
 
   let vsEditor = yield ShadersEditorsView._getEditor("vs");
   let fsEditor = yield ShadersEditorsView._getEditor("fs");
 
   is(ShadersListView.selectedIndex, 0,
     "The first program is currently selected.");
   is(vsEditor.getText().indexOf("1);"), 136,
     "The vertex shader editor contains the correct initial text (1).");
@@ -73,14 +78,8 @@ function ifWebGLSupported() {
   is(vsEditor.getText().indexOf("2.);"), 136,
     "The vertex shader editor contains the correct text (4).");
   is(fsEditor.getText().indexOf(".0);"), 116,
     "The fragment shader editor contains the correct text (4).");
 
   yield teardown(panel);
   finish();
 }
-
-function once(aTarget, aEvent) {
-  let deferred = promise.defer();
-  aTarget.once(aEvent, deferred.resolve);
-  return deferred.promise;
-}
--- a/browser/devtools/shadereditor/test/browser_webgl-actor-test-13.js
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-13.js
@@ -4,18 +4,17 @@
 /**
  * Tests if multiple WebGL contexts are correctly handled.
  */
 
 function ifWebGLSupported() {
   let [target, debuggee, front] = yield initBackend(MULTIPLE_CONTEXTS_URL);
   front.setup({ reload: true });
 
-  let firstProgramActor = yield once(front, "program-linked");
-  let secondProgramActor = yield once(front, "program-linked");
+  let [firstProgramActor, secondProgramActor] = yield getPrograms(front, 2);
 
   isnot(firstProgramActor, secondProgramActor,
     "Two distinct program actors were recevide from two separate contexts.");
 
   let firstVertexShader = yield firstProgramActor.getVertexShader();
   let firstFragmentShader = yield firstProgramActor.getFragmentShader();
   let secondVertexShader = yield secondProgramActor.getVertexShader();
   let secondFragmentShader = yield secondProgramActor.getFragmentShader();
--- a/browser/devtools/shadereditor/test/browser_webgl-actor-test-14.js
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-14.js
@@ -5,18 +5,18 @@
  * Tests that the rendering is updated when a uniform variable is
  * changed in one shader of a page with multiple WebGL contexts.
  */
 
 function ifWebGLSupported() {
   let [target, debuggee, front] = yield initBackend(MULTIPLE_CONTEXTS_URL);
   front.setup({ reload: true });
 
-  let firstProgramActor = yield once(front, "program-linked");
-  let secondProgramActor = yield once(front, "program-linked");
+  let [firstProgramActor, secondProgramActor] = yield getPrograms(front, 2);
+
   let firstFragmentShader = yield firstProgramActor.getFragmentShader();
   let secondFragmentShader = yield secondProgramActor.getFragmentShader();
 
   let oldFragSource = yield firstFragmentShader.getText();
   let newFragSource = oldFragSource.replace("vec4(uColor", "vec4(0.25, 0.25, 0.25");
   let status = yield firstFragmentShader.compile(newFragSource);
   ok(!status,
     "The first new fragment shader source was compiled without errors.");
--- a/browser/devtools/shadereditor/test/browser_webgl-actor-test-15.js
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-15.js
@@ -11,18 +11,17 @@ function ifWebGLSupported() {
 
   reload(target);
   let firstProgram = yield once(front, "program-linked");
   yield checkFirstCachedPrograms(firstProgram);
   yield checkHighlightingInTheFirstPage(firstProgram);
   ok(true, "The cached programs behave correctly before the navigation.");
 
   navigate(target, MULTIPLE_CONTEXTS_URL);
-  let secondProgram = yield once(front, "program-linked");
-  let thirdProgram = yield once(front, "program-linked");
+  let [secondProgram, thirdProgram] = yield getPrograms(front, 2);
   yield checkSecondCachedPrograms(firstProgram, [secondProgram, thirdProgram]);
   yield checkHighlightingInTheSecondPage(secondProgram, thirdProgram);
   ok(true, "The cached programs behave correctly after the navigation.");
 
   once(front, "program-linked").then(() => {
     ok(false, "Shouldn't have received any more program-linked notifications.");
   });
 
--- a/browser/devtools/shadereditor/test/browser_webgl-actor-test-16.js
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-16.js
@@ -18,18 +18,17 @@ function ifWebGLSupported() {
   is(programs.length, 1,
     "The first program should be returned by a call to getPrograms().");
   is(programs[0], firstProgram,
     "The first programs was correctly retrieved from the cache.");
 
   // 1. Perform a simple navigation.
 
   navigate(target, MULTIPLE_CONTEXTS_URL);
-  let secondProgram = yield once(front, "program-linked");
-  let thirdProgram = yield once(front, "program-linked");
+  let [secondProgram, thirdProgram] = yield getPrograms(front, 2);
   let programs = yield front.getPrograms();
   is(programs.length, 2,
     "The second and third programs should be returned by a call to getPrograms().");
   is(programs[0], secondProgram,
     "The second programs was correctly retrieved from the cache.");
   is(programs[1], thirdProgram,
     "The third programs was correctly retrieved from the cache.");
 
@@ -60,18 +59,17 @@ function ifWebGLSupported() {
   let globalCreated = observe("content-document-global-created");
   reload(target);
 
   yield globalDestroyed;
   let programs = yield front.getPrograms();
   is(programs.length, 0,
     "There should be no cached program actors yet.");
 
-  yield once(front, "program-linked");
-  yield once(front, "program-linked");
+  yield getPrograms(front, 2);
   yield globalCreated;
   let programs = yield front.getPrograms();
   is(programs.length, 2,
     "There should be 2 cached program actors now.");
 
   yield checkHighlightingInTheSecondPage(programs[0], programs[1]);
   ok(true, "The cached programs behave correctly after navigating forward and reloading.");
 
--- a/browser/devtools/shadereditor/test/browser_webgl-actor-test-17.js
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-17.js
@@ -5,18 +5,17 @@
  * Tests that the blackbox/unblackbox operations work as expected with
  * overlapping geometry.
  */
 
 function ifWebGLSupported() {
   let [target, debuggee, front] = yield initBackend(OVERLAPPING_GEOMETRY_CANVAS_URL);
   front.setup({ reload: true });
 
-  let firstProgramActor = yield once(front, "program-linked");
-  let secondProgramActor = yield once(front, "program-linked");
+  let [firstProgramActor, secondProgramActor] = yield getPrograms(front, 2);
 
   yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true);
   yield ensurePixelIs(debuggee, { x: 64, y: 64 }, { r: 0, g: 255, b: 255, a: 255 }, true);
   yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true);
   ok(true, "The corner vs. center pixel colors are correct before blackboxing.");
 
   yield firstProgramActor.blackbox();
   yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
--- a/browser/devtools/shadereditor/test/doc_blended-geometry.html
+++ b/browser/devtools/shadereditor/test/doc_blended-geometry.html
@@ -43,17 +43,17 @@
       let canvas, gl;
       let program = [];
       let squareVerticesPositionBuffer;
       let vertexPositionAttribute = [];
       let depthUniform = [];
 
       window.onload = function() {
         canvas = document.querySelector("canvas");
-        gl = canvas.getContext("webgl");
+        gl = canvas.getContext("webgl", { preserveDrawingBuffer: true });
         gl.clearColor(0.0, 0.0, 0.0, 1.0);
 
         initProgram(0);
         initProgram(1);
         initBuffers();
         drawScene();
       }
 
--- a/browser/devtools/shadereditor/test/doc_multiple-contexts.html
+++ b/browser/devtools/shadereditor/test/doc_multiple-contexts.html
@@ -37,17 +37,17 @@
       let program = [];
       let squareVerticesPositionBuffer = [];
       let vertexPositionAttribute = [];
       let colorUniform = [];
 
       window.onload = function() {
         for (let i = 0; i < 2; i++) {
           canvas[i] = document.querySelector("#canvas" + (i + 1));
-          gl[i] = canvas[i].getContext("webgl");
+          gl[i] = canvas[i].getContext("webgl", { preserveDrawingBuffer: true });
           gl[i].clearColor(0.0, 0.0, 0.0, 1.0);
 
           initProgram(i);
           initBuffers(i);
           drawScene(i);
         }
       }
 
--- a/browser/devtools/shadereditor/test/doc_overlapping-geometry.html
+++ b/browser/devtools/shadereditor/test/doc_overlapping-geometry.html
@@ -43,17 +43,17 @@
       let canvas, gl;
       let program = [];
       let squareVerticesPositionBuffer;
       let vertexPositionAttribute = [];
       let depthUniform = [];
 
       window.onload = function() {
         canvas = document.querySelector("canvas");
-        gl = canvas.getContext("webgl");
+        gl = canvas.getContext("webgl", { preserveDrawingBuffer: true });
         gl.clearColor(0.0, 0.0, 0.0, 1.0);
 
         initProgram(0);
         initProgram(1);
         initBuffers();
         drawScene();
       }
 
--- a/browser/devtools/shadereditor/test/doc_shader-order.html
+++ b/browser/devtools/shadereditor/test/doc_shader-order.html
@@ -30,17 +30,17 @@
 
     <script type="text/javascript;version=1.8">
       "use strict";
 
       let canvas, gl;
 
       window.onload = function() {
         canvas = document.querySelector("canvas");
-        gl = canvas.getContext("webgl");
+        gl = canvas.getContext("webgl", { preserveDrawingBuffer: true });
 
         let shaderProgram = gl.createProgram();
         let vertexShader, fragmentShader;
 
         // Compile and attach the shaders in a random order. The test will
         // ensure that the correct vertex and fragment source is retrieved
         // regardless of this crazyness.
         if (Math.random() > 0.5) {
--- a/browser/devtools/shadereditor/test/doc_simple-canvas.html
+++ b/browser/devtools/shadereditor/test/doc_simple-canvas.html
@@ -39,17 +39,17 @@
       let program;
       let squareVerticesPositionBuffer;
       let squareVerticesColorBuffer;
       let vertexPositionAttribute;
       let vertexColorAttribute;
 
       window.onload = function() {
         canvas = document.querySelector("canvas");
-        gl = canvas.getContext("webgl");
+        gl = canvas.getContext("webgl", { preserveDrawingBuffer: true });
         gl.clearColor(0.0, 0.0, 0.0, 1.0);
 
         initProgram();
         initBuffers();
         drawScene();
       }
 
       function initProgram() {
--- a/browser/devtools/shadereditor/test/head.js
+++ b/browser/devtools/shadereditor/test/head.js
@@ -7,17 +7,17 @@ const { classes: Cc, interfaces: Ci, uti
 let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
 
 // Enable logging for all the tests. Both the debugger server and frontend will
 // be affected by this pref.
 let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
 Services.prefs.setBoolPref("devtools.debugger.log", true);
 
 let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
-let { Promise: promise } = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+let { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
 let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
 let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
 let { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {});
 
 let { WebGLFront } = devtools.require("devtools/server/actors/webgl");
 let TiltGL = devtools.require("devtools/tilt/tilt-gl");
 let TargetFactory = devtools.TargetFactory;
@@ -119,32 +119,42 @@ function isWebGLSupported() {
 }
 
 function once(aTarget, aEventName, aUseCapture = false) {
   info("Waiting for event: '" + aEventName + "' on " + aTarget + ".");
 
   let deferred = promise.defer();
 
   for (let [add, remove] of [
+    ["on", "off"], // Use event emitter before DOM events for consistency
     ["addEventListener", "removeEventListener"],
-    ["addListener", "removeListener"],
-    ["on", "off"]
+    ["addListener", "removeListener"]
   ]) {
     if ((add in aTarget) && (remove in aTarget)) {
       aTarget[add](aEventName, function onEvent(...aArgs) {
         aTarget[remove](aEventName, onEvent, aUseCapture);
-        deferred.resolve.apply(deferred, aArgs);
+        deferred.resolve(...aArgs);
       }, aUseCapture);
       break;
     }
   }
 
   return deferred.promise;
 }
 
+// Hack around `once`, as that only resolves to a single (first) argument
+// and discards the rest. `onceSpread` is similar, except resolves to an
+// array of all of the arguments in the handler. These should be consolidated
+// into the same function, but many tests will need to be changed.
+function onceSpread(aTarget, aEvent) {
+  let deferred = promise.defer();
+  aTarget.once(aEvent, (...args) => deferred.resolve(args));
+  return deferred.promise;
+}
+
 function observe(aNotificationName, aOwnsWeak = false) {
   info("Waiting for observer notification: '" + aNotificationName + ".");
 
   let deferred = promise.defer();
 
   Services.obs.addObserver(function onNotification(...aArgs) {
     Services.obs.removeObserver(onNotification, aNotificationName);
     deferred.resolve.apply(deferred, aArgs);
@@ -268,8 +278,32 @@ function initShaderEditor(aUrl) {
 function teardown(aPanel) {
   info("Destroying the specified shader editor.");
 
   return promise.all([
     once(aPanel, "destroyed"),
     removeTab(aPanel.target.tab)
   ]);
 }
+
+// Due to `program-linked` events firing synchronously, we cannot
+// just yield/chain them together, as then we miss all actors after the
+// first event since they're fired consecutively. This allows us to capture
+// all actors and returns an array containing them.
+//
+// Takes a `front` object that is an event emitter, the number of
+// programs that should be listened to and waited on, and an optional
+// `onAdd` function that calls with the entire actors array on program link
+function getPrograms(front, count, onAdd) {
+  let actors = [];
+  let deferred = promise.defer();
+  front.on("program-linked", function onLink (actor) {
+    if (actors.length !== count) {
+      actors.push(actor);
+      if (typeof onAdd === 'function') onAdd(actors)
+    }
+    if (actors.length === count) {
+      front.off("program-linked", onLink);
+      deferred.resolve(actors);
+    }
+  });
+  return deferred.promise;
+}
--- a/browser/devtools/shared/widgets/VariablesView.jsm
+++ b/browser/devtools/shared/widgets/VariablesView.jsm
@@ -72,16 +72,17 @@ const STR = Services.strings.createBundl
 this.VariablesView = function VariablesView(aParentNode, aFlags = {}) {
   this._store = []; // Can't use a Map because Scope names needn't be unique.
   this._itemsByElement = new WeakMap();
   this._prevHierarchy = new Map();
   this._currHierarchy = new Map();
 
   this._parent = aParentNode;
   this._parent.classList.add("variables-view-container");
+  this._parent.classList.add("theme-body");
   this._appendEmptyNotice();
 
   this._onSearchboxInput = this._onSearchboxInput.bind(this);
   this._onSearchboxKeyPress = this._onSearchboxKeyPress.bind(this);
   this._onViewKeyPress = this._onViewKeyPress.bind(this);
   this._onViewKeyDown = this._onViewKeyDown.bind(this);
 
   // Create an internal scrollbox container.
--- a/browser/devtools/shared/widgets/ViewHelpers.jsm
+++ b/browser/devtools/shared/widgets/ViewHelpers.jsm
@@ -649,17 +649,17 @@ this.WidgetMethods = {
    *          - description: an optional description of the item
    * @param object aOptions [optional]
    *        Additional options or flags supported by this operation:
    *          - staged: true to stage the item to be appended later
    *          - index: specifies on which position should the item be appended
    *          - relaxed: true if this container should allow dupes & degenerates
    *          - attachment: some attached primitive/object for the item
    *          - attributes: a batch of attributes set to the displayed element
-   *          - finalize: function invokde when the item is removed
+   *          - finalize: function invoked when the item is removed
    * @return Item
    *         The item associated with the displayed element if an unstaged push,
    *         undefined if the item was staged for a later commit.
    */
   push: function(aContents, aOptions = {}) {
     let item = new Item(this, aOptions.attachment, aContents);
 
     // Batch the item to be added later.
@@ -728,16 +728,17 @@ this.WidgetMethods = {
    *        The item associated with the element to remove.
    */
   remove: function(aItem) {
     if (!aItem) {
       return;
     }
     this._widget.removeChild(aItem._target);
     this._untangleItem(aItem);
+    if (!this.itemCount) this.empty();
   },
 
   /**
    * Removes the item at the specified index from this container.
    *
    * @param number aIndex
    *        The index of the item to remove.
    */
@@ -1004,17 +1005,19 @@ this.WidgetMethods = {
     if (this.autoFocusOnSelection && targetElement) {
       targetElement.focus();
     }
 
     // Prevent selecting the same item again and avoid dispatching
     // a redundant selection event, so return early.
     if (targetElement != prevElement) {
       this._widget.selectedItem = targetElement;
-      ViewHelpers.dispatchEvent(targetElement || prevElement, "select", aItem);
+      let dispTarget = targetElement || prevElement;
+      let dispName = this.suppressSelectionEvents ? "suppressed-select" : "select";
+      ViewHelpers.dispatchEvent(dispTarget, dispName, aItem);
     }
 
     // Updates this container to reflect the information provided by the
     // currently selected item.
     this.refresh();
   },
 
   /**
@@ -1040,16 +1043,25 @@ this.WidgetMethods = {
   /**
    * Selects the element with the specified value in this container.
    * @param string aValue
    */
   set selectedValue(aValue)
     this.selectedItem = this._itemsByValue.get(aValue),
 
   /**
+   * Specifies if "select" events dispatched from the elements in this container
+   * when their respective items are selected should be suppressed or not.
+   *
+   * If this flag is set to true, then consumers of this container won't
+   * be normally notified when items are selected.
+   */
+  suppressSelectionEvents: false,
+
+  /**
    * Focus this container the first time an element is inserted?
    *
    * If this flag is set to true, then when the first item is inserted in
    * this container (and thus it's the only item available), its corresponding
    * target element is focused as well.
    */
   autoFocusOnFirstItem: true,
 
@@ -1301,16 +1313,24 @@ this.WidgetMethods = {
       if (aPredicate(item)) {
         return item;
       }
     }
     return null;
   },
 
   /**
+   * Shortcut function for getItemForPredicate which works on item attachments.
+   * @see getItemForPredicate
+   */
+  getItemForAttachment: function(aPredicate, aOwner = this) {
+    return this.getItemForPredicate(e => aPredicate(e.attachment));
+  },
+
+  /**
    * Finds the index of an item in the container.
    *
    * @param Item aItem
    *        The item get the index for.
    * @return number
    *         The index of the matched item, or -1 if nothing is found.
    */
   indexOfItem: function(aItem) {
--- a/browser/devtools/sourceeditor/editor.js
+++ b/browser/devtools/sourceeditor/editor.js
@@ -231,17 +231,23 @@ Editor.prototype = {
       cm = win.CodeMirror(win.document.body, this.config);
       cm.getWrapperElement().addEventListener("contextmenu", (ev) => {
         ev.preventDefault();
         this.showContextMenu(el.ownerDocument, ev.screenX, ev.screenY);
       }, false);
 
       cm.on("focus", () => this.emit("focus"));
       cm.on("scroll", () => this.emit("scroll"));
-      cm.on("change", () => this.emit("change"));
+      cm.on("change", () => {
+        this.emit("change");
+        if (!this._lastDirty) {
+          this._lastDirty = true;
+          this.emit("dirty-change");
+        }
+      });
       cm.on("cursorActivity", (cm) => this.emit("cursorActivity"));
 
       cm.on("gutterClick", (cm, line, gutter, ev) => {
         let head = { line: line, ch: 0 };
         let tail = { line: line, ch: this.getText(line).length };
 
         // Shift-click on a gutter selects the whole line.
         if (ev.shiftKey)
@@ -609,16 +615,18 @@ Editor.prototype = {
 
   /**
    * Marks the contents as clean and returns the current
    * version number.
    */
   setClean: function () {
     let cm = editors.get(this);
     this.version = cm.changeGeneration();
+    this._lastDirty = false;
+    this.emit("dirty-change");
     return this.version;
   },
 
   /**
    * Returns true if contents of the text area are
    * clean i.e. no changes were made since the last version.
    */
   isClean: function () {
--- a/browser/devtools/styleeditor/StyleSheetEditor.jsm
+++ b/browser/devtools/styleeditor/StyleSheetEditor.jsm
@@ -229,17 +229,17 @@ StyleSheetEditor.prototype = {
 
       sourceEditor.setFirstVisibleLine(this._state.topIndex);
       sourceEditor.setSelection(this._state.selection.start,
                                 this._state.selection.end);
 
       this.emit("source-editor-load");
     });
 
-    sourceEditor.on("change", this._onPropertyChange);
+    sourceEditor.on("dirty-change", this._onPropertyChange);
   },
 
   /**
    * Get the source editor for this editor.
    *
    * @return {Promise}
    *         Promise that will resolve with the editor.
    */
--- a/browser/devtools/styleeditor/test/browser_styleeditor_filesave.js
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_filesave.js
@@ -9,17 +9,16 @@ const Cc = Components.classes;
 const Ci = Components.interfaces;
 
 let tempScope = {};
 Components.utils.import("resource://gre/modules/FileUtils.jsm", tempScope);
 Components.utils.import("resource://gre/modules/NetUtil.jsm", tempScope);
 let FileUtils = tempScope.FileUtils;
 let NetUtil = tempScope.NetUtil;
 
-
 function test()
 {
   waitForExplicitFinish();
 
   copy(TESTCASE_URI_HTML, "simple.html", function(htmlFile) {
     copy(TESTCASE_URI_CSS, "simple.css", function(cssFile) {
       addTabAndOpenStyleEditor(function(panel) {
         let UI = panel.UI;
@@ -36,19 +35,32 @@ function test()
       let filePath = uri.resolve("");
       content.location = filePath;
     });
   });
 }
 
 function runTests(editor)
 {
+  editor.sourceEditor.once("dirty-change", () => {
+    is(editor.sourceEditor.isClean(), false, "Editor is dirty.");
+    ok(editor.summary.classList.contains("unsaved"),
+       "Star icon is present in the corresponding summary.");
+  });
+  let beginCursor = {line: 0, ch: 0};
+  editor.sourceEditor.replaceText("DIRTY TEXT", beginCursor, beginCursor);
+
+  editor.sourceEditor.once("dirty-change", () => {
+    is(editor.sourceEditor.isClean(), true, "Editor is clean.");
+    ok(!editor.summary.classList.contains("unsaved"),
+       "Star icon is not present in the corresponding summary.");
+    finish();
+  });
   editor.saveToFile(null, function (file) {
     ok(file, "file should get saved directly when using a file:// URI");
-    finish();
   });
 }
 
 function copy(aSrcChromeURL, aDestFileName, aCallback)
 {
   let destFile = FileUtils.getFile("ProfD", [aDestFileName]);
   write(read(aSrcChromeURL), destFile, aCallback);
 }
--- a/browser/devtools/webconsole/webconsole.js
+++ b/browser/devtools/webconsole/webconsole.js
@@ -3317,20 +3317,22 @@ JSTerm.prototype = {
     if (aOptions.targetElement) {
       let deferred = promise.defer();
       openPromise = deferred.promise;
       let document = aOptions.targetElement.ownerDocument;
       let iframe = document.createElementNS(XHTML_NS, "iframe");
 
       iframe.addEventListener("load", function onIframeLoad(aEvent) {
         iframe.removeEventListener("load", onIframeLoad, true);
+        iframe.style.visibility = "visible";
         deferred.resolve(iframe.contentWindow);
       }, true);
 
       iframe.flex = 1;
+      iframe.style.visibility = "hidden";
       iframe.setAttribute("src", VARIABLES_VIEW_URL);
       aOptions.targetElement.appendChild(iframe);
     }
     else {
       if (!this.sidebar) {
         this._createSidebar();
       }
       openPromise = this._addVariablesViewSidebarTab();
--- a/browser/locales/en-US/chrome/browser/devtools/debugger.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/debugger.dtd
@@ -124,18 +124,20 @@
 <!ENTITY debuggerUI.seMenuBreak.key     "B">
 
 <!-- LOCALIZATION NOTE (debuggerUI.seMenuCondBreak): This is the text that
   -  appears in the source editor context menu for adding a conditional
   -  breakpoint. -->
 <!ENTITY debuggerUI.seMenuCondBreak     "Add conditional breakpoint">
 <!ENTITY debuggerUI.seMenuCondBreak.key "B">
 
-<!-- LOCALIZATION NOTE (debuggerUI.instruments.*): This is the text that
-  -  appears in the debugger's instruments pane tabs. -->
+<!-- LOCALIZATION NOTE (debuggerUI.tabs.*): This is the text that
+  -  appears in the debugger's side pane tabs. -->
+<!ENTITY debuggerUI.tabs.sources        "Sources">
+<!ENTITY debuggerUI.tabs.callstack      "Call Stack">
 <!ENTITY debuggerUI.tabs.variables      "Variables">
 <!ENTITY debuggerUI.tabs.events         "Events">
 
 <!-- LOCALIZATION NOTE (debuggerUI.seMenuAddWatch): This is the text that
   -  appears in the source editor context menu for adding an expression. -->
 <!ENTITY debuggerUI.seMenuAddWatch      "Selection to watch expression">
 <!ENTITY debuggerUI.seMenuAddWatch.key  "E">
 
--- a/browser/locales/en-US/chrome/browser/devtools/debugger.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/debugger.properties
@@ -64,20 +64,24 @@ stepOutTooltip=Step Out (%S)
 # LOCALIZATION NOTE (emptyGlobalsText): The text to display in the menulist
 # when there are no chrome globals available.
 noGlobalsText=No globals
 
 # LOCALIZATION NOTE (noSourcesText): The text to display in the sources menu
 # when there are no scripts.
 noSourcesText=This page has no sources.
 
-# LOCALIZATION NOTE (noEventsTExt): The text to display in the events tab
+# LOCALIZATION NOTE (noEventListenersText): The text to display in the events tab
 # when there are no events.
 noEventListenersText=No event listeners to display
 
+# LOCALIZATION NOTE (noStackFramesText): The text to display in the call stack tab
+# when there are no stack frames.
+noStackFramesText=No stack frames to display
+
 # LOCALIZATION NOTE (eventCheckboxTooltip): The tooltip text to display when
 # the user hovers over the checkbox used to toggle an event breakpoint.
 eventCheckboxTooltip=Toggle breaking on this event
 
 # LOCALIZATION NOTE (eventOnSelector): The text to display in the events tab
 # for every event item, between the event type and event selector.
 eventOnSelector=on
 
--- a/browser/metro/base/content/browser.xul
+++ b/browser/metro/base/content/browser.xul
@@ -242,17 +242,17 @@ Desktop browser's sync prefs.
             </box>
 
             <textbox id="urlbar-edit" type="url" flex="1"
                      autocompletesearch="history"
                      autocompletepopup="urlbar-autocomplete"
                      completeselectedindex="true"
                      placeholder="&urlbar.emptytext;"
                      tabscrolling="true"
-                     onclick="SelectionHelperUI.urlbarClick();"/>
+                     onclick="SelectionHelperUI.urlbarTextboxClick(this);"/>
 
             <toolbarbutton id="go-button" class="urlbar-button"
                            command="cmd_go"/>
             <toolbarbutton id="reload-button" class="urlbar-button"
                            oncommand="CommandUpdater.doCommand(
                                         event.shiftKey ? 'cmd_forceReload'
                                                        : 'cmd_reload');"/>
             <toolbarbutton id="stop-button" class="urlbar-button"
--- a/browser/metro/base/content/helperui/ChromeSelectionHandler.js
+++ b/browser/metro/base/content/helperui/ChromeSelectionHandler.js
@@ -29,20 +29,19 @@ var ChromeSelectionHandler = {
 
   /*
    * General selection start method for both caret and selection mode.
    */
   _onSelectionAttach: function _onSelectionAttach(aJson) {
     this._domWinUtils = Util.getWindowUtils(window);
     this._contentWindow = window;
     this._targetElement = this._domWinUtils.elementFromPoint(aJson.xPos, aJson.yPos, true, false);
-
     this._targetIsEditable = this._targetElement instanceof Components.interfaces.nsIDOMXULTextBoxElement;
     if (!this._targetIsEditable) {
-      this._onFail("not an editable?");
+      this._onFail("not an editable?", this._targetElement);
       return;
     }
 
     let selection = this._getSelection();
     if (!selection) {
       this._onFail("no selection.");
       return;
     }
--- a/browser/metro/base/content/helperui/SelectionHelperUI.js
+++ b/browser/metro/base/content/helperui/SelectionHelperUI.js
@@ -339,19 +339,19 @@ var SelectionHelperUI = {
 
   /*
    * Observers
    */
 
   observe: function (aSubject, aTopic, aData) {
     switch (aTopic) {
       case "attach_edit_session_to_content":
-        let event = aSubject;
-        this.attachEditSession(Browser.selectedTab.browser,
-                               event.clientX, event.clientY);
+        // We receive this from text input bindings when this module
+        // isn't accessible.
+        this.chromeTextboxClick(aSubject);
         break;
 
       case "apzc-transform-begin":
         if (this.isActive && this.layerMode == kContentLayer) {
           this._hideMonocles();
         }
         break;
 
@@ -510,16 +510,50 @@ var SelectionHelperUI = {
     // which we will call _shutdown().
     let clearSelection = aClearSelection || false;
     this._sendAsyncMessage("Browser:SelectionClose", {
       clearSelection: clearSelection
     });
   },
 
   /*
+   * Event handler on the navbar text input. Called from navbar bindings
+   * when focus is applied to the edit.
+   */
+  urlbarTextboxClick: function(aEdit) {
+    // workaround for bug 925457: taping browser chrome resets last tap
+    // co-ordinates to 'undefined' so that we know not to shift the browser
+    // when the keyboard is up in SelectionHandler's _calcNewContentPosition().
+    Browser.selectedTab.browser.messageManager.sendAsyncMessage("Browser:ResetLastPos", {
+      xPos: null,
+      yPos: null
+    });
+
+    if (InputSourceHelper.isPrecise || !aEdit.textLength) {
+      return;
+    }
+
+    // Enable selection when there's text in the control
+    let innerRect = aEdit.inputField.getBoundingClientRect();
+    this.attachEditSession(ChromeSelectionHandler,
+                           innerRect.left,
+                           innerRect.top);
+  },
+
+  /*
+   * Click handler for chrome pages loaded into the browser (about:config).
+   * Called from the text input bindings via the attach_edit_session_to_content
+   * observer.
+   */
+  chromeTextboxClick: function (aEvent) {
+    this.attachEditSession(Browser.selectedTab.browser,
+                           aEvent.clientX, aEvent.clientY);
+  },
+
+  /*
    * Handy debug routines that work independent of selection. They
    * make use of the selection overlay for drawing points.
    */
 
   debugDisplayDebugPoint: function (aLeft, aTop, aSize, aCssColorStr, aFill) {
     this.overlay.enabled = true;
     this.overlay.displayDebugLayer = true;
     this.overlay.addDebugRect(aLeft, aTop, aLeft + aSize, aTop + aSize,
@@ -830,30 +864,20 @@ var SelectionHelperUI = {
     }
     return true;
   },
 
   /*
    * Event handlers for document events
    */
 
-   urlbarClick: function() {
-    // Workaround for bug 925457: taping browser chrome resets last tap
-    // co-ordinates to 'undefined' so that we know not to shift the browser
-    // when the keyboard is up (in SelectionHandler._calcNewContentPosition())
-    Browser.selectedTab.browser.messageManager.sendAsyncMessage("Browser:ResetLastPos", {
-      xPos: null,
-      yPos: null
-    });
-   },
-
   /*
    * Handles taps that move the current caret around in text edits,
    * clear active selection and focus when neccessary, or change
-   * modes.
+   * modes. Only active afer SelectionHandlerUI is initialized.
    */
   _onClick: function(aEvent) {
     if (this.layerMode == kChromeLayer && this._targetIsEditable) {
       this.attachToCaret(this._msgTarget, aEvent.clientX, aEvent.clientY);
     }
   },
 
   _onKeypress: function _onKeypress() {
--- a/browser/themes/linux/devtools/debugger.css
+++ b/browser/themes/linux/devtools/debugger.css
@@ -4,28 +4,32 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* Sources and breakpoints pane */
 
 #sources-pane {
   min-width: 50px;
 }
 
+#sources-pane > tabs {
+  -moz-border-end: 1px solid #222426; /* Match the sources list's dark margin. */
+}
+
 #sources-and-editor-splitter {
   -moz-border-start-color: transparent;
 }
 
 /* Sources toolbar */
 
 #sources-toolbar {
   border: none; /* Remove the devtools-toolbar's black bottom border. */
   -moz-border-end: 1px solid #222426; /* Match the sources list's dark margin. */
 }
 
-#sources-toolbar .devtools-toolbarbutton {
+#sources-toolbar > #sources-controls > .devtools-toolbarbutton {
   min-width: 32px;
 }
 
 #pretty-print {
   font-weight: bold;
 }
 
 #black-box {
@@ -82,50 +86,89 @@
 }
 
 /* ListWidget items */
 
 .list-widget-item {
   padding: 2px;
 }
 
-.list-widget-item:not(.selected):not(.empty):hover {
+.theme-light .list-widget-item:not(.selected):not(.empty):hover {
   background: linear-gradient(rgba(255,255,255,0.9), rgba(255,255,255,0.85)), Highlight;
 }
 
-.list-widget-item.selected.light {
+.theme-light .list-widget-item.selected.light {
   background: linear-gradient(rgba(255,255,255,0.85), rgba(255,255,255,0.8)), Highlight;
   color: #000;
 }
 
+.theme-dark .list-widget-item:not(.selected):not(.empty):hover {
+  background: linear-gradient(rgba(255,255,255,0.1), rgba(255,255,255,0.05));
+}
+
+.theme-dark .list-widget-item.selected.light {
+  background: linear-gradient(rgba(255,255,255,0.05), rgba(255,255,255,0.025));
+}
+
 .list-widget-item.selected {
   background: Highlight;
   color: HighlightText;
 }
 
 .list-widget-item.empty {
   color: GrayText;
   padding: 2px;
 }
 
-/* Stack frames view */
+/* Breadcrumbs stack frames view */
+
+.breadcrumbs-widget-item {
+  max-width: none;
+}
 
 .dbg-stackframe-details {
   -moz-padding-start: 4px;
 }
 
-.dbg-stackframe-menuitem[checked] {
-  margin-top: 3px;
-  margin-bottom: 3px;
-  outline: 1px solid #eee;
+/* Classic stack frames view */
+
+.dbg-classic-stackframe {
+  display: block;
+  padding: 4px;
+}
+
+.dbg-classic-stackframe-title {
   font-weight: 600;
+  color: #046;
+}
+
+.dbg-classic-stackframe-details:-moz-locale-dir(ltr) {
+  float: right;
 }
 
-.dbg-stackframe-menuitem-details {
-  -moz-padding-start: 16px;
+.dbg-classic-stackframe-details:-moz-locale-dir(rtl) {
+  float: left;
+}
+
+.dbg-classic-stackframe-details-url {
+  max-width: 90%;
+  text-align: end;
+  color: #666;
+}
+
+.dbg-classic-stackframe-details-sep {
+  color: #aaa;
+}
+
+.dbg-classic-stackframe-details-line {
+  color: #58b;
+}
+
+#callstack-list .side-menu-widget-item.selected label {
+  color: #fff;
 }
 
 /* Sources and breakpoints view */
 
 .dbg-breakpoint {
   -moz-margin-start: 4px;
 }
 
@@ -175,18 +218,19 @@
 
 /* Instruments pane (watch expressions, variables, event listeners...) */
 
 #instruments-pane > tabs > tab {
   min-height: 25px !important;
   padding: 0 !important;
 }
 
-#instruments-pane > tabpanels > tabpanel {
-  background: #fff;
+#instruments-pane .side-menu-widget-container,
+#instruments-pane .side-menu-widget-empty-notice-container {
+  box-shadow: none !important;
 }
 
 /* Watch expressions view */
 
 #expressions {
   min-height: 10px;
   max-height: 125px;
 }
@@ -198,16 +242,17 @@
 .dbg-expression-arrow {
   width: 16px;
   height: auto;
   background: -moz-image-rect(url(commandline-icon.png), 0, 32, 16, 16);
 }
 
 .dbg-expression-input {
   -moz-padding-start: 2px !important;
+  color: inherit;
 }
 
 /* Event listeners view */
 
 .dbg-event-listener {
   padding: 4px 8px;
 }
 
@@ -222,16 +267,20 @@
 .dbg-event-listener-targets {
   color: #046;
 }
 
 .dbg-event-listener-location {
   color: #666;
 }
 
+#event-listeners .side-menu-widget-item.selected {
+  background: none !important;
+}
+
 /* Searchbox and the search operations help panel */
 
 #searchbox {
   min-width: 220px;
   -moz-margin-start: 1px;
 }
 
 #filter-label {
@@ -375,16 +424,21 @@
 
 .dbg-results-line-contents-string[match=true][focused] {
   transition-duration: 0.1s;
   transform: scale(1.75, 1.75);
 }
 
 /* Toolbar controls */
 
+.devtools-sidebar-tabs > tabs > tab {
+  min-height: 25px !important;
+  padding: 0 !important;
+}
+
 #resumption-panel-desc {
   width: 200px;
 }
 
 #resumption-order-panel {
   -moz-margin-start: -8px;
 }
 
@@ -463,22 +517,27 @@
 
 /* Horizontal vs. vertical layout */
 
 #vertical-layout-panes-container {
   min-height: 35vh;
   max-height: 80vh;
 }
 
+#body[layout=vertical] #sources-pane > tabs {
+  -moz-border-end: none;
+}
+
 #body[layout=vertical] #instruments-pane {
   margin: 0 !important;
   /* To prevent all the margin hacks to hide the sidebar. */
 }
 
-#body[layout=vertical] .side-menu-widget-container {
+#body[layout=vertical] .side-menu-widget-container,
+#body[layout=vertical] .side-menu-widget-empty-notice-container {
   box-shadow: none !important;
 }
 
 #body[layout=vertical] .side-menu-widget-item-arrow {
   background-image: none !important;
 }
 
 #body[layout=vertical] .side-menu-widget-group,
--- a/browser/themes/linux/devtools/netmonitor.css
+++ b/browser/themes/linux/devtools/netmonitor.css
@@ -307,16 +307,20 @@ box.requests-menu-status[code^="5"] {
 .requests-menu-timings-cap.receive {
   background-color: rgba(255,255,255,1.0);
   box-shadow: 0 0 8px 0 rgba(128,255,255,1.0),
               0 0 4px 0 rgba(255,255,255,1.0) inset;
 }
 
 /* SideMenuWidget */
 
+.side-menu-widget-container {
+  box-shadow: none !important;
+}
+
 .side-menu-widget-item[odd] {
   background: rgba(255,255,255,0.05);
 }
 
 /* Network request details */
 
 #details-pane {
   background: hsl(208,11%,27%);
@@ -358,20 +362,16 @@ box.requests-menu-status[code^="5"] {
   text-shadow: 0 1px 0 #000;
   color: hsl(210,30%,85%);
 }
 
 .tabpanel-summary-value {
   -moz-padding-start: 3px;
 }
 
-.variable-or-property:not(:focus) > .title > .token-string {
-  color: #10c !important;
-}
-
 /* Headers tabpanel */
 
 #headers-summary-status,
 #headers-summary-version {
   padding-bottom: 2px;
 }
 
 #headers-summary-size {
--- a/browser/themes/linux/devtools/widgets.css
+++ b/browser/themes/linux/devtools/widgets.css
@@ -276,25 +276,27 @@
 
 .side-menu-widget-container[theme="light"] {
   background: #fff;
   color: #000;
 }
 
 /* SideMenuWidget container */
 
-.side-menu-widget-container[with-arrows=true]:-moz-locale-dir(ltr) {
+.side-menu-widget-container:-moz-locale-dir(ltr),
+.side-menu-widget-empty-notice-container:-moz-locale-dir(ltr) {
   box-shadow: inset -1px 0 0 #222426;
 }
 
-.side-menu-widget-container[with-arrows=true]:-moz-locale-dir(rtl) {
+.side-menu-widget-container:-moz-locale-dir(rtl),
+.side-menu-widget-empty-notice-container:-moz-locale-dir(rtl) {
   box-shadow: inset 1px 0 0 #222426;
 }
 
-.side-menu-widget-container[with-arrows=true] .side-menu-widget-group {
+.side-menu-widget-group {
   /* To allow visibility of the dark margin shadow. */
   -moz-margin-end: 1px;
 }
 
 .side-menu-widget-container[with-arrows=true] .side-menu-widget-item {
   /* To compensate for the arrow image's dark margin. */
   -moz-margin-end: -1px;
 }
@@ -323,36 +325,34 @@
   border-top: 1px solid hsla(210,8%,5%,.25);
   border-bottom: 1px solid hsla(210,16%,76%,.1);
   margin-top: -1px;
   margin-bottom: -1px;
 }
 
 .side-menu-widget-item[theme="light"] {
   border-top: 1px solid hsla(210,8%,75%,.25);
+  border-bottom: 1px solid hsla(210,16%,76%,.1);
   margin-top: -1px;
+  margin-bottom: -1px;
 }
 
 .side-menu-widget-item[theme="dark"]:last-of-type {
   box-shadow: inset 0 -1px 0 hsla(210,8%,5%,.25);
 }
 
 .side-menu-widget-item[theme="light"]:last-of-type {
   box-shadow: inset 0 -1px 0 hsla(210,8%,75%,.25);
 }
 
-.side-menu-widget-item[theme="dark"].selected {
+.side-menu-widget-item.selected {
   background: linear-gradient(hsl(206,61%,40%), hsl(206,61%,31%)) repeat-x top left !important;
   box-shadow: inset 0 1px 0 hsla(210,40%,83%,.15);
 }
 
-.side-menu-widget-item[theme="light"].selected {
-  /* Nothing here yet */
-}
-
 .side-menu-widget-item.selected > .side-menu-widget-item-arrow {
   background-size: auto, 1px 100%;
   background-repeat: no-repeat;
 }
 
 .side-menu-widget-item.selected > .side-menu-widget-item-arrow:-moz-locale-dir(ltr) {
   background-image: url(itemArrow-ltr.png), linear-gradient(to right, #222426, #222426);
   background-position: center right, top right;
@@ -427,135 +427,93 @@
 .side-menu-widget-empty-notice-container[theme="light"] {
   background: #fff;
   padding: 4px 8px;
   color: GrayText;
 }
 
 /* VariablesView */
 
-.variables-view-container {
-  background: #fff;
-}
-
 .variables-view-empty-notice {
   color: GrayText;
   padding: 2px;
 }
 
 .variables-view-scope > .title {
   color: #fff;
 }
 
-.variables-view-scope:focus > .title {
-  background: Highlight;
-  color: HighlightText;
-}
-
 .variables-view-scope > .variables-view-element-details:not(:empty) {
   -moz-margin-start: 2px;
   -moz-margin-end: 1px;
 }
 
 /* Generic traits applied to both variables and properties */
 
 .variable-or-property {
-  transition: background 1s ease-in-out;
-  color: #000;
+  transition: background 1s ease-in-out, color 1s ease-in-out;
 }
 
 .variable-or-property[changed] {
-  background: rgba(255,255,0,0.65);
-  transition-duration: 0.4s;
+  color: black;
+  transition-duration: .4s;
 }
 
 .variable-or-property > .title > .value {
   -moz-box-flex: 1;
   -moz-padding-start: 6px;
   -moz-padding-end: 4px;
 }
 
-.variable-or-property:focus > .title {
-  background: Highlight;
-  color: HighlightText;
-  border-radius: 4px;
-}
-
 .variable-or-property[editable] > .title > .value {
   cursor: text;
 }
 
 .variable-or-property:not([non-header]) > .variables-view-element-details {
   -moz-margin-start: 10px;
 }
 
 /* Custom variables and properties traits */
 
 .variables-view-variable {
   -moz-margin-start: 1px;
   -moz-margin-end: 1px;
 }
 
 .variables-view-variable:not(:last-child) {
-  border-bottom: 1px solid #eee;
+  border-bottom: 1px solid rgba(128, 128, 128, .15);
 }
 
 .variables-view-variable > .title > .name {
   font-weight: 600;
 }
 
-.variables-view-variable:not(:focus) > .title > .name {
-  color: #048;
-}
-
-.variables-view-property:not(:focus) > .title > .name {
-  color: #881090;
-}
-
-/* Token value colors */
-
-.variable-or-property:not(:focus) > .title > .token-undefined {
-  color: #bbb;
-}
-
-.variable-or-property:not(:focus) > .title > .token-null {
-  color: #999;
-}
-
-.variable-or-property:not(:focus) > .title > .token-boolean {
-  color: #10c;
-}
-
-.variable-or-property:not(:focus) > .title > .token-number {
-  color: #c00;
-}
-
-.variable-or-property:not(:focus) > .title > .token-string {
-  color: #282;
-}
-
-.variable-or-property:not(:focus) > .title > .token-other {
-  color: #333;
+.variable-or-property:focus > .title > label {
+  color: inherit !important;
 }
 
 /* Custom configurable/enumerable/writable or frozen/sealed/extensible
  * variables and properties */
 
 .variable-or-property[non-enumerable]:not([self]):not([exception]):not([return]):not([scope]) > .title > .name {
-  opacity: 0.5;
+  opacity: 0.6;
 }
 
 .variable-or-property[non-configurable] > .title > .name {
   border-bottom: 1px dashed #99f;
 }
 
 .variable-or-property[non-writable] > .title > .name {
   border-bottom: 1px dashed #f99;
 }
 
+.variable-or-property[safe-getter] > .title > .name {
+  border-bottom: 1px dashed #8b0;
+}
+
 .variable-or-property-non-writable-icon {
   background: url("chrome://browser/skin/identity-icons-https.png") no-repeat;
   width: 16px;
   height: 16px;
   opacity: 0.5;
 }
 
 @media (min-resolution: 2dppx) {
@@ -572,37 +530,16 @@
 }
 
 .variable-or-property:not(:focus) > .title > .variable-or-property-frozen-label,
 .variable-or-property:not(:focus) > .title > .variable-or-property-sealed-label,
 .variable-or-property:not(:focus) > .title > .variable-or-property-non-extensible-label {
   color: #666;
 }
 
-/* Special variables and properties */
-
-.variable-or-property[safe-getter] > .title > .name {
-  border-bottom: 1px dashed #8b0;
-}
-
-.variable-or-property[exception]:not(:focus) > .title > .name {
-  color: #a00;
-  text-shadow: 0 0 8px #fcc;
-}
-
-.variable-or-property[return]:not(:focus) > .title > .name {
-  color: #0a0;
-  text-shadow: 0 0 8px #cfc;
-}
-
-.variable-or-property[scope]:not(:focus) > .title > .name {
-  color: #00a;
-  text-shadow: 0 0 8px #ccf;
-}
-
 /* Aligned values */
 
 .variables-view-container[aligned-values] .title > .separator {
   -moz-box-flex: 1;
 }
 
 .variables-view-container[aligned-values] .title > .value {
   -moz-box-flex: 0;
@@ -629,17 +566,17 @@
 .variable-or-property > tooltip > label {
   margin: 0 2px 0 2px;
 }
 
 .variable-or-property[non-enumerable] > tooltip > label[value=enumerable],
 .variable-or-property[non-configurable] > tooltip > label[value=configurable],
 .variable-or-property[non-writable] > tooltip > label[value=writable],
 .variable-or-property[non-extensible] > tooltip > label[value=extensible] {
-  color: #f44;
+  color: #800;
   text-decoration: line-through;
 }
 
 .variable-or-property[safe-getter] > tooltip > label[value=WebIDL] {
   -moz-padding-start: 4px;
   -moz-border-start: 1px dotted #000;
   color: #080;
 }
@@ -675,24 +612,23 @@
 .element-value-input {
   -moz-margin-start: 4px !important;
   -moz-margin-end: 2px !important;
 }
 
 .element-name-input {
   -moz-margin-start: -2px !important;
   -moz-margin-end: 2px !important;
-  color: #048;
   font-weight: 600;
 }
 
 .element-value-input,
 .element-name-input {
-  border: 1px solid #999 !important;
-  box-shadow: 1px 2px 4px #aaa;
+  border: 1px solid rgba(128, 128, 128, .5) !important;
+  color: inherit;
 }
 
 /* Variables and properties searching */
 
 .variables-view-searchinput {
   min-height: 24px;
 }
 
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -3589,21 +3589,26 @@ toolbarbutton.chevron > .toolbarbutton-m
   margin-left: 1em;
 }
 
 /* Lion Fullscreen window styling */
 @media (-moz-mac-lion-theme) {
   #navigator-toolbox[inFullscreen]:not(:-moz-lwtheme)::before {
     height: calc(@tabHeight@ + 11px) !important;
   }
+  #main-window[inFullscreen][privatebrowsingmode=temporary],
   #main-window[inFullscreen]:-moz-lwtheme {
     /* This additional padding matches the change in height in the pseudo-element
-     * above. The rules combined force the top 22px of the background image to
-     * be hidden, so there image doesn't jump around with the loss of the titlebar */
+     * above. */
     padding-top: 11px;
+  }
+  #main-window[inFullscreen]:not([privatebrowsingmode=temporary]):-moz-lwtheme {
+    /* In combination with the previous rule, forces the top 22px of the
+     * background image to be hidden, so the image doesn't jump around with
+     * the loss of the titlebar. */
     background-position: right -11px;
   }
 }
 
 #full-screen-warning-message {
   background-image: url("chrome://browser/skin/fullscreen-darknoise.png");
   color: white;
   border-radius: 4px;
--- a/browser/themes/osx/devtools/debugger.css
+++ b/browser/themes/osx/devtools/debugger.css
@@ -6,28 +6,32 @@
 %include ../shared.inc
 
 /* Sources and breakpoints pane */
 
 #sources-pane {
   min-width: 50px;
 }
 
+#sources-pane > tabs {
+  -moz-border-end: 1px solid #222426; /* Match the sources list's dark margin. */
+}
+
 #sources-and-editor-splitter {
   -moz-border-start-color: transparent;
 }
 
 /* Sources toolbar */
 
 #sources-toolbar {
   border: none; /* Remove the devtools-toolbar's black bottom border. */
   -moz-border-end: 1px solid #222426; /* Match the sources list's dark margin. */
 }
 
-#sources-toolbar .devtools-toolbarbutton {
+#sources-toolbar > #sources-controls > .devtools-toolbarbutton {
   min-width: 32px;
 }
 
 #pretty-print {
   font-weight: bold;
 }
 
 #black-box {
@@ -84,50 +88,89 @@
 }
 
 /* ListWidget items */
 
 .list-widget-item {
   padding: 2px;
 }
 
-.list-widget-item:not(.selected):not(.empty):hover {
+.theme-light .list-widget-item:not(.selected):not(.empty):hover {
   background: linear-gradient(rgba(255,255,255,0.9), rgba(255,255,255,0.85)), Highlight;
 }
 
-.list-widget-item.selected.light {
+.theme-light .list-widget-item.selected.light {
   background: linear-gradient(rgba(255,255,255,0.85), rgba(255,255,255,0.8)), Highlight;
   color: #000;
 }
 
+.theme-dark .list-widget-item:not(.selected):not(.empty):hover {
+  background: linear-gradient(rgba(255,255,255,0.1), rgba(255,255,255,0.05));
+}
+
+.theme-dark .list-widget-item.selected.light {
+  background: linear-gradient(rgba(255,255,255,0.05), rgba(255,255,255,0.025));
+}
+
 .list-widget-item.selected {
   background: Highlight;
   color: HighlightText;
 }
 
 .list-widget-item.empty {
   color: GrayText;
   padding: 2px;
 }
 
-/* Stack frames view */
+/* Breadcrumbs stack frames view */
+
+.breadcrumbs-widget-item {
+  max-width: none;
+}
 
 .dbg-stackframe-details {
   -moz-padding-start: 4px;
 }
 
-.dbg-stackframe-menuitem[checked] {
-  margin-top: 3px;
-  margin-bottom: 3px;
-  outline: 1px solid #eee;
+/* Classic stack frames view */
+
+.dbg-classic-stackframe {
+  display: block;
+  padding: 4px;
+}
+
+.dbg-classic-stackframe-title {
   font-weight: 600;
+  color: #046;
+}
+
+.dbg-classic-stackframe-details:-moz-locale-dir(ltr) {
+  float: right;
 }
 
-.dbg-stackframe-menuitem-details {
-  -moz-padding-start: 16px;
+.dbg-classic-stackframe-details:-moz-locale-dir(rtl) {
+  float: left;
+}
+
+.dbg-classic-stackframe-details-url {
+  max-width: 90%;
+  text-align: end;
+  color: #666;
+}
+
+.dbg-classic-stackframe-details-sep {
+  color: #aaa;
+}
+
+.dbg-classic-stackframe-details-line {
+  color: #58b;
+}
+
+#callstack-list .side-menu-widget-item.selected label {
+  color: #fff;
 }
 
 /* Sources and breakpoints view */
 
 .dbg-breakpoint {
   -moz-margin-start: 4px;
 }
 
@@ -177,18 +220,19 @@
 
 /* Instruments pane (watch expressions, variables, event listeners...) */
 
 #instruments-pane > tabs > tab {
   min-height: 1em !important;
   padding: 0 !important;
 }
 
-#instruments-pane > tabpanels > tabpanel {
-  background: #fff;
+#instruments-pane .side-menu-widget-container,
+#instruments-pane .side-menu-widget-empty-notice-container {
+  box-shadow: none !important;
 }
 
 /* Watch expressions view */
 
 #expressions {
   min-height: 10px;
   max-height: 125px;
 }
@@ -200,16 +244,17 @@
 .dbg-expression-arrow {
   width: 16px;
   height: auto;
   background: -moz-image-rect(url(commandline-icon.png), 0, 32, 16, 16);
 }
 
 .dbg-expression-input {
   -moz-padding-start: 2px !important;
+  color: inherit;
 }
 
 /* Event listeners view */
 
 .dbg-event-listener {
   padding: 4px 8px;
 }
 
@@ -224,16 +269,20 @@
 .dbg-event-listener-targets {
   color: #046;
 }
 
 .dbg-event-listener-location {
   color: #666;
 }
 
+#event-listeners .side-menu-widget-item.selected {
+  background: none !important;
+}
+
 /* Searchbox and the search operations help panel */
 
 #searchbox {
   min-width: 220px;
   -moz-margin-start: 1px;
 }
 
 #filter-label {
@@ -377,16 +426,21 @@
 
 .dbg-results-line-contents-string[match=true][focused] {
   transition-duration: 0.1s;
   transform: scale(1.75, 1.75);
 }
 
 /* Toolbar controls */
 
+.devtools-sidebar-tabs > tabs > tab {
+  min-height: 1em !important;
+  padding: 0 !important;
+}
+
 #resumption-panel-desc {
   width: 200px;
 }
 
 #resumption-order-panel {
   -moz-margin-start: -8px;
 }
 
@@ -465,22 +519,27 @@
 
 /* Horizontal vs. vertical layout */
 
 #vertical-layout-panes-container {
   min-height: 35vh;
   max-height: 80vh;
 }
 
+#body[layout=vertical] #sources-pane > tabs {
+  -moz-border-end: none;
+}
+
 #body[layout=vertical] #instruments-pane {
   margin: 0 !important;
   /* To prevent all the margin hacks to hide the sidebar. */
 }
 
-#body[layout=vertical] .side-menu-widget-container {
+#body[layout=vertical] .side-menu-widget-container,
+#body[layout=vertical] .side-menu-widget-empty-notice-container {
   box-shadow: none !important;
 }
 
 #body[layout=vertical] .side-menu-widget-item-arrow {
   background-image: none !important;
 }
 
 #body[layout=vertical] .side-menu-widget-group,
--- a/browser/themes/osx/devtools/netmonitor.css
+++ b/browser/themes/osx/devtools/netmonitor.css
@@ -307,16 +307,20 @@ box.requests-menu-status[code^="5"] {
 .requests-menu-timings-cap.receive {
   background-color: rgba(255,255,255,1.0);
   box-shadow: 0 0 8px 0 rgba(128,255,255,1.0),
               0 0 4px 0 rgba(255,255,255,1.0) inset;
 }
 
 /* SideMenuWidget */
 
+.side-menu-widget-container {
+  box-shadow: none !important;
+}
+
 .side-menu-widget-item[odd] {
   background: rgba(255,255,255,0.05);
 }
 
 /* Network request details */
 
 #details-pane {
   background: hsl(208,11%,27%);
@@ -358,20 +362,16 @@ box.requests-menu-status[code^="5"] {
   text-shadow: 0 1px 0 #000;
   color: hsl(210,30%,85%);
 }
 
 .tabpanel-summary-value {
   -moz-padding-start: 3px;
 }
 
-.variable-or-property:not(:focus) > .title > .token-string {
-  color: #10c !important;
-}
-
 /* Headers tabpanel */
 
 #headers-summary-status,
 #headers-summary-version {
   padding-bottom: 2px;
 }
 
 #headers-summary-size {
--- a/browser/themes/osx/devtools/widgets.css
+++ b/browser/themes/osx/devtools/widgets.css
@@ -276,25 +276,27 @@
 
 .side-menu-widget-container[theme="light"] {
   background: #fff;
   color: #000;
 }
 
 /* SideMenuWidget container */
 
-.side-menu-widget-container[with-arrows=true]:-moz-locale-dir(ltr) {
+.side-menu-widget-container:-moz-locale-dir(ltr),
+.side-menu-widget-empty-notice-container:-moz-locale-dir(ltr) {
   box-shadow: inset -1px 0 0 #222426;
 }
 
-.side-menu-widget-container[with-arrows=true]:-moz-locale-dir(rtl) {
+.side-menu-widget-container:-moz-locale-dir(rtl),
+.side-menu-widget-empty-notice-container:-moz-locale-dir(rtl) {
   box-shadow: inset 1px 0 0 #222426;
 }
 
-.side-menu-widget-container[with-arrows=true] .side-menu-widget-group {
+.side-menu-widget-group {
   /* To allow visibility of the dark margin shadow. */
   -moz-margin-end: 1px;
 }
 
 .side-menu-widget-container[with-arrows=true] .side-menu-widget-item {
   /* To compensate for the arrow image's dark margin. */
   -moz-margin-end: -1px;
 }
@@ -323,36 +325,34 @@
   border-top: 1px solid hsla(210,8%,5%,.25);
   border-bottom: 1px solid hsla(210,16%,76%,.1);
   margin-top: -1px;
   margin-bottom: -1px;
 }
 
 .side-menu-widget-item[theme="light"] {
   border-top: 1px solid hsla(210,8%,75%,.25);
+  border-bottom: 1px solid hsla(210,16%,76%,.1);
   margin-top: -1px;
+  margin-bottom: -1px;
 }
 
 .side-menu-widget-item[theme="dark"]:last-of-type {
   box-shadow: inset 0 -1px 0 hsla(210,8%,5%,.25);
 }
 
 .side-menu-widget-item[theme="light"]:last-of-type {
   box-shadow: inset 0 -1px 0 hsla(210,8%,75%,.25);
 }
 
-.side-menu-widget-item[theme="dark"].selected {
+.side-menu-widget-item.selected {
   background: linear-gradient(hsl(206,61%,40%), hsl(206,61%,31%)) repeat-x top left !important;
   box-shadow: inset 0 1px 0 hsla(210,40%,83%,.15);
 }
 
-.side-menu-widget-item[theme="light"].selected {
-  /* Nothing here yet */
-}
-
 .side-menu-widget-item.selected > .side-menu-widget-item-arrow {
   background-size: auto, 1px 100%;
   background-repeat: no-repeat;
 }
 
 .side-menu-widget-item.selected > .side-menu-widget-item-arrow:-moz-locale-dir(ltr) {
   background-image: url(itemArrow-ltr.png), linear-gradient(to right, #222426, #222426);
   background-position: center right, top right;
@@ -421,135 +421,93 @@
 .side-menu-widget-empty-notice-container[theme="light"] {
   background: #fff;
   padding: 4px 8px;
   color: GrayText;
 }
 
 /* VariablesView */
 
-.variables-view-container {
-  background: #fff;
-}
-
 .variables-view-empty-notice {
   color: GrayText;
   padding: 2px;
 }
 
 .variables-view-scope > .title {
   color: #fff;
 }
 
-.variables-view-scope:focus > .title {
-  background: Highlight;
-  color: HighlightText;
-}
-
 .variables-view-scope > .variables-view-element-details:not(:empty) {
   -moz-margin-start: 2px;
   -moz-margin-end: 1px;
 }
 
 /* Generic traits applied to both variables and properties */
 
 .variable-or-property {
-  transition: background 1s ease-in-out;
-  color: #000;
+  transition: background 1s ease-in-out, color 1s ease-in-out;
 }
 
 .variable-or-property[changed] {
-  background: rgba(255,255,0,0.65);
-  transition-duration: 0.4s;
+  color: black;
+  transition-duration: .4s;
 }
 
 .variable-or-property > .title > .value {
   -moz-box-flex: 1;
   -moz-padding-start: 6px;
   -moz-padding-end: 4px;
 }
 
-.variable-or-property:focus > .title {
-  background: Highlight;
-  color: HighlightText;
-  border-radius: 4px;
-}
-
 .variable-or-property[editable] > .title > .value {
   cursor: text;
 }
 
 .variable-or-property:not([non-header]) > .variables-view-element-details {
   -moz-margin-start: 10px;
 }
 
 /* Custom variables and properties traits */
 
 .variables-view-variable {
   -moz-margin-start: 1px;
   -moz-margin-end: 1px;
 }
 
 .variables-view-variable:not(:last-child) {
-  border-bottom: 1px solid #eee;
+  border-bottom: 1px solid rgba(128, 128, 128, .15);
 }
 
 .variables-view-variable > .title > .name {
   font-weight: 600;
 }
 
-.variables-view-variable:not(:focus) > .title > .name {
-  color: #048;
-}
-
-.variables-view-property:not(:focus) > .title > .name {
-  color: #881090;
-}
-
-/* Token value colors */
-
-.variable-or-property:not(:focus) > .title > .token-undefined {
-  color: #bbb;
-}
-
-.variable-or-property:not(:focus) > .title > .token-null {
-  color: #999;
-}
-
-.variable-or-property:not(:focus) > .title > .token-boolean {
-  color: #10c;
-}
-
-.variable-or-property:not(:focus) > .title > .token-number {
-  color: #c00;
-}
-
-.variable-or-property:not(:focus) > .title > .token-string {
-  color: #282;
-}
-
-.variable-or-property:not(:focus) > .title > .token-other {
-  color: #333;
+.variable-or-property:focus > .title > label {
+  color: inherit !important;
 }
 
 /* Custom configurable/enumerable/writable or frozen/sealed/extensible
  * variables and properties */
 
 .variable-or-property[non-enumerable]:not([self]):not([exception]):not([return]):not([scope]) > .title > .name {
-  opacity: 0.5;
+  opacity: 0.6;
 }
 
 .variable-or-property[non-configurable] > .title > .name {
   border-bottom: 1px dashed #99f;
 }
 
 .variable-or-property[non-writable] > .title > .name {
   border-bottom: 1px dashed #f99;
 }
 
+.variable-or-property[safe-getter] > .title > .name {
+  border-bottom: 1px dashed #8b0;
+}
+
 .variable-or-property-non-writable-icon {
   background: url("chrome://browser/skin/identity-icons-https.png") no-repeat;
   width: 16px;
   height: 16px;
   opacity: 0.5;
 }
 
 @media (min-resolution: 2dppx) {
@@ -566,37 +524,16 @@
 }
 
 .variable-or-property:not(:focus) > .title > .variable-or-property-frozen-label,
 .variable-or-property:not(:focus) > .title > .variable-or-property-sealed-label,
 .variable-or-property:not(:focus) > .title > .variable-or-property-non-extensible-label {
   color: #666;
 }
 
-/* Special variables and properties */
-
-.variable-or-property[safe-getter] > .title > .name {
-  border-bottom: 1px dashed #8b0;
-}
-
-.variable-or-property[exception]:not(:focus) > .title > .name {
-  color: #a00;
-  text-shadow: 0 0 8px #fcc;
-}
-
-.variable-or-property[return]:not(:focus) > .title > .name {
-  color: #0a0;
-  text-shadow: 0 0 8px #cfc;
-}
-
-.variable-or-property[scope]:not(:focus) > .title > .name {
-  color: #00a;
-  text-shadow: 0 0 8px #ccf;
-}
-
 /* Aligned values */
 
 .variables-view-container[aligned-values] .title > .separator {
   -moz-box-flex: 1;
 }
 
 .variables-view-container[aligned-values] .title > .value {
   -moz-box-flex: 0;
@@ -669,24 +606,23 @@
 .element-value-input {
   -moz-margin-start: 4px !important;
   -moz-margin-end: 2px !important;
 }
 
 .element-name-input {
   -moz-margin-start: -2px !important;
   -moz-margin-end: 2px !important;
-  color: #048;
   font-weight: 600;
 }
 
 .element-value-input,
 .element-name-input {
-  border: 1px solid #999 !important;
-  box-shadow: 1px 2px 4px #aaa;
+  border: 1px solid rgba(128, 128, 128, .5) !important;
+  color: inherit;
 }
 
 /* Variables and properties searching */
 
 .variables-view-searchinput {
   min-height: 24px;
 }
 
--- a/browser/themes/shared/devtools/dark-theme.css
+++ b/browser/themes/shared/devtools/dark-theme.css
@@ -46,17 +46,18 @@
 .theme-selected {
   background: #26394D;
 }
 
 .theme-bg-darker {
   background-color: rgba(0,0,0,0.5);
 }
 
-.theme-bg-contrast { /* contrast bg color to attract attention on a container */
+.theme-bg-contrast,
+.variable-or-property[changed] { /* contrast bg color to attract attention on a container */
   background: #a18650;
 }
 
 .theme-link,
 .cm-s-mozilla .cm-link { /* blue */
   color: #3689b2;
 }
 
@@ -68,71 +69,82 @@
 .cm-s-mozilla .cm-link:visited { /* blue */
   color: #3689b2;
 }
 
 
 .theme-comment,
 .cm-s-mozilla .cm-meta,
 .cm-s-mozilla .cm-hr,
-.cm-s-mozilla .cm-comment { /* grey */
+.cm-s-mozilla .cm-comment,
+.variable-or-property .token-undefined,
+.variable-or-property .token-null { /* grey */
   color: #5c6773;
 }
 
 .theme-gutter {
   background-color: #0f171f;
   color: #667380;
   border-color: #303b47;
 }
 
 .theme-separator { /* grey */
   border-color: #303b47;
 }
 
 .theme-fg-color1,
-.cm-s-mozilla .cm-number { /* green */
+.cm-s-mozilla .cm-number,
+.variable-or-property .token-number,
+.variable-or-property[return] > .title > .name { /* green */
   color: #5c9966;
 }
 
 .theme-fg-color2,
 .cm-s-mozilla .cm-attribute,
 .cm-s-mozilla .cm-variable,
 .cm-s-mozilla .cm-def,
 .cm-s-mozilla .cm-property,
-.cm-s-mozilla .cm-qualifier { /* blue */
+.cm-s-mozilla .cm-qualifier,
+.variables-view-variable > .title > .name,
+.variable-or-property[scope] > .title > .name { /* blue */
   color: #3689b2;
 }
 
 .theme-fg-color3,
 .cm-s-mozilla .cm-builtin,
 .cm-s-mozilla .cm-tag,
-.cm-s-mozilla .cm-header { /* pink/lavender */
+.cm-s-mozilla .cm-header,
+.variables-view-property > .title > .name,
+.variable-or-property[safe-getter] > .title > .name { /* pink/lavender */
   color: #a673bf;
 }
 
 .theme-fg-color4 { /* purple/violet */
   color: #6270b2;
 }
 
 .theme-fg-color5,
 .cm-s-mozilla .cm-bracket,
 .cm-s-mozilla .cm-keyword { /* Yellow */
   color: #a18650;
 }
 
 .theme-fg-color6,
 .cm-s-mozilla .cm-string,
-.cm-s-mozilla .cm-string-2 { /* Orange */
+.cm-s-mozilla .cm-string-2,
+.variable-or-property .token-string { /* Orange */
   color: #b26b47;
 }
 
 .theme-fg-color7,
 .cm-s-mozilla .cm-atom,
 .cm-s-mozilla .cm-quote,
-.cm-s-mozilla .cm-error { /* Red */
+.cm-s-mozilla .cm-error,
+.variable-or-property .token-boolean,
+.variable-or-property[exception] > .title > .name { /* Red */
   color: #bf5656;
 }
 
 .theme-toolbar,
 .devtools-toolbar { /* General toolbar styling */
   color: hsl(210,30%,85%);
   background-color: #343c45;
   border-color: #060a0d;
@@ -143,16 +155,22 @@
 }
 
 .ruleview-colorswatch,
 .computedview-colorswatch,
 .markupview-colorswatch {
   box-shadow: 0 0 0 1px rgba(0,0,0,0.5);
 }
 
+.variables-view-scope:focus > .title,
+.variable-or-property:focus > .title {
+  background:  #3689b2; /* fg-color2 */
+  color: white;
+}
+
 /* CodeMirror specific styles.
  * Best effort to match the existing theme, some of the colors
  * are duplicated here to prevent weirdness in the main theme. */
 
 .CodeMirror { /* Inherit platform specific font sizing and styles */
   font-family: inherit;
   font-size: inherit;
   background: transparent;
--- a/browser/themes/shared/devtools/light-theme.css
+++ b/browser/themes/shared/devtools/light-theme.css
@@ -46,17 +46,18 @@
 .theme-selected {
   background-color: #CCC;
 }
 
 .theme-bg-darker {
   background: #EFEFEF;
 }
 
-.theme-bg-contrast { /* contrast bg color to attract attention on a container */
+.theme-bg-contrast,
+.variable-or-property[changed] { /* contrast bg color to attract attention on a container */
   background: #a18650;
 }
 
 .theme-link,
 .cm-s-mozilla .cm-link { /* blue */
   color: hsl(208,56%,40%);
 }
 
@@ -67,71 +68,82 @@
 .theme-link:visited,
 .cm-s-mozilla .cm-link:visited { /* blue */
   color: hsl(208,56%,40%);
 }
 
 .theme-comment,
 .cm-s-mozilla .cm-meta,
 .cm-s-mozilla .cm-hr,
-.cm-s-mozilla .cm-comment { /* grey */
+.cm-s-mozilla .cm-comment,
+.variable-or-property .token-undefined,
+.variable-or-property .token-null { /* grey */
   color: hsl(90,2%,46%);
 }
 
 .theme-gutter {
   background-color: hsl(0,0%,90%);
   color: #667380;
   border-color: hsl(0,0%,65%);
 }
 
 .theme-separator { /* grey */
   border-color: #cddae5;
 }
 
 .theme-fg-color1,
-.cm-s-mozilla .cm-number { /* green */
+.cm-s-mozilla .cm-number,
+.variable-or-property .token-number,
+.variable-or-property[return] > .title > .name { /* green */
   color: hsl(72,100%,27%);
 }
 
 .theme-fg-color2,
 .cm-s-mozilla .cm-attribute,
 .cm-s-mozilla .cm-builtin,
 .cm-s-mozilla .cm-def,
 .cm-s-mozilla .cm-property,
-.cm-s-mozilla .cm-qualifier { /* blue */
+.cm-s-mozilla .cm-qualifier,
+.variables-view-variable > .title > .name,
+.variable-or-property[scope] > .title > .name { /* blue */
   color: hsl(208,56%,40%);
 }
 
 .theme-fg-color3,
 .cm-s-mozilla .cm-variable,
 .cm-s-mozilla .cm-tag,
-.cm-s-mozilla .cm-header { /* dark blue */
+.cm-s-mozilla .cm-header,
+.variables-view-property > .title > .name,
+.variable-or-property[safe-getter] > .title > .name { /* dark blue */
   color: hsl(208,81%,21%)
 }
 
 .theme-fg-color4 { /* Orange */
   color: hsl(24,85%,39%);
 }
 
 .theme-fg-color5,
 .cm-s-mozilla .cm-bracket,
 .cm-s-mozilla .cm-keyword { /* Yellow */
   color: #a18650;
 }
 
 .theme-fg-color6,
 .cm-s-mozilla .cm-string,
-.cm-s-mozilla .cm-string-2 { /* Orange */
+.cm-s-mozilla .cm-string-2,
+.variable-or-property .token-string { /* Orange */
   color: hsl(24,85%,39%);
 }
 
 .theme-fg-color7,
 .cm-s-mozilla .cm-atom,
 .cm-s-mozilla .cm-quote,
-.cm-s-mozilla .cm-error { /* Red */
+.cm-s-mozilla .cm-error,
+.variable-or-property .token-boolean,
+.variable-or-property[exception] > .title > .name { /* Red */
   color: #bf5656;
 }
 
 .theme-fg-contrast { /* To be used for text on theme-bg-contrast */
   color: black;
 }
 
 .theme-toolbar,
@@ -142,16 +154,22 @@
 }
 
 .ruleview-colorswatch,
 .computedview-colorswatch,
 .markupview-colorswatch {
   box-shadow: 0 0 0 1px #EFEFEF;
 }
 
+.variables-view-scope:focus > .title,
+.variable-or-property:focus > .title {
+  background:  hsl(208,56%,40%); /* fg-color2 */
+  color: white;
+}
+
 /* CodeMirror specific styles.
  * Best effort to match the existing theme, some of the colors
  * are duplicated here to prevent weirdness in the main theme. */
 
 .CodeMirror { /* Inherit platform specific font sizing and styles */
   font-family: inherit;
   font-size: inherit;
   background: transparent;
--- a/browser/themes/shared/devtools/toolbars.inc.css
+++ b/browser/themes/shared/devtools/toolbars.inc.css
@@ -255,21 +255,25 @@
   -moz-margin-start: -1px;
 }
 
 .devtools-sidebar-tabs > tabs > tab:first-of-type {
   -moz-margin-start: -3px;
 }
 
 .devtools-sidebar-tabs > tabs > tab {
-  background-size: calc(100% - 2px) 100%, 1px 100%;
+  background-size: calc(100% - 1px) 100%, 1px 100%;
   background-repeat: no-repeat;
   background-position: 1px, 0;
 }
 
+.devtools-sidebar-tabs > tabs > tab:not(:last-of-type) {
+  background-size: calc(100% - 2px) 100%, 1px 100%;
+}
+
 .devtools-sidebar-tabs:-moz-locale-dir(rtl) > tabs > tab {
   background-position: calc(100% - 1px), 100%;
 }
 
 .devtools-sidebar-tabs > tabs > tab {
   background-color: transparent;
   background-image: linear-gradient(transparent, transparent), @smallSeparator@;
 }
--- a/browser/themes/shared/devtools/webconsole.inc.css
+++ b/browser/themes/shared/devtools/webconsole.inc.css
@@ -286,21 +286,21 @@ a {
 
 .inlined-variables-view .body {
   display: flex;
   flex-direction: column;
 }
 .inlined-variables-view iframe {
   display: block;
   flex: 1;
+  margin-top: 5px;
   margin-bottom: 15px;
   -moz-margin-end: 15px;
-  border: 1px solid #ccc;
-  border-radius: 4px;
-  box-shadow: 0 0 12px #dfdfdf;
+  border: 1px solid rgba(128, 128, 128, .5);
+  border-radius: 3px;
 }
 
 #webconsole-sidebar > tabs {
   height: 0;
   border: none;
 }
 
 /* Security styles */
@@ -349,24 +349,33 @@ a {
 .theme-dark .jsterm-complete-node {
   color: #5c6773; /* commentColor */
 }
 
 .theme-dark .navigation-marker .url {
   background: #131c26; /* mainBackgroundColor */
 }
 
+.theme-dark .inlined-variables-view iframe {
+  border-color: #333;
+}
+
 .theme-light .jsterm-input-container {
   background-color: #fff; /* mainBackgroundColor */
   border-color: ThreeDShadow;
 }
 
 .theme-light .jsterm-input-node {
   color: black; /* textColor */
 }
 
 .theme-light .jsterm-complete-node {
   color: hsl(90,2%,46%); /* commentColor */
 }
 
 .theme-light .navigation-marker .url {
   background: #fff; /* mainBackgroundColor */
 }
+
+.theme-light .inlined-variables-view iframe {
+  border-color: #ccc;
+}
+
--- a/browser/themes/windows/devtools/debugger.css
+++ b/browser/themes/windows/devtools/debugger.css
@@ -4,28 +4,32 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* Sources and breakpoints pane */
 
 #sources-pane {
   min-width: 50px;
 }
 
+#sources-pane > tabs {
+  -moz-border-end: 1px solid #222426; /* Match the sources list's dark margin. */
+}
+
 #sources-and-editor-splitter {
   -moz-border-start-color: transparent;
 }
 
 /* Sources toolbar */
 
 #sources-toolbar {
   border: none; /* Remove the devtools-toolbar's black bottom border. */
   -moz-border-end: 1px solid #222426; /* Match the sources list's dark margin. */
 }
 
-#sources-toolbar .devtools-toolbarbutton {
+#sources-toolbar > #sources-controls > .devtools-toolbarbutton {
   min-width: 32px;
 }
 
 #pretty-print {
   font-weight: bold;
 }
 
 #black-box {
@@ -82,50 +86,89 @@
 }
 
 /* ListWidget items */
 
 .list-widget-item {
   padding: 2px;
 }
 
-.list-widget-item:not(.selected):not(.empty):hover {
+.theme-light .list-widget-item:not(.selected):not(.empty):hover {
   background: linear-gradient(rgba(255,255,255,0.9), rgba(255,255,255,0.85)), Highlight;
 }
 
-.list-widget-item.selected.light {
+.theme-light .list-widget-item.selected.light {
   background: linear-gradient(rgba(255,255,255,0.85), rgba(255,255,255,0.8)), Highlight;
   color: #000;
 }
 
+.theme-dark .list-widget-item:not(.selected):not(.empty):hover {
+  background: linear-gradient(rgba(255,255,255,0.1), rgba(255,255,255,0.05));
+}
+
+.theme-dark .list-widget-item.selected.light {
+  background: linear-gradient(rgba(255,255,255,0.05), rgba(255,255,255,0.025));
+}
+
 .list-widget-item.selected {
   background: Highlight;
   color: HighlightText;
 }
 
 .list-widget-item.empty {
   color: GrayText;
   padding: 2px;
 }
 
-/* Stack frames view */
+/* Breadcrumbs stack frames view */
+
+.breadcrumbs-widget-item {
+  max-width: none;
+}
 
 .dbg-stackframe-details {
   -moz-padding-start: 4px;
 }
 
-.dbg-stackframe-menuitem[checked] {
-  margin-top: 3px;
-  margin-bottom: 3px;
-  outline: 1px solid #eee;
+/* Classic stack frames view */
+
+.dbg-classic-stackframe {
+  display: block;
+  padding: 4px;
+}
+
+.dbg-classic-stackframe-title {
   font-weight: 600;
+  color: #046;
+}
+
+.dbg-classic-stackframe-details:-moz-locale-dir(ltr) {
+  float: right;
 }
 
-.dbg-stackframe-menuitem-details {
-  -moz-padding-start: 16px;
+.dbg-classic-stackframe-details:-moz-locale-dir(rtl) {
+  float: left;
+}
+
+.dbg-classic-stackframe-details-url {
+  max-width: 90%;
+  text-align: end;
+  color: #666;
+}
+
+.dbg-classic-stackframe-details-sep {
+  color: #aaa;
+}
+
+.dbg-classic-stackframe-details-line {
+  color: #58b;
+}
+
+#callstack-list .side-menu-widget-item.selected label {
+  color: #fff;
 }
 
 /* Sources and breakpoints view */
 
 .dbg-breakpoint {
   -moz-margin-start: 4px;
 }
 
@@ -175,18 +218,19 @@
 
 /* Instruments pane (watch expressions, variables, event listeners...) */
 
 #instruments-pane > tabs > tab {
   min-height: 25px !important;
   padding: 0 !important;
 }
 
-#instruments-pane > tabpanels > tabpanel {
-  background: #fff;
+#instruments-pane .side-menu-widget-container,
+#instruments-pane .side-menu-widget-empty-notice-container {
+  box-shadow: none !important;
 }
 
 /* Watch expressions view */
 
 #expressions {
   min-height: 10px;
   max-height: 125px;
 }
@@ -198,16 +242,17 @@
 .dbg-expression-arrow {
   width: 16px;
   height: auto;
   background: -moz-image-rect(url(commandline-icon.png), 0, 32, 16, 16);
 }
 
 .dbg-expression-input {
   -moz-padding-start: 2px !important;
+  color: inherit;
 }
 
 /* Event listeners view */
 
 .dbg-event-listener {
   padding: 4px 8px;
 }
 
@@ -222,16 +267,20 @@
 .dbg-event-listener-targets {
   color: #046;
 }
 
 .dbg-event-listener-location {
   color: #666;
 }
 
+#event-listeners .side-menu-widget-item.selected {
+  background: none !important;
+}
+
 /* Searchbox and the search operations help panel */
 
 #searchbox {
   min-width: 220px;
   -moz-margin-start: 1px;
 }
 
 #filter-label {
@@ -375,16 +424,21 @@
 
 .dbg-results-line-contents-string[match=true][focused] {
   transition-duration: 0.1s;
   transform: scale(1.75, 1.75);
 }
 
 /* Toolbar controls */
 
+.devtools-sidebar-tabs > tabs > tab {
+  min-height: 25px !important;
+  padding: 0 !important;
+}
+
 #resumption-panel-desc {
   width: 200px;
 }
 
 #resumption-order-panel {
   -moz-margin-start: -8px;
 }
 
@@ -468,22 +522,27 @@
 
 /* Horizontal vs. vertical layout */
 
 #vertical-layout-panes-container {
   min-height: 35vh;
   max-height: 80vh;
 }
 
+#body[layout=vertical] #sources-pane > tabs {
+  -moz-border-end: none;
+}
+
 #body[layout=vertical] #instruments-pane {
   margin: 0 !important;
   /* To prevent all the margin hacks to hide the sidebar. */
 }
 
-#body[layout=vertical] .side-menu-widget-container {
+#body[layout=vertical] .side-menu-widget-container,
+#body[layout=vertical] .side-menu-widget-empty-notice-container {
   box-shadow: none !important;
 }
 
 #body[layout=vertical] .side-menu-widget-item-arrow {
   background-image: none !important;
 }
 
 #body[layout=vertical] .side-menu-widget-group,
--- a/browser/themes/windows/devtools/netmonitor.css
+++ b/browser/themes/windows/devtools/netmonitor.css
@@ -307,16 +307,20 @@ box.requests-menu-status[code^="5"] {
 .requests-menu-timings-cap.receive {
   background-color: rgba(255,255,255,1.0);
   box-shadow: 0 0 8px 0 rgba(128,255,255,1.0),
               0 0 4px 0 rgba(255,255,255,1.0) inset;
 }
 
 /* SideMenuWidget */
 
+.side-menu-widget-container {
+  box-shadow: none !important;
+}
+
 .side-menu-widget-item[odd] {
   background: rgba(255,255,255,0.05);
 }
 
 /* Network request details */
 
 #details-pane {
   background: hsl(208,11%,27%);
@@ -358,20 +362,16 @@ box.requests-menu-status[code^="5"] {
   text-shadow: 0 1px 0 #000;
   color: hsl(210,30%,85%);
 }
 
 .tabpanel-summary-value {
   -moz-padding-start: 3px;
 }
 
-.variable-or-property:not(:focus) > .title > .token-string {
-  color: #10c !important;
-}
-
 /* Headers tabpanel */
 
 #headers-summary-status,
 #headers-summary-version {
   padding-bottom: 2px;
 }
 
 #headers-summary-size {
--- a/browser/themes/windows/devtools/widgets.css
+++ b/browser/themes/windows/devtools/widgets.css
@@ -280,25 +280,27 @@
 
 .side-menu-widget-container[theme="light"] {
   background: #fff;
   color: #000;
 }
 
 /* SideMenuWidget container */
 
-.side-menu-widget-container[with-arrows=true]:-moz-locale-dir(ltr) {
+.side-menu-widget-container:-moz-locale-dir(ltr),
+.side-menu-widget-empty-notice-container:-moz-locale-dir(ltr) {
   box-shadow: inset -1px 0 0 #222426;
 }
 
-.side-menu-widget-container[with-arrows=true]:-moz-locale-dir(rtl) {
+.side-menu-widget-container:-moz-locale-dir(rtl),
+.side-menu-widget-empty-notice-container:-moz-locale-dir(rtl) {
   box-shadow: inset 1px 0 0 #222426;
 }
 
-.side-menu-widget-container[with-arrows=true] .side-menu-widget-group {
+.side-menu-widget-group {
   /* To allow visibility of the dark margin shadow. */
   -moz-margin-end: 1px;
 }
 
 .side-menu-widget-container[with-arrows=true] .side-menu-widget-item {
   /* To compensate for the arrow image's dark margin. */
   -moz-margin-end: -1px;
 }
@@ -327,36 +329,34 @@
   border-top: 1px solid hsla(210,8%,5%,.25);
   border-bottom: 1px solid hsla(210,16%,76%,.1);
   margin-top: -1px;
   margin-bottom: -1px;
 }
 
 .side-menu-widget-item[theme="light"] {
   border-top: 1px solid hsla(210,8%,75%,.25);
+  border-bottom: 1px solid hsla(210,16%,76%,.1);
   margin-top: -1px;
+  margin-bottom: -1px;
 }
 
 .side-menu-widget-item[theme="dark"]:last-of-type {
   box-shadow: inset 0 -1px 0 hsla(210,8%,5%,.25);
 }
 
 .side-menu-widget-item[theme="light"]:last-of-type {
   box-shadow: inset 0 -1px 0 hsla(210,8%,75%,.25);
 }
 
-.side-menu-widget-item[theme="dark"].selected {
+.side-menu-widget-item.selected {
   background: linear-gradient(hsl(206,61%,40%), hsl(206,61%,31%)) repeat-x top left !important;
   box-shadow: inset 0 1px 0 hsla(210,40%,83%,.15);
 }
 
-.side-menu-widget-item[theme="light"].selected {
-  /* Nothing here yet */
-}
-
 .side-menu-widget-item.selected > .side-menu-widget-item-arrow {
   background-size: auto, 1px 100%;
   background-repeat: no-repeat;
 }
 
 .side-menu-widget-item.selected > .side-menu-widget-item-arrow:-moz-locale-dir(ltr) {
   background-image: url(itemArrow-ltr.png), linear-gradient(to right, #222426, #222426);
   background-position: center right, top right;
@@ -424,135 +424,93 @@
 .side-menu-widget-empty-notice-container[theme="light"] {
   background: #fff;
   padding: 4px 8px;
   color: GrayText;
 }
 
 /* VariablesView */
 
-.variables-view-container {
-  background: #fff;
-}
-
 .variables-view-empty-notice {
   color: GrayText;
   padding: 2px;
 }
 
 .variables-view-scope > .title {
   color: #fff;
 }
 
-.variables-view-scope:focus > .title {
-  background: Highlight;
-  color: HighlightText;
-}
-
 .variables-view-scope > .variables-view-element-details:not(:empty) {
   -moz-margin-start: 2px;
   -moz-margin-end: 1px;
 }
 
 /* Generic traits applied to both variables and properties */
 
 .variable-or-property {
-  transition: background 1s ease-in-out;
-  color: #000;
+  transition: background 1s ease-in-out, color 1s ease-in-out;
 }
 
 .variable-or-property[changed] {
-  background: rgba(255,255,0,0.65);
-  transition-duration: 0.4s;
+  color: black;
+  transition-duration: .4s;
 }
 
 .variable-or-property > .title > .value {
   -moz-box-flex: 1;
   -moz-padding-start: 6px;
   -moz-padding-end: 4px;
 }
 
-.variable-or-property:focus > .title {
-  background: Highlight;
-  color: HighlightText;
-  border-radius: 4px;
-}
-
 .variable-or-property[editable] > .title > .value {
   cursor: text;
 }
 
 .variable-or-property:not([non-header]) > .variables-view-element-details {
   -moz-margin-start: 10px;
 }
 
 /* Custom variables and properties traits */
 
 .variables-view-variable {
   -moz-margin-start: 1px;
   -moz-margin-end: 1px;
 }
 
 .variables-view-variable:not(:last-child) {
-  border-bottom: 1px solid #eee;
+  border-bottom: 1px solid rgba(128, 128, 128, .15);
 }
 
 .variables-view-variable > .title > .name {
   font-weight: 600;
 }
 
-.variables-view-variable:not(:focus) > .title > .name {
-  color: #048;
-}
-
-.variables-view-property:not(:focus) > .title > .name {
-  color: #881090;
-}
-
-/* Token value colors */
-
-.variable-or-property:not(:focus) > .title > .token-undefined {
-  color: #bbb;
-}
-
-.variable-or-property:not(:focus) > .title > .token-null {
-  color: #999;
-}
-
-.variable-or-property:not(:focus) > .title > .token-boolean {
-  color: #10c;
-}
-
-.variable-or-property:not(:focus) > .title > .token-number {
-  color: #c00;
-}
-
-.variable-or-property:not(:focus) > .title > .token-string {
-  color: #282;
-}
-
-.variable-or-property:not(:focus) > .title > .token-other {
-  color: #333;
+.variable-or-property:focus > .title > label {
+  color: inherit !important;
 }
 
 /* Custom configurable/enumerable/writable or frozen/sealed/extensible
  * variables and properties */
 
 .variable-or-property[non-enumerable]:not([self]):not([exception]):not([return]):not([scope]) > .title > .name {
-  opacity: 0.5;
+  opacity: 0.6;
 }
 
 .variable-or-property[non-configurable] > .title > .name {
   border-bottom: 1px dashed #99f;
 }
 
 .variable-or-property[non-writable] > .title > .name {
   border-bottom: 1px dashed #f99;
 }
 
+.variable-or-property[safe-getter] > .title > .name {
+  border-bottom: 1px dashed #8b0;
+}
+
 .variable-or-property-non-writable-icon {
   background: url("chrome://browser/skin/identity-icons-https.png") no-repeat;
   width: 16px;
   height: 16px;
   opacity: 0.5;
 }
 
 @media (min-resolution: 2dppx) {
@@ -569,37 +527,16 @@
 }
 
 .variable-or-property:not(:focus) > .title > .variable-or-property-frozen-label,
 .variable-or-property:not(:focus) > .title > .variable-or-property-sealed-label,
 .variable-or-property:not(:focus) > .title > .variable-or-property-non-extensible-label {
   color: #666;
 }
 
-/* Special variables and properties */
-
-.variable-or-property[safe-getter] > .title > .name {
-  border-bottom: 1px dashed #8b0;
-}
-
-.variable-or-property[exception]:not(:focus) > .title > .name {
-  color: #a00;
-  text-shadow: 0 0 8px #fcc;
-}
-
-.variable-or-property[return]:not(:focus) > .title > .name {
-  color: #0a0;
-  text-shadow: 0 0 8px #cfc;
-}
-
-.variable-or-property[scope]:not(:focus) > .title > .name {
-  color: #00a;
-  text-shadow: 0 0 8px #ccf;
-}
-
 /* Aligned values */
 
 .variables-view-container[aligned-values] .title > .separator {
   -moz-box-flex: 1;
 }
 
 .variables-view-container[aligned-values] .title > .value {
   -moz-box-flex: 0;
@@ -672,24 +609,23 @@
 .element-value-input {
   -moz-margin-start: 4px !important;
   -moz-margin-end: 2px !important;
 }
 
 .element-name-input {
   -moz-margin-start: -2px !important;
   -moz-margin-end: 2px !important;
-  color: #048;
   font-weight: 600;
 }
 
 .element-value-input,
 .element-name-input {
-  border: 1px solid #999 !important;
-  box-shadow: 1px 2px 4px #aaa;
+  border: 1px solid rgba(128, 128, 128, .5) !important;
+  color: inherit;
 }
 
 /* Variables and properties searching */
 
 .variables-view-searchinput {
   min-height: 24px;
 }
 
--- a/build/mobile/robocop/FennecNativeActions.java
+++ b/build/mobile/robocop/FennecNativeActions.java
@@ -1,150 +1,102 @@
 /* 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/. */
 
 package org.mozilla.gecko;
 
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Proxy;
-import java.lang.reflect.InvocationHandler;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-import java.util.ArrayList;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.gfx.GeckoLayerClient;
+import org.mozilla.gecko.gfx.GeckoLayerClient.DrawListener;
+import org.mozilla.gecko.mozglue.GeckoLoader;
+import org.mozilla.gecko.sqlite.SQLiteBridge;
+import org.mozilla.gecko.util.GeckoEventListener;
 
 import android.app.Activity;
 import android.app.Instrumentation;
 import android.content.Context;
 import android.database.Cursor;
 import android.os.SystemClock;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.ArrayList;
+import org.json.JSONObject;
 
 import com.jayway.android.robotium.solo.Solo;
 
 import static org.mozilla.gecko.FennecNativeDriver.LogLevel;
 
 public class FennecNativeActions implements Actions {
+    private static final String LOGTAG = "FennecNativeActions";
+
     private Solo mSolo;
     private Instrumentation mInstr;
-    private Activity mGeckoApp;
     private Assert mAsserter;
 
-    // Objects for reflexive access of fennec classes.
-    private ClassLoader mClassLoader;
-    private Class mApiClass;
-    private Class mEventListenerClass;
-    private Class mDrawListenerClass;
-    private Method mRegisterEventListener;
-    private Method mUnregisterEventListener;
-    private Method mBroadcastEvent;
-    private Method mPreferencesGetEvent;
-    private Method mPreferencesObserveEvent;
-    private Method mPreferencesRemoveObserversEvent;
-    private Method mSetDrawListener;
-    private Method mQuerySql;
-    private Object mRobocopApi;
-
-    private static final String LOGTAG = "FennecNativeActions";
-
     public FennecNativeActions(Activity activity, Solo robocop, Instrumentation instrumentation, Assert asserter) {
         mSolo = robocop;
         mInstr = instrumentation;
-        mGeckoApp = activity;
         mAsserter = asserter;
-        // Set up reflexive access of java classes and methods.
-        try {
-            mClassLoader = activity.getClassLoader();
-
-            mApiClass = mClassLoader.loadClass("org.mozilla.gecko.RobocopAPI");
-            mEventListenerClass = mClassLoader.loadClass("org.mozilla.gecko.util.GeckoEventListener");
-            mDrawListenerClass = mClassLoader.loadClass("org.mozilla.gecko.gfx.GeckoLayerClient$DrawListener");
-
-            mRegisterEventListener = mApiClass.getMethod("registerEventListener", String.class, mEventListenerClass);
-            mUnregisterEventListener = mApiClass.getMethod("unregisterEventListener", String.class, mEventListenerClass);
-            mBroadcastEvent = mApiClass.getMethod("broadcastEvent", String.class, String.class);
-            mPreferencesGetEvent = mApiClass.getMethod("preferencesGetEvent", Integer.TYPE, String[].class);
-            mPreferencesObserveEvent = mApiClass.getMethod("preferencesObserveEvent", Integer.TYPE, String[].class);
-            mPreferencesRemoveObserversEvent = mApiClass.getMethod("preferencesRemoveObserversEvent", Integer.TYPE);
-            mSetDrawListener = mApiClass.getMethod("setDrawListener", mDrawListenerClass);
-            mQuerySql = mApiClass.getMethod("querySql", String.class, String.class);
-
-            mRobocopApi = mApiClass.getConstructor(Activity.class).newInstance(activity);
-        } catch (Exception e) {
-            FennecNativeDriver.log(LogLevel.ERROR, e);
-        }
-    }
 
-    class wakeInvocationHandler implements InvocationHandler {
-        private final GeckoEventExpecter mEventExpecter;
-
-        public wakeInvocationHandler(GeckoEventExpecter expecter) {
-            mEventExpecter = expecter;
-        }
-
-        public Object invoke(Object proxy, Method method, Object[] args) {
-            String methodName = method.getName();
-            //Depending on the method, return a completely different type.
-            if(methodName.equals("toString")) {
-                return this.toString();
-            }
-            if(methodName.equals("equals")) {
-                return
-                    args[0] == null ? false :
-                    this.toString().equals(args[0].toString());
-            }
-            if(methodName.equals("clone")) {
-                return this;
-            }
-            if(methodName.equals("hashCode")) {
-                return 314;
-            }
-            FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG, 
-                "Waking up on "+methodName);
-            mEventExpecter.notifyOfEvent(args);
-            return null;
-        }
+        GeckoLoader.loadSQLiteLibs(activity, activity.getApplication().getPackageResourcePath());
     }
 
     class GeckoEventExpecter implements RepeatedEventExpecter {
+        private static final int MAX_WAIT_MS = 90000;
+
+        private volatile boolean mIsRegistered;
+
         private final String mGeckoEvent;
-        private Object[] mRegistrationParams;
-        private boolean mEventEverReceived;
+        private final GeckoEventListener mListener;
+
+        private volatile boolean mEventEverReceived;
         private String mEventData;
         private BlockingQueue<String> mEventDataQueue;
-        private static final int MAX_WAIT_MS = 90000;
 
-        GeckoEventExpecter(String geckoEvent, Object[] registrationParams) {
+        GeckoEventExpecter(final String geckoEvent) {
             if (TextUtils.isEmpty(geckoEvent)) {
                 throw new IllegalArgumentException("geckoEvent must not be empty");
             }
-            if (registrationParams == null || registrationParams.length == 0) {
-                throw new IllegalArgumentException("registrationParams must not be empty");
-            }
 
             mGeckoEvent = geckoEvent;
-            mRegistrationParams = registrationParams;
             mEventDataQueue = new LinkedBlockingQueue<String>();
+
+            final GeckoEventExpecter expecter = this;
+            mListener = new GeckoEventListener() {
+                @Override
+                public void handleMessage(final String event, final JSONObject message) {
+                    FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
+                            "handleMessage called for: " + event + "; expecting: " + mGeckoEvent);
+                    mAsserter.is(event, mGeckoEvent, "Given message occurred for registered event");
+
+                    expecter.notifyOfEvent(message);
+                }
+            };
+
+            GeckoAppShell.registerEventListener(mGeckoEvent, mListener);
+            mIsRegistered = true;
         }
 
         public void blockForEvent() {
             blockForEvent(MAX_WAIT_MS, true);
         }
 
         private void blockForEvent(long millis, boolean failOnTimeout) {
-            if (mRegistrationParams == null) {
+            if (!mIsRegistered) {
                 throw new IllegalStateException("listener not registered");
             }
+
             try {
                 mEventData = mEventDataQueue.poll(millis, TimeUnit.MILLISECONDS);
             } catch (InterruptedException ie) {
                 FennecNativeDriver.log(LogLevel.ERROR, ie);
             }
             if (mEventData == null) {
                 if (failOnTimeout) {
                     FennecNativeDriver.logAllStackTraces(FennecNativeDriver.LogLevel.ERROR);
@@ -156,22 +108,23 @@ public class FennecNativeActions impleme
                 }
             } else {
                 FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
                     "unblocked on expecter for " + mGeckoEvent);
             }
         }
 
         public void blockUntilClear(long millis) {
-            if (mRegistrationParams == null) {
+            if (!mIsRegistered) {
                 throw new IllegalStateException("listener not registered");
             }
             if (millis <= 0) {
                 throw new IllegalArgumentException("millis must be > 0");
             }
+
             // wait for at least one event
             try {
                 mEventData = mEventDataQueue.poll(MAX_WAIT_MS, TimeUnit.MILLISECONDS);
             } catch (InterruptedException ie) {
                 FennecNativeDriver.log(LogLevel.ERROR, ie);
             }
             if (mEventData == null) {
                 FennecNativeDriver.logAllStackTraces(FennecNativeDriver.LogLevel.ERROR);
@@ -200,149 +153,92 @@ public class FennecNativeActions impleme
         }
 
         public String blockForEventDataWithTimeout(long millis) {
             blockForEvent(millis, false);
             return mEventData;
         }
 
         public void unregisterListener() {
-            if (mRegistrationParams == null) {
+            if (!mIsRegistered) {
                 throw new IllegalStateException("listener not registered");
             }
-            try {
-                FennecNativeDriver.log(LogLevel.INFO, "EventExpecter: no longer listening for "+mGeckoEvent);
-                mUnregisterEventListener.invoke(mRobocopApi, mRegistrationParams);
-                mRegistrationParams = null;
-            } catch (IllegalAccessException e) {
-                FennecNativeDriver.log(LogLevel.ERROR, e);
-            } catch (InvocationTargetException e) {
-                FennecNativeDriver.log(LogLevel.ERROR, e);
-            }
+
+            FennecNativeDriver.log(LogLevel.INFO,
+                    "EventExpecter: no longer listening for " + mGeckoEvent);
+
+            GeckoAppShell.unregisterEventListener(mGeckoEvent, mListener);
+            mIsRegistered = false;
         }
 
-        public synchronized boolean eventReceived() {
+        public boolean eventReceived() {
             return mEventEverReceived;
         }
 
-        void notifyOfEvent(Object[] args) {
+        void notifyOfEvent(final JSONObject message) {
             FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
-                "received event " + mGeckoEvent);
-            synchronized (this) {
-                mEventEverReceived = true;
-            }
+                    "received event " + mGeckoEvent);
+
+            mEventEverReceived = true;
+
             try {
-                mEventDataQueue.put(args[1].toString());
+                mEventDataQueue.put(message.toString());
             } catch (InterruptedException e) {
                 FennecNativeDriver.log(LogLevel.ERROR,
-                    "EventExpecter dropped event: "+args[1].toString());
-                FennecNativeDriver.log(LogLevel.ERROR, e);
+                    "EventExpecter dropped event: " + message.toString(), e);
             }
         }
     }
 
-    public RepeatedEventExpecter expectGeckoEvent(String geckoEvent) {
-        FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
-            "waiting for "+geckoEvent);
-        try {
-            Object[] finalParams = new Object[2];
-            finalParams[0] = geckoEvent;
-            GeckoEventExpecter expecter = new GeckoEventExpecter(geckoEvent, finalParams);
-            wakeInvocationHandler wIH = new wakeInvocationHandler(expecter);
-            Object proxy = Proxy.newProxyInstance(mClassLoader, new Class[] { mEventListenerClass }, wIH);
-            finalParams[1] = proxy;
-
-            mRegisterEventListener.invoke(mRobocopApi, finalParams);
-            return expecter;
-        } catch (IllegalAccessException e) {
-            FennecNativeDriver.log(LogLevel.ERROR, e);
-        } catch (InvocationTargetException e) {
-            FennecNativeDriver.log(LogLevel.ERROR, e);
-        }
-        return null;
+    public RepeatedEventExpecter expectGeckoEvent(final String geckoEvent) {
+        FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG, "waiting for " + geckoEvent);
+        return new GeckoEventExpecter(geckoEvent);
     }
 
-    public void sendGeckoEvent(String geckoEvent, String data) {
-        try {
-            mBroadcastEvent.invoke(mRobocopApi, geckoEvent, data);
-        } catch (IllegalAccessException e) {
-            FennecNativeDriver.log(LogLevel.ERROR, e);
-        } catch (InvocationTargetException e) {
-            FennecNativeDriver.log(LogLevel.ERROR, e);
-        }
-    }
-
-    private void sendPreferencesEvent(Method method, int requestId, String[] prefNames) {
-        try {
-            method.invoke(mRobocopApi, requestId, prefNames);
-        } catch (IllegalAccessException e) {
-            FennecNativeDriver.log(LogLevel.ERROR, e);
-        } catch (InvocationTargetException e) {
-            FennecNativeDriver.log(LogLevel.ERROR, e);
-        }
+    public void sendGeckoEvent(final String geckoEvent, final String data) {
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(geckoEvent, data));
     }
 
     public void sendPreferencesGetEvent(int requestId, String[] prefNames) {
-        sendPreferencesEvent(mPreferencesGetEvent, requestId, prefNames);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createPreferencesGetEvent(requestId, prefNames));
     }
 
     public void sendPreferencesObserveEvent(int requestId, String[] prefNames) {
-        sendPreferencesEvent(mPreferencesObserveEvent, requestId, prefNames);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createPreferencesObserveEvent(requestId, prefNames));
     }
 
     public void sendPreferencesRemoveObserversEvent(int requestId) {
-        try {
-            mPreferencesRemoveObserversEvent.invoke(mRobocopApi, requestId);
-        } catch (IllegalAccessException e) {
-            FennecNativeDriver.log(LogLevel.ERROR, e);
-        } catch (InvocationTargetException e) {
-            FennecNativeDriver.log(LogLevel.ERROR, e);
-        } 
-    }
-
-    class DrawListenerProxy implements InvocationHandler {
-        private final PaintExpecter mPaintExpecter;
-
-        DrawListenerProxy(PaintExpecter paintExpecter) {
-            mPaintExpecter = paintExpecter;
-        }
-
-        public Object invoke(Object proxy, Method method, Object[] args) {
-            String methodName = method.getName();
-            if ("drawFinished".equals(methodName)) {
-                FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
-                    "Received drawFinished notification");
-                mPaintExpecter.notifyOfEvent(args);
-            } else if ("toString".equals(methodName)) {
-                return "DrawListenerProxy";
-            } else if ("equals".equals(methodName)) {
-                return false;
-            } else if ("hashCode".equals(methodName)) {
-                return 0;
-            }
-            return null;
-        }
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createPreferencesRemoveObserversEvent(requestId));
     }
 
     class PaintExpecter implements RepeatedEventExpecter {
-        private boolean mPaintDone;
-        private boolean mListening;
         private static final int MAX_WAIT_MS = 90000;
 
-        PaintExpecter() throws IllegalAccessException, InvocationTargetException {
-            Object proxy = Proxy.newProxyInstance(mClassLoader, new Class[] { mDrawListenerClass }, new DrawListenerProxy(this));
-            mSetDrawListener.invoke(mRobocopApi, proxy);
+        private boolean mPaintDone;
+        private boolean mListening;
+
+        private final GeckoLayerClient mLayerClient;
+
+        PaintExpecter() {
+            final PaintExpecter expecter = this;
+            mLayerClient = GeckoAppShell.getLayerView().getLayerClient();
+            mLayerClient.setDrawListener(new DrawListener() {
+                @Override
+                public void drawFinished() {
+                    FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
+                            "Received drawFinished notification");
+                    expecter.notifyOfEvent();
+                }
+            });
             mListening = true;
         }
 
-        void notifyOfEvent(Object[] args) {
-            synchronized (this) {
-                mPaintDone = true;
-                this.notifyAll();
-            }
+        private synchronized void notifyOfEvent() {
+            mPaintDone = true;
+            this.notifyAll();
         }
 
         private synchronized void blockForEvent(long millis, boolean failOnTimeout) {
             if (!mListening) {
                 throw new IllegalStateException("draw listener not registered");
             }
             long startTime = SystemClock.uptimeMillis();
             long endTime = 0;
@@ -426,33 +322,26 @@ public class FennecNativeActions impleme
                 startTime = endTime;
             }
         }
 
         public synchronized void unregisterListener() {
             if (!mListening) {
                 throw new IllegalStateException("listener not registered");
             }
-            try {
-                FennecNativeDriver.log(LogLevel.INFO, "PaintExpecter: no longer listening for events");
-                mListening = false;
-                mSetDrawListener.invoke(mRobocopApi, (Object)null);
-            } catch (Exception e) {
-                FennecNativeDriver.log(LogLevel.ERROR, e);
-            }
+
+            FennecNativeDriver.log(LogLevel.INFO,
+                    "PaintExpecter: no longer listening for events");
+            mLayerClient.setDrawListener(null);
+            mListening = false;
         }
     }
 
     public RepeatedEventExpecter expectPaint() {
-        try {
-            return new PaintExpecter();
-        } catch (Exception e) {
-            FennecNativeDriver.log(LogLevel.ERROR, e);
-            return null;
-        }
+        return new PaintExpecter();
     }
 
     public void sendSpecialKey(SpecialKey button) {
         switch(button) {
             case DOWN:
                 sendKeyCode(KeyEvent.KEYCODE_DPAD_DOWN);
                 break;
             case UP:
@@ -490,19 +379,12 @@ public class FennecNativeActions impleme
     public void sendKeys(String input) {
         mInstr.sendStringSync(input);
     }
 
     public void drag(int startingX, int endingX, int startingY, int endingY) {
         mSolo.drag(startingX, endingX, startingY, endingY, 10);
     }
 
-    public Cursor querySql(String dbPath, String sql) {
-        try {
-            return (Cursor)mQuerySql.invoke(mRobocopApi, dbPath, sql);
-        } catch(InvocationTargetException ex) {
-            Log.e(LOGTAG, "Error invoking method", ex);
-        } catch(IllegalAccessException ex) {
-            Log.e(LOGTAG, "Error using field", ex);
-        }
-        return null;
+    public Cursor querySql(final String dbPath, final String sql) {
+        return new SQLiteBridge(dbPath).rawQuery(sql, null);
     }
 }
--- a/build/mobile/robocop/FennecNativeDriver.java
+++ b/build/mobile/robocop/FennecNativeDriver.java
@@ -1,35 +1,34 @@
 /* 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/. */
 
 package org.mozilla.gecko;
 
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.gfx.PanningPerfAPI;
+import org.mozilla.gecko.util.GeckoEventListener;
+
 import java.io.BufferedOutputStream;
 import java.io.BufferedReader;
 import java.io.DataOutputStream;
 import java.io.File;
 import java.io.FileReader;
 import java.io.FileWriter;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.nio.IntBuffer;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Proxy;
-import java.lang.reflect.InvocationHandler;
-
 import android.app.Activity;
 import android.opengl.GLSurfaceView;
 import android.view.View;
 import android.util.Log;
 
 import org.json.*;
 
 import com.jayway.android.robotium.solo.Solo;
@@ -41,28 +40,16 @@ public class FennecNativeDriver implemen
     private HashMap mLocators = null;
     private Activity mActivity;
     private Solo mSolo;
     private String mRootPath;
 
     private static String mLogFile = null;
     private static LogLevel mLogLevel = LogLevel.INFO;
 
-    // Objects for reflexive access of fennec classes.
-    private ClassLoader mClassLoader;
-    private Class mApiClass;
-    private Class mEventListenerClass;
-    private Method mRegisterEventListener;
-    private Method mGetPixels;
-    private Method mStartFrameRecording;
-    private Method mStopFrameRecording;
-    private Method mStartCheckerboardRecording;
-    private Method mStopCheckerboardRecording;
-    private Object mRobocopApi;
-
     public enum LogLevel {
         DEBUG(1),
         INFO(2),
         WARN(3),
         ERROR(4);
 
         private int mValue;
         LogLevel(int value) {
@@ -78,35 +65,16 @@ public class FennecNativeDriver implemen
 
     public FennecNativeDriver(Activity activity, Solo robocop, String rootPath) {
         mActivity = activity;
         mSolo = robocop;
         mRootPath = rootPath;
 
         // Set up table of fennec_ids.
         mLocators = convertTextToTable(getFile(mRootPath + "/fennec_ids.txt"));
-
-        // Set up reflexive access of java classes and methods.
-        try {
-            mClassLoader = activity.getClassLoader();
-
-            mApiClass = mClassLoader.loadClass("org.mozilla.gecko.RobocopAPI");
-            mEventListenerClass = mClassLoader.loadClass("org.mozilla.gecko.util.GeckoEventListener");
-
-            mRegisterEventListener = mApiClass.getMethod("registerEventListener", String.class, mEventListenerClass);
-            mGetPixels = mApiClass.getMethod("getViewPixels", View.class);
-            mStartFrameRecording = mApiClass.getDeclaredMethod("startFrameTimeRecording");
-            mStopFrameRecording = mApiClass.getDeclaredMethod("stopFrameTimeRecording");
-            mStartCheckerboardRecording = mApiClass.getDeclaredMethod("startCheckerboardRecording");
-            mStopCheckerboardRecording = mApiClass.getDeclaredMethod("stopCheckerboardRecording");
-
-            mRobocopApi = mApiClass.getConstructor(Activity.class).newInstance(activity);
-        } catch (Exception e) {
-            log(LogLevel.ERROR, e);
-        }
     }
 
     //Information on the location of the Gecko Frame.
     private boolean mGeckoInfo = false;
     private int mGeckoTop = 100;
     private int mGeckoLeft = 0;
     private int mGeckoHeight= 700;
     private int mGeckoWidth = 1024;
@@ -167,109 +135,69 @@ public class FennecNativeDriver implemen
             return new FennecNativeElement(Integer.decode((String)mLocators.get(name)), activity, mSolo);
         }
         FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR,
             "findElement: Element '"+name+"' does not exist in the list");
         return null;
     }
 
     public void startFrameRecording() {
-        try {
-            mStartFrameRecording.invoke(null);
-        } catch (IllegalAccessException e) {
-            log(LogLevel.ERROR, e);
-        } catch (InvocationTargetException e) {
-            log(LogLevel.ERROR, e);
-        }
+        PanningPerfAPI.startFrameTimeRecording();
     }
 
     public int stopFrameRecording() {
-        try {
-            List<Long> frames = (List<Long>)mStopFrameRecording.invoke(null);
-            int badness = 0;
-            for (int i = 1; i < frames.size(); i++) {
-                long frameTime = frames.get(i) - frames.get(i - 1);
-                int delay = (int)(frameTime - FRAME_TIME_THRESHOLD);
-                // for each frame we miss, add the square of the delay. This
-                // makes large delays much worse than small delays.
-                if (delay > 0) {
-                    badness += delay * delay;
-                }
+        final List<Long> frames = PanningPerfAPI.stopFrameTimeRecording();
+        int badness = 0;
+        for (int i = 1; i < frames.size(); i++) {
+            long frameTime = frames.get(i) - frames.get(i - 1);
+            int delay = (int)(frameTime - FRAME_TIME_THRESHOLD);
+            // for each frame we miss, add the square of the delay. This
+            // makes large delays much worse than small delays.
+            if (delay > 0) {
+                badness += delay * delay;
             }
-            // Don't do any averaging of the numbers because really we want to
-            // know how bad the jank was at its worst
-            return badness;
-        } catch (IllegalAccessException e) {
-            log(LogLevel.ERROR, e);
-        } catch (InvocationTargetException e) {
-            log(LogLevel.ERROR, e);
         }
 
-        // higher values are worse, and the test failing is the worst!
-        return Integer.MAX_VALUE;
+        // Don't do any averaging of the numbers because really we want to
+        // know how bad the jank was at its worst
+        return badness;
     }
 
     public void startCheckerboardRecording() {
-        try {
-            mStartCheckerboardRecording.invoke(null);
-        } catch (IllegalAccessException e) {
-            log(LogLevel.ERROR, e);
-        } catch (InvocationTargetException e) {
-            log(LogLevel.ERROR, e);
-        }
+        PanningPerfAPI.startCheckerboardRecording();
     }
 
     public float stopCheckerboardRecording() {
-        try {
-            List<Float> checkerboard = (List<Float>)mStopCheckerboardRecording.invoke(null);
-            float total = 0;
-            for (float val : checkerboard) {
-                total += val;
-            }
-            return total * 100.0f;
-        } catch (IllegalAccessException e) {
-            log(LogLevel.ERROR, e);
-        } catch (InvocationTargetException e) {
-            log(LogLevel.ERROR, e);
+        final List<Float> checkerboard = PanningPerfAPI.stopCheckerboardRecording();
+        float total = 0;
+        for (float val : checkerboard) {
+            total += val;
         }
-
-        return 0.0f;
+        return total * 100.0f;
     }
 
-    private View getSurfaceView() {
-        ArrayList<View> views = mSolo.getCurrentViews();
-        try {
-            Class c = Class.forName("org.mozilla.gecko.gfx.LayerView");
-            for (View v : views) {
-                if (c.isInstance(v)) {
-                    return v;
-                }
+    private LayerView getSurfaceView() {
+        final LayerView layerView = mSolo.getView(LayerView.class, 0);
+
+        if (layerView == null) {
+            log(LogLevel.WARN, "getSurfaceView could not find LayerView");
+            for (final View v : mSolo.getViews()) {
+                log(LogLevel.WARN, "  View: " + v);
             }
-        } catch (ClassNotFoundException e) {
-            log(LogLevel.ERROR, e);
         }
-        log(LogLevel.WARN, "getSurfaceView could not find LayerView");
-        for (View v : views) {
-            log(LogLevel.WARN, v.toString());
-        }
-        return null;
+        return layerView;
     }
 
     public PaintedSurface getPaintedSurface() {
-        View view = getSurfaceView();
+        final LayerView view = getSurfaceView();
         if (view == null) {
             return null;
         }
-        IntBuffer pixelBuffer;
-        try {
-            pixelBuffer = (IntBuffer)mGetPixels.invoke(mRobocopApi, view);
-        } catch (Exception e) {
-            log(LogLevel.ERROR, e);
-            return null;
-        }
+
+        final IntBuffer pixelBuffer = view.getPixels();
 
         // now we need to (1) flip the image, because GL likes to do things up-side-down,
         // and (2) rearrange the bits from AGBR-8888 to ARGB-8888.
         int w = view.getWidth();
         int h = view.getHeight();
         pixelBuffer.position(0);
         String mapFile = mRootPath + "/pixels.map";
 
@@ -307,62 +235,44 @@ public class FennecNativeDriver implemen
         }
         return new PaintedSurface(mapFile, w, h);
     }
 
     public int mHeight=0;
     public int mScrollHeight=0;
     public int mPageHeight=10;
 
-    class scrollHandler implements InvocationHandler {
-        public scrollHandler(){};
-        public Object invoke(Object proxy, Method method, Object[] args) {
-            try {
-                // Disect the JSON object into the appropriate variables 
-                JSONObject jo = ((JSONObject)args[1]);
-                mScrollHeight = jo.getInt("y");
-                mHeight = jo.getInt("cheight");
-                // We don't want a height of 0. That means it's a bad response.
-                if (mHeight > 0) {
-                    mPageHeight = jo.getInt("height");
-                }
-
-            } catch( Throwable e) {
-                FennecNativeDriver.log(FennecNativeDriver.LogLevel.WARN, 
-                    "WARNING: ScrollReceived, but read wrong!");
-            }
-            return null;
-        }
-    }
-
     public int getScrollHeight() {
         return mScrollHeight;
     }
     public int getPageHeight() {
         return mPageHeight;
     }
     public int getHeight() {
         return mHeight;
     }
 
     public void setupScrollHandling() {
-        //Setup scrollHandler to catch "robocop:scroll" events. 
-        try {
-            Class [] interfaces = new Class[1];
-            interfaces[0] = mEventListenerClass;
-            Object[] finalParams = new Object[2];
-            finalParams[0] = "robocop:scroll";
-            finalParams[1] = Proxy.newProxyInstance(mClassLoader, interfaces, new scrollHandler());
-            mRegisterEventListener.invoke(mRobocopApi, finalParams);
-        } catch (IllegalAccessException e) {
-            log(LogLevel.ERROR, e);
-        } catch (InvocationTargetException e) {
-            log(LogLevel.ERROR, e);
-        }
-
+        GeckoAppShell.registerEventListener("robocop:scroll", new GeckoEventListener() {
+            @Override
+            public void handleMessage(final String event, final JSONObject message) {
+                try {
+                    mScrollHeight = message.getInt("y");
+                    mHeight = message.getInt("cheight");
+                    // We don't want a height of 0. That means it's a bad response.
+                    if (mHeight > 0) {
+                        mPageHeight = message.getInt("height");
+                    }
+                } catch (JSONException e) {
+                    FennecNativeDriver.log(FennecNativeDriver.LogLevel.WARN,
+                            "WARNING: ScrollReceived, but message does not contain " +
+                            "expected fields: " + e);
+                }
+            }
+        });
     }
 
     /**
      *  Takes a filename, loads the file, and returns a string version of the entire file.
      */
     public static String getFile(String filename)
     {
         StringBuilder text = new StringBuilder();
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -320,16 +320,17 @@ public class GeckoAppShell
     }
 
     private static LayerView sLayerView;
 
     public static void setLayerView(LayerView lv) {
         sLayerView = lv;
     }
 
+    @RobocopTarget
     public static LayerView getLayerView() {
         return sLayerView;
     }
 
     public static void runGecko(String apkPath, String args, String url, String type) {
         // Preparation for pumpMessageLoop()
         MessageQueue.IdleHandler idleHandler = new MessageQueue.IdleHandler() {
             @Override public boolean queueIdle() {
@@ -387,16 +388,17 @@ public class GeckoAppShell
         try {
             while (!gPendingEvents.isEmpty()) {
                 GeckoEvent e = gPendingEvents.removeFirst();
                 notifyGeckoOfEvent(e);
             }
         } catch (NoSuchElementException e) {}
     }
 
+    @RobocopTarget
     public static void sendEventToGecko(GeckoEvent e) {
         if (GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) {
             notifyGeckoOfEvent(e);
         } else {
             gPendingEvents.addLast(e);
         }
     }
 
@@ -2285,31 +2287,33 @@ public class GeckoAppShell
 
     /**
      * Adds a listener for a gecko event.
      * This method is thread-safe and may be called at any time. In particular, calling it
      * with an event that is currently being processed has the properly-defined behaviour that
      * any added listeners will not be invoked on the event currently being processed, but
      * will be invoked on future events of that type.
      */
+    @RobocopTarget
     public static void registerEventListener(String event, GeckoEventListener listener) {
         sEventDispatcher.registerEventListener(event, listener);
     }
 
     public static EventDispatcher getEventDispatcher() {
         return sEventDispatcher;
     }
 
     /**
      * Remove a previously-registered listener for a gecko event.
      * This method is thread-safe and may be called at any time. In particular, calling it
      * with an event that is currently being processed has the properly-defined behaviour that
      * any removed listeners will still be invoked on the event currently being processed, but
      * will not be invoked on future events of that type.
      */
+    @RobocopTarget
     public static void unregisterEventListener(String event, GeckoEventListener listener) {
         sEventDispatcher.unregisterEventListener(event, listener);
     }
 
     /*
      * Battery API related methods.
      */
     @WrapElementForJNI
--- a/mobile/android/base/GeckoEvent.java
+++ b/mobile/android/base/GeckoEvent.java
@@ -5,16 +5,17 @@
 
 package org.mozilla.gecko;
 
 import org.mozilla.gecko.gfx.DisplayPortMetrics;
 import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
 import org.mozilla.gecko.mozglue.JNITarget;
 import org.mozilla.gecko.mozglue.generatorannotations.GeneratorOptions;
 import org.mozilla.gecko.mozglue.generatorannotations.WrapEntireClassForJNI;
+import org.mozilla.gecko.mozglue.RobocopTarget;
 
 import android.content.res.Resources;
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.hardware.Sensor;
 import android.hardware.SensorEvent;
 import android.hardware.SensorManager;
@@ -607,16 +608,17 @@ public class GeckoEvent {
     public static GeckoEvent createSizeChangedEvent(int w, int h, int screenw, int screenh) {
         GeckoEvent event = new GeckoEvent(NativeGeckoEvent.SIZE_CHANGED);
         event.mPoints = new Point[2];
         event.mPoints[0] = new Point(w, h);
         event.mPoints[1] = new Point(screenw, screenh);
         return event;
     }
 
+    @RobocopTarget
     public static GeckoEvent createBroadcastEvent(String subject, String data) {
         GeckoEvent event = new GeckoEvent(NativeGeckoEvent.BROADCAST);
         event.mCharacters = subject;
         event.mCharactersExtra = data;
         return event;
     }
 
     public static GeckoEvent createViewportEvent(ImmutableViewportMetrics metrics, DisplayPortMetrics displayPort) {
@@ -697,30 +699,33 @@ public class GeckoEvent {
     }
 
     public static GeckoEvent createRemoveObserverEvent(String observerKey) {
         GeckoEvent event = new GeckoEvent(NativeGeckoEvent.REMOVE_OBSERVER);
         event.mCharacters = observerKey;
         return event;
     }
 
+    @RobocopTarget
     public static GeckoEvent createPreferencesObserveEvent(int requestId, String[] prefNames) {
         GeckoEvent event = new GeckoEvent(NativeGeckoEvent.PREFERENCES_OBSERVE);
         event.mCount = requestId;
         event.mPrefNames = prefNames;
         return event;
     }
 
+    @RobocopTarget
     public static GeckoEvent createPreferencesGetEvent(int requestId, String[] prefNames) {
         GeckoEvent event = new GeckoEvent(NativeGeckoEvent.PREFERENCES_GET);
         event.mCount = requestId;
         event.mPrefNames = prefNames;
         return event;
     }
 
+    @RobocopTarget
     public static GeckoEvent createPreferencesRemoveObserversEvent(int requestId) {
         GeckoEvent event = new GeckoEvent(NativeGeckoEvent.PREFERENCES_REMOVE_OBSERVERS);
         event.mCount = requestId;
         return event;
     }
 
     public static GeckoEvent createLowMemoryEvent(int level) {
         GeckoEvent event = new GeckoEvent(NativeGeckoEvent.LOW_MEMORY);
deleted file mode 100644
--- a/mobile/android/base/RobocopAPI.java
+++ /dev/null
@@ -1,101 +0,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/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.gfx.GeckoLayerClient;
-import org.mozilla.gecko.gfx.LayerView;
-import org.mozilla.gecko.gfx.PanningPerfAPI;
-import org.mozilla.gecko.mozglue.GeckoLoader;
-import org.mozilla.gecko.mozglue.RobocopTarget;
-import org.mozilla.gecko.sqlite.SQLiteBridge;
-import org.mozilla.gecko.util.GeckoEventListener;
-
-import android.app.Activity;
-import android.database.Cursor;
-import android.view.View;
-
-import java.nio.IntBuffer;
-import java.util.List;
-
-/**
- * Class to provide wrapper methods around methods wanted by Robocop.
- *
- * This class provides fixed entry points into code that is liable to be optimised by Proguard without
- * needing to prevent Proguard from optimising the wrapped methods.
- * Wrapping in this way still slightly hinders Proguard's ability to optimise.
- *
- * If you find yourself wanting to add a method to this class - proceed with caution. If you're writing
- * a test that's not about manipulating the UI, you might be better off using JUnit (Or similar)
- * instead of Robocop.
- *
- * Alternatively, you might be able to get what you want by reflecting on a method annotated for the
- * benefit of the C++ wrapper generator - these methods are sure to not disappear at compile-time.
- * 
- * Finally, you might be able to get what you want via Reflection on Android's libraries. Those are
- * also not prone to vanishing at compile-time, but doing this might substantially complicate your
- * work, ultimately not proving worth the extra effort to avoid making a slight mess here.
- */
-@RobocopTarget
-public class RobocopAPI {
-    private final GeckoApp mGeckoApp;
-
-    public RobocopAPI(Activity activity) {
-        mGeckoApp = (GeckoApp)activity;
-    }
-
-    public void registerEventListener(String event, GeckoEventListener listener) {
-        GeckoAppShell.registerEventListener(event, listener);
-    }
-
-    public void unregisterEventListener(String event, GeckoEventListener listener) {
-        GeckoAppShell.unregisterEventListener(event, listener);
-    }
-
-    public void broadcastEvent(String subject, String data) {
-        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(subject, data));
-    }
-
-    public void preferencesGetEvent(int requestId, String[] prefNames) {
-        GeckoAppShell.sendEventToGecko(GeckoEvent.createPreferencesGetEvent(requestId, prefNames));
-    }
-
-    public void preferencesObserveEvent(int requestId, String[] prefNames) {
-        GeckoAppShell.sendEventToGecko(GeckoEvent.createPreferencesObserveEvent(requestId, prefNames));
-    }
-
-    public void preferencesRemoveObserversEvent(int requestId) {
-        GeckoAppShell.sendEventToGecko(GeckoEvent.createPreferencesRemoveObserversEvent(requestId));
-    }
-
-    public void setDrawListener(GeckoLayerClient.DrawListener listener) {
-        GeckoAppShell.getLayerView().getLayerClient().setDrawListener(listener);
-    }
-
-    public Cursor querySql(String dbPath, String query) {
-        GeckoLoader.loadSQLiteLibs(mGeckoApp, mGeckoApp.getApplication().getPackageResourcePath());
-        return new SQLiteBridge(dbPath).rawQuery(query, null);
-    }
-
-    public IntBuffer getViewPixels(View view) {
-        return ((LayerView)view).getPixels();
-    }
-
-    // PanningPerfAPI.
-    public static void startFrameTimeRecording() {
-        PanningPerfAPI.startFrameTimeRecording();
-    }
-
-    public static List<Long> stopFrameTimeRecording() {
-        return PanningPerfAPI.stopFrameTimeRecording();
-    }
-
-    public static void startCheckerboardRecording() {
-        PanningPerfAPI.startCheckerboardRecording();
-    }
-
-    public static List<Float> stopCheckerboardRecording() {
-        return PanningPerfAPI.stopCheckerboardRecording();
-    }
-}
--- a/mobile/android/base/gfx/LayerView.java
+++ b/mobile/android/base/gfx/LayerView.java
@@ -10,16 +10,17 @@ import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.PrefsHelper;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.TouchEventInterceptor;
 import org.mozilla.gecko.ZoomConstraints;
 import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI;
+import org.mozilla.gecko.mozglue.RobocopTarget;
 import org.mozilla.gecko.util.EventDispatcher;
 
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.PixelFormat;
@@ -319,16 +320,17 @@ public class LayerView extends FrameLayo
             addView(mSurfaceView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
 
             SurfaceHolder holder = mSurfaceView.getHolder();
             holder.addCallback(new SurfaceListener());
             holder.setFormat(PixelFormat.RGB_565);
         }
     }
 
+    @RobocopTarget
     public GeckoLayerClient getLayerClient() { return mLayerClient; }
     public PanZoomController getPanZoomController() { return mPanZoomController; }
     public LayerMarginsAnimator getLayerMarginsAnimator() { return mMarginsAnimator; }
 
     public ImmutableViewportMetrics getViewportMetrics() {
         return mLayerClient.getViewportMetrics();
     }
 
@@ -451,16 +453,17 @@ public class LayerView extends FrameLayo
         mRenderer.removeRenderTask(task);
     }
 
     public int getMaxTextureSize() {
         return mRenderer.getMaxTextureSize();
     }
 
     /** Used by robocop for testing purposes. Not for production use! */
+    @RobocopTarget
     public IntBuffer getPixels() {
         return mRenderer.getPixels();
     }
 
     /* paintState must be a PAINT_xxx constant. */
     public void setPaintState(int paintState) {
         mPaintState = paintState;
     }
--- a/mobile/android/base/gfx/PanningPerfAPI.java
+++ b/mobile/android/base/gfx/PanningPerfAPI.java
@@ -1,15 +1,17 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * 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/. */
 
 package org.mozilla.gecko.gfx;
 
+import org.mozilla.gecko.mozglue.RobocopTarget;
+
 import android.os.SystemClock;
 import android.util.Log;
 
 import java.util.ArrayList;
 import java.util.List;
 
 public class PanningPerfAPI {
     private static final String LOGTAG = "GeckoPanningPerfAPI";
@@ -35,26 +37,28 @@ public class PanningPerfAPI {
         }
         if (mCheckerboardAmounts == null) {
             mCheckerboardAmounts = new ArrayList<Float>(EXPECTED_FRAME_COUNT);
         } else {
             mCheckerboardAmounts.clear();
         }
     }
 
+    @RobocopTarget
     public static void startFrameTimeRecording() {
         if (mRecordingFrames || mRecordingCheckerboard) {
             Log.e(LOGTAG, "Error: startFrameTimeRecording() called while already recording!");
             return;
         }
         mRecordingFrames = true;
         initialiseRecordingArrays();
         mFrameStartTime = SystemClock.uptimeMillis();
     }
 
+    @RobocopTarget
     public static List<Long> stopFrameTimeRecording() {
         if (!mRecordingFrames) {
             Log.e(LOGTAG, "Error: stopFrameTimeRecording() called when not recording!");
             return null;
         }
         mRecordingFrames = false;
         return mFrameTimes;
     }
@@ -65,26 +69,28 @@ public class PanningPerfAPI {
             mFrameTimes.add(SystemClock.uptimeMillis() - mFrameStartTime);
         }
     }
 
     public static boolean isRecordingCheckerboard() {
         return mRecordingCheckerboard;
     }
 
+    @RobocopTarget
     public static void startCheckerboardRecording() {
         if (mRecordingCheckerboard || mRecordingFrames) {
             Log.e(LOGTAG, "Error: startCheckerboardRecording() called while already recording!");
             return;
         }
         mRecordingCheckerboard = true;
         initialiseRecordingArrays();
         mCheckerboardStartTime = SystemClock.uptimeMillis();
     }
 
+    @RobocopTarget
     public static List<Float> stopCheckerboardRecording() {
         if (!mRecordingCheckerboard) {
             Log.e(LOGTAG, "Error: stopCheckerboardRecording() called when not recording!");
             return null;
         }
         mRecordingCheckerboard = false;
 
         // We take the number of values in mCheckerboardAmounts here, as there's
--- a/mobile/android/base/home/BrowserSearch.java
+++ b/mobile/android/base/home/BrowserSearch.java
@@ -320,66 +320,140 @@ public class BrowserSearch extends HomeF
     }
 
     @Override
     protected void load() {
         SearchLoader.init(getLoaderManager(), LOADER_ID_SEARCH, mCursorLoaderCallbacks, mSearchTerm);
     }
 
     private void handleAutocomplete(String searchTerm, Cursor c) {
-        if (TextUtils.isEmpty(mSearchTerm) || c == null || mAutocompleteHandler == null) {
+        if (c == null ||
+            mAutocompleteHandler == null ||
+            TextUtils.isEmpty(searchTerm)) {
             return;
         }
 
         // Avoid searching the path if we don't have to. Currently just
-        // decided by if there is a '/' character in the string.
-        final boolean searchPath = (searchTerm.indexOf("/") > 0);
+        // decided by whether there is a '/' character in the string.
+        final boolean searchPath = searchTerm.indexOf('/') > 0;
         final String autocompletion = findAutocompletion(searchTerm, c, searchPath);
 
-        if (autocompletion != null && mAutocompleteHandler != null) {
-            mAutocompleteHandler.onAutocomplete(autocompletion);
-            mAutocompleteHandler = null;
+        if (autocompletion == null || mAutocompleteHandler == null) {
+            return;
         }
+
+        // Prefetch auto-completed domain since it's a likely target
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Session:Prefetch", "http://" + autocompletion));
+
+        mAutocompleteHandler.onAutocomplete(autocompletion);
+        mAutocompleteHandler = null;
+    }
+
+    /**
+     * Returns the substring of a provided URI, starting at the given offset,
+     * and extending up to the end of the path segment in which the provided
+     * index is found.
+     *
+     * For example, given
+     *
+     *   "www.reddit.com/r/boop/abcdef", 0, ?
+     *
+     * this method returns
+     *
+     *   ?=2:  "www.reddit.com/"
+     *   ?=17: "www.reddit.com/r/boop/"
+     *   ?=21: "www.reddit.com/r/boop/"
+     *   ?=22: "www.reddit.com/r/boop/abcdef"
+     *
+     */
+    private static String uriSubstringUpToMatchedPath(final String url, final int offset, final int begin) {
+        final int afterEnd = url.length();
+
+        // We want to include the trailing slash, but not other characters.
+        int chop = url.indexOf('/', begin);
+        if (chop != -1) {
+            ++chop;
+            if (chop < offset) {
+                // This isn't supposed to happen. Fall back to returning the whole damn thing.
+                return url;
+            }
+        } else {
+            chop = url.indexOf('?', begin);
+            if (chop == -1) {
+                chop = url.indexOf('#', begin);
+            }
+            if (chop == -1) {
+                chop = afterEnd;
+            }
+        }
+
+        return url.substring(offset, chop);
     }
 
     private String findAutocompletion(String searchTerm, Cursor c, boolean searchPath) {
         if (!c.moveToFirst()) {
             return null;
         }
 
+        final int searchLength = searchTerm.length();
         final int urlIndex = c.getColumnIndexOrThrow(URLColumns.URL);
         int searchCount = 0;
 
         do {
-            final Uri url = Uri.parse(c.getString(urlIndex));
-            final String host = StringUtils.stripCommonSubdomains(url.getHost());
+            final String url = c.getString(urlIndex);
+
+            if (searchCount == 0) {
+                // Prefetch the first item in the list since it's weighted the highest
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Session:Prefetch", url.toString()));
+            }
 
-            // Host may be null for about pages
+            // Does the completion match against the whole URL? This will match
+            // about: pages, as well as user input including "http://...".
+            if (url.startsWith(searchTerm)) {
+                return uriSubstringUpToMatchedPath(url, 0, searchLength);
+            }
+
+            final Uri uri = Uri.parse(url);
+            final String host = uri.getHost();
+
+            // Host may be null for about pages.
             if (host == null) {
                 continue;
             }
 
-            final StringBuilder hostBuilder = new StringBuilder(host);
-            if (hostBuilder.indexOf(searchTerm) == 0) {
-                return hostBuilder.append("/").toString();
+            if (host.startsWith(searchTerm)) {
+                return host + "/";
+            }
+
+            final String strippedHost = StringUtils.stripCommonSubdomains(host);
+            if (strippedHost.startsWith(searchTerm)) {
+                return strippedHost + "/";
+            }
+
+            ++searchCount;
+
+            if (!searchPath) {
+                continue;
             }
 
-            if (searchPath) {
-                final List<String> path = url.getPathSegments();
-
-                for (String s : path) {
-                    hostBuilder.append("/").append(s);
-
-                    if (hostBuilder.indexOf(searchTerm) == 0) {
-                        return hostBuilder.append("/").toString();
-                    }
-                }
+            // Otherwise, if we're matching paths, let's compare against the string itself.
+            final int hostOffset = url.indexOf(strippedHost);
+            if (hostOffset == -1) {
+                // This was a URL string that parsed to a different host (normalized?).
+                // Give up.
+                continue;
             }
 
-            searchCount++;
+            // We already matched the non-stripped host, so now we're
+            // substring-searching in the part of the URL without the common
+            // subdomains.
+            if (url.startsWith(searchTerm, hostOffset)) {
+                // Great! Return including the rest of the path segment.
+                return uriSubstringUpToMatchedPath(url, hostOffset, hostOffset + searchLength);
+            }
         } while (searchCount < MAX_AUTOCOMPLETE_SEARCH && c.moveToNext());
 
         return null;
     }
 
     private void filterSuggestions() {
         if (mSuggestClient == null || !mSuggestionsEnabled) {
             return;
@@ -782,17 +856,17 @@ public class BrowserSearch extends HomeF
             return SearchLoader.createInstance(getActivity(), args);
         }
 
         @Override
         public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
             mAdapter.swapCursor(c);
 
             // We should handle autocompletion based on the search term
-            // associated with the currently loader that has just provided
+            // associated with the loader that has just provided
             // the results.
             SearchCursorLoader searchLoader = (SearchCursorLoader) loader;
             handleAutocomplete(searchLoader.getSearchTerm(), c);
         }
 
         @Override
         public void onLoaderReset(Loader<Cursor> loader) {
             mAdapter.swapCursor(null);
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -232,16 +232,18 @@ size. -->
 <!ENTITY contextmenu_top_sites_pin "Pin Site">
 <!ENTITY contextmenu_top_sites_unpin "Unpin Site">
 <!ENTITY contextmenu_add_search_engine "Add a Search Engine">
 
 <!ENTITY pref_titlebar_mode "Title bar">
 <!ENTITY pref_titlebar_mode_title "Show page title">
 <!ENTITY pref_titlebar_mode_url "Show page address">
 
+<!ENTITY pref_dynamic_toolbar "Hide title bar when scrolling">
+
 <!ENTITY history_removed "Page removed">
 
 <!ENTITY bookmark_edit_title "Edit Bookmark">
 <!ENTITY bookmark_edit_name "Name">
 <!ENTITY bookmark_edit_location "Location">
 <!ENTITY bookmark_edit_keyword "Keyword">
 
 <!-- Localization note (site_settings_*) : These strings are used in the "Site Settings"
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -268,17 +268,16 @@ gbjar.sources += [
     'prompts/IconGridInput.java',
     'prompts/Prompt.java',
     'prompts/PromptInput.java',
     'prompts/PromptService.java',
     'ReaderModeUtils.java',
     'ReferrerReceiver.java',
     'RemoteTabs.java',
     'Restarter.java',
-    'RobocopAPI.java',
     'ScrollAnimator.java',
     'ServiceNotificationClient.java',
     'SessionParser.java',
     'SharedPreferencesHelper.java',
     'SiteIdentityPopup.java',
     'SmsManager.java',
     'sqlite/ByteBufferInputStream.java',
     'sqlite/MatrixBlobCursor.java',
--- a/mobile/android/base/mozglue/GeckoLoader.java.in
+++ b/mobile/android/base/mozglue/GeckoLoader.java.in
@@ -212,16 +212,17 @@ public final class GeckoLoader {
         if (cacheDir.isDirectory()) {
             cacheDir.setWritable(true, false);
             cacheDir.setExecutable(true, false);
             cacheDir.setReadable(true, false);
         }
 #endif
     }
 
+    @RobocopTarget
     public static void loadSQLiteLibs(Context context, String apkName) {
         synchronized (sLibLoadingLock) {
             if (sSQLiteLibsLoaded) {
                 return;
             }
             sSQLiteLibsLoaded = true;
         }
 
--- a/mobile/android/base/resources/xml/preferences_display.xml
+++ b/mobile/android/base/resources/xml/preferences_display.xml
@@ -16,16 +16,19 @@
                     android:persistent="false" />
 
     <ListPreference android:key="browser.chrome.titlebarMode"
                     android:title="@string/pref_titlebar_mode"
                     android:entries="@array/pref_titlebar_mode_entries"
                     android:entryValues="@array/pref_titlebar_mode_values"
                     android:persistent="false" />
 
+    <CheckBoxPreference android:key="browser.chrome.dynamictoolbar"
+                        android:title="@string/pref_dynamic_toolbar" />
+
     <PreferenceCategory android:title="@string/pref_category_advanced">
 
         <CheckBoxPreference
                         android:key="browser.zoom.reflowOnZoom"
                         android:title="@string/pref_reflow_on_zoom"
                         android:defaultValue="false"
                         android:persistent="false" />
 
--- a/mobile/android/base/sqlite/SQLiteBridge.java
+++ b/mobile/android/base/sqlite/SQLiteBridge.java
@@ -46,16 +46,17 @@ public class SQLiteBridge {
                                                             String[] aParams,
                                                             long[] aUpdateResult)
         throws SQLiteBridgeException;
     private static native long openDatabase(String aDb)
         throws SQLiteBridgeException;
     private static native void closeDatabase(long aDb);
 
     // Takes the path to the database we want to access.
+    @RobocopTarget
     public SQLiteBridge(String aDb) throws SQLiteBridgeException {
         mDb = aDb;
     }
 
     // Executes a simple line of sql.
     public void execSQL(String sql)
                 throws SQLiteBridgeException {
         internalQuery(sql, null);
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -229,16 +229,18 @@
   <string name="contextmenu_top_sites_pin">&contextmenu_top_sites_pin;</string>
   <string name="contextmenu_top_sites_unpin">&contextmenu_top_sites_unpin;</string>
   <string name="contextmenu_add_search_engine">&contextmenu_add_search_engine;</string>
 
   <string name="pref_titlebar_mode">&pref_titlebar_mode;</string>
   <string name="pref_titlebar_mode_title">&pref_titlebar_mode_title;</string>
   <string name="pref_titlebar_mode_url">&pref_titlebar_mode_url;</string>
 
+  <string name="pref_dynamic_toolbar">&pref_dynamic_toolbar;</string>
+
   <string name="history_removed">&history_removed;</string>
 
   <string name="bookmark_edit_title">&bookmark_edit_title;</string>
   <string name="bookmark_edit_name">&bookmark_edit_name;</string>
   <string name="bookmark_edit_location">&bookmark_edit_location;</string>
   <string name="bookmark_edit_keyword">&bookmark_edit_keyword;</string>
 
   <string name="pref_use_master_password">&pref_use_master_password;</string>
--- a/mobile/android/base/tests/testInputUrlBar.java
+++ b/mobile/android/base/tests/testInputUrlBar.java
@@ -16,63 +16,65 @@ public final class testInputUrlBar exten
     private EditText mUrlBarEditView;
 
     public void testInputUrlBar() {
         blockForGeckoReady();
 
         startEditingMode();
         assertUrlBarText("about:home");
 
-        mActions.sendKeys("ab");
-        assertUrlBarText("ab");
+        // Avoid any auto domain completion by using a prefix that matches
+        //  nothing, including about: pages
+        mActions.sendKeys("zy");
+        assertUrlBarText("zy");
 
         mActions.sendKeys("cd");
-        assertUrlBarText("abcd");
+        assertUrlBarText("zycd");
 
         mActions.sendSpecialKey(Actions.SpecialKey.LEFT);
         mActions.sendSpecialKey(Actions.SpecialKey.LEFT);
 
         // Inserting "" should not do anything.
         mActions.sendKeys("");
-        assertUrlBarText("abcd");
+        assertUrlBarText("zycd");
 
         mActions.sendKeys("ef");
-        assertUrlBarText("abefcd");
+        assertUrlBarText("zyefcd");
 
         mActions.sendSpecialKey(Actions.SpecialKey.RIGHT);
         mActions.sendKeys("gh");
-        assertUrlBarText("abefcghd");
+        assertUrlBarText("zyefcghd");
 
         final EditText editText = mUrlBarEditView;
         runOnUiThreadSync(new Runnable() {
             public void run() {
                 // Select "ef"
                 editText.setSelection(2);
             }
         });
         mActions.sendKeys("op");
-        assertUrlBarText("abopefcghd");
+        assertUrlBarText("zyopefcghd");
 
         runOnUiThreadSync(new Runnable() {
             public void run() {
                 // Select "cg"
                 editText.setSelection(6, 8);
             }
         });
         mActions.sendKeys("qr");
-        assertUrlBarText("abopefqrhd");
+        assertUrlBarText("zyopefqrhd");
 
         runOnUiThreadSync(new Runnable() {
             public void run() {
                 // Select "op"
                 editText.setSelection(4,2);
             }
         });
         mActions.sendKeys("st");
-        assertUrlBarText("abstefqrhd");
+        assertUrlBarText("zystefqrhd");
 
         runOnUiThreadSync(new Runnable() {
             public void run() {
                 editText.selectAll();
             }
         });
         mActions.sendKeys("uv");
         assertUrlBarText("uv");
--- a/mobile/android/chrome/content/SelectionHandler.js
+++ b/mobile/android/chrome/content/SelectionHandler.js
@@ -384,29 +384,31 @@ var SelectionHandler = {
     SHARE: {
       label: Strings.browser.GetStringFromName("contextmenu.share"),
       id: "share_action",
       icon: "drawable://ic_menu_share",
       action: function() {
         SelectionHandler.shareSelection();
       },
       showAsAction: function(aElement) {
-        return !(aElement instanceof HTMLInputElement && aElement.mozIsTextField(false))
+        return !((aElement instanceof HTMLInputElement && aElement.mozIsTextField(false)) ||
+                 (aElement instanceof HTMLTextAreaElement));
       },
       selector: ClipboardHelper.shareContext,
     },
 
     SEARCH: {
       label: function() {
         return Strings.browser.formatStringFromName("contextmenu.search", [Services.search.defaultEngine.name], 1);
       },
       id: "search_action",
       icon: "drawable://ic_url_bar_search",
       showAsAction: function(aElement) {
-        return !(aElement instanceof HTMLInputElement && aElement.mozIsTextField(false))
+        return !((aElement instanceof HTMLInputElement && aElement.mozIsTextField(false)) ||
+                 (aElement instanceof HTMLTextAreaElement));
       },
       action: function() {
         SelectionHandler.searchSelection();
         SelectionHandler._closeSelection();
       },
       selector: ClipboardHelper.searchWithContext,
     },
 
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -4278,17 +4278,19 @@ var BrowserEventHandler = {
       closest = ElementTouchHelper.elementFromPoint(aEvent.changedTouches[0].screenX,
                                                     aEvent.changedTouches[0].screenY);
     if (!closest)
       closest = aEvent.target;
 
     if (closest) {
       let uri = this._getLinkURI(closest);
       if (uri) {
-        Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
+        try {
+          Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
+        } catch (e) {}
       }
       this._doTapHighlight(closest);
     }
   },
 
   _getLinkURI: function(aElement) {
     if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
         ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) ||
@@ -4388,17 +4390,17 @@ var BrowserEventHandler = {
             let data = JSON.parse(aData);
             let [x, y] = [data.x, data.y];
             if (ElementTouchHelper.isElementClickable(element)) {
               [x, y] = this._moveClickPoint(element, x, y);
               element = ElementTouchHelper.anyElementFromPoint(x, y);
             }
 
             // Was the element already focused before it was clicked?
-            let isFocused = (element == BrowserApp.getFocusedInput(BrowserApp.selectedBrowser, true));
+            let isFocused = (element == BrowserApp.getFocusedInput(BrowserApp.selectedBrowser));
 
             this._sendMouseEvent("mousemove", element, x, y);
             this._sendMouseEvent("mousedown", element, x, y);
             this._sendMouseEvent("mouseup",   element, x, y);
 
             // If the element was previously focused, show the caret attached to it.
             if (isFocused)
               SelectionHandler.attachCaret(element);
@@ -6702,17 +6704,19 @@ var SearchEngines = {
       }
     });
 
     // Send a speculative connection to the default engine.
     let connector = Services.io.QueryInterface(Ci.nsISpeculativeConnect);
     let searchURI = Services.search.defaultEngine.getSubmission("dummy").uri;
     let callbacks = window.QueryInterface(Ci.nsIInterfaceRequestor)
                           .getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsILoadContext);
-    connector.speculativeConnect(searchURI, callbacks);
+    try {
+      connector.speculativeConnect(searchURI, callbacks);
+    } catch (e) {}
   },
 
   _handleSearchEnginesGetAll: function _handleSearchEnginesGetAll(rv) {
     this._handleSearchEnginesGet(rv, true);
   },
   _handleSearchEnginesGetVisible: function _handleSearchEnginesGetVisible(rv) {
     this._handleSearchEnginesGet(rv, false)
   },
@@ -8104,86 +8108,117 @@ var Distribution = {
       if (!(reason instanceof OS.File.Error && reason.becauseNoSuchFile)) {
         Cu.reportError("Distribution: Could not read from " + aFile.leafName + " file");
       }
     });
   }
 };
 
 var Tabs = {
-  // This object provides functions to manage a most-recently-used list
-  // of tabs. Each tab has a timestamp associated with it that indicates when
-  // it was last touched.
-
   _enableTabExpiration: false,
+  _domains: new Set(),
 
   init: function() {
-    // on low-memory platforms, always allow tab expiration. on high-mem
-    // platforms, allow it to be turned on once we hit a low-mem situation
+    // On low-memory platforms, always allow tab expiration. On high-mem
+    // platforms, allow it to be turned on once we hit a low-mem situation.
     if (BrowserApp.isOnLowMemoryPlatform) {
       this._enableTabExpiration = true;
     } else {
       Services.obs.addObserver(this, "memory-pressure", false);
     }
+
+    Services.obs.addObserver(this, "Session:Prefetch", false);
+
+    BrowserApp.deck.addEventListener("pageshow", this, false);
   },
 
   uninit: function() {
     if (!this._enableTabExpiration) {
-      // if _enableTabExpiration is true then we won't have this
+      // If _enableTabExpiration is true then we won't have this
       // observer registered any more.
       Services.obs.removeObserver(this, "memory-pressure");
     }
+
+    Services.obs.removeObserver(this, "Session:Prefetch");
+
+    BrowserApp.deck.removeEventListener("pageshow", this);
   },
 
   observe: function(aSubject, aTopic, aData) {
-    if (aTopic == "memory-pressure" && aData != "heap-minimize") {
-      this._enableTabExpiration = true;
-      Services.obs.removeObserver(this, "memory-pressure");
+    switch (aTopic) {
+      case "memory-pressure":
+        if (aData != "heap-minimize") {
+          this._enableTabExpiration = true;
+          Services.obs.removeObserver(this, "memory-pressure");
+        }
+        break;
+      case "Session:Prefetch":
+        if (aData) {
+          let uri = Services.io.newURI(aData, null, null);
+          if (uri && !this._domains.has(uri.host)) {
+            try {
+              Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
+              this._domains.add(uri.host);
+            } catch (e) {}
+          }
+        }
+        break;
+    }
+  },
+
+  handleEvent: function(aEvent) {
+    switch (aEvent.type) {
+      case "pageshow":
+        // Clear the domain cache whenever a page get loaded into any browser.
+        this._domains.clear();
+        break;
     }
   },
 
   touch: function(aTab) {
     aTab.lastTouchedAt = Date.now();
   },
 
+  // Manage the most-recently-used list of tabs. Each tab has a timestamp
+  // associated with it that indicates when it was last touched.
   expireLruTab: function() {
     if (!this._enableTabExpiration) {
       return false;
     }
     let expireTimeMs = Services.prefs.getIntPref("browser.tabs.expireTime") * 1000;
     if (expireTimeMs < 0) {
-      // this behaviour is disabled
+      // This behaviour is disabled.
       return false;
     }
     let tabs = BrowserApp.tabs;
     let selected = BrowserApp.selectedTab;
     let lruTab = null;
-    // find the least recently used non-zombie tab
+    // Find the least recently used non-zombie tab.
     for (let i = 0; i < tabs.length; i++) {
       if (tabs[i] == selected || tabs[i].browser.__SS_restore) {
-        // this tab is selected or already a zombie, skip it
+        // This tab is selected or already a zombie, skip it.
         continue;
       }
       if (lruTab == null || tabs[i].lastTouchedAt < lruTab.lastTouchedAt) {
         lruTab = tabs[i];
       }
     }
-    // if the tab was last touched more than browser.tabs.expireTime seconds ago,
-    // zombify it
+    // If the tab was last touched more than browser.tabs.expireTime seconds ago,
+    // zombify it.
     if (lruTab) {
       let tabAgeMs = Date.now() - lruTab.lastTouchedAt;
       if (tabAgeMs > expireTimeMs) {
         MemoryObserver.zombify(lruTab);
         Telemetry.addData("FENNEC_TAB_EXPIRED", tabAgeMs / 1000);
         return true;
       }
     }
     return false;
   },
 
-  // for debugging
+  // For debugging
   dump: function(aPrefix) {
     let tabs = BrowserApp.tabs;
     for (let i = 0; i < tabs.length; i++) {
       dump(aPrefix + " | " + "Tab [" + tabs[i].browser.contentWindow.location.href + "]: lastTouchedAt:" + tabs[i].lastTouchedAt + ", zombie:" + tabs[i].browser.__SS_restore);
     }
   },
 };
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -0,0 +1,639 @@
+/* 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 = ["fxAccounts", "FxAccounts"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://services-crypto/utils.js");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/FxAccountsClient.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto",
+                                  "resource://gre/modules/identity/jwcrypto.jsm");
+
+const DATA_FORMAT_VERSION = 1;
+const DEFAULT_STORAGE_FILENAME = "signedInUser.json";
+const ASSERTION_LIFETIME = 1000 * 60 * 5; // 5 minutes
+const KEY_LIFETIME = 1000 * 3600 * 12;    // 12 hours
+const CERT_LIFETIME = 1000 * 3600 * 6;    // 6 hours
+const POLL_SESSION = 1000 * 60 * 5;       // 5 minutes
+const POLL_STEP = 1000 * 3;               // 3 seconds
+
+// loglevel preference should be one of: "FATAL", "ERROR", "WARN", "INFO",
+// "CONFIG", "DEBUG", "TRACE" or "ALL". We will be logging error messages by
+// default.
+const PREF_LOG_LEVEL = "identity.fxaccounts.loglevel";
+try {
+  this.LOG_LEVEL =
+    Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING
+    && Services.prefs.getCharPref(PREF_LOG_LEVEL);
+} catch (e) {
+  this.LOG_LEVEL = Log.Level.Error;
+}
+
+let log = Log.repository.getLogger("Services.FxAccounts");
+log.level = LOG_LEVEL;
+log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
+
+InternalMethods = function(mock) {
+  this.cert = null;
+  this.keyPair = null;
+  this.signedInUser = null;
+  this.version = DATA_FORMAT_VERSION;
+
+  // Make a local copy of these constants so we can mock it in testing
+  this.POLL_STEP = POLL_STEP;
+  this.POLL_SESSION = POLL_SESSION;
+  // We will create this.pollTimeRemaining below; it will initially be
+  // set to the value of POLL_SESSION.
+
+  // We interact with the Firefox Accounts auth server in order to confirm that
+  // a user's email has been verified and also to fetch the user's keys from
+  // the server.  We manage these processes in possibly long-lived promises
+  // that are internal to this object (never exposed to callers).  Because
+  // Firefox Accounts allows for only one logged-in user, and because it's
+  // conceivable that while we are waiting to verify one identity, a caller
+  // could start verification on a second, different identity, we need to be
+  // able to abort all work on the first sign-in process.  The currentTimer and
+  // generationCount are used for this purpose.
+  this.whenVerifiedPromise = null;
+  this.whenKeysReadyPromise = null;
+  this.currentTimer = null;
+  this.generationCount = 0;
+
+  this.fxAccountsClient = new FxAccountsClient();
+
+  if (mock) { // Testing.
+    Object.keys(mock).forEach((prop) => {
+      log.debug("InternalMethods: mocking: " + prop);
+      this[prop] = mock[prop];
+    });
+  }
+  if (!this.signedInUserStorage) {
+    // Normal (i.e., non-testing) initialization.
+    // We don't reference |profileDir| in the top-level module scope
+    // as we may be imported before we know where it is.
+    this.signedInUserStorage = new JSONStorage({
+      filename: DEFAULT_STORAGE_FILENAME,
+      baseDir: OS.Constants.Path.profileDir,
+    });
+  }
+}
+InternalMethods.prototype = {
+
+  /**
+   * Ask the server whether the user's email has been verified
+   */
+  checkEmailStatus: function checkEmailStatus(sessionToken) {
+    return this.fxAccountsClient.recoveryEmailStatus(sessionToken);
+  },
+
+  /**
+   * Once the user's email is verified, we can request the keys
+   */
+  fetchKeys: function fetchKeys(keyFetchToken) {
+    log.debug("fetchKeys: " + keyFetchToken);
+    return this.fxAccountsClient.accountKeys(keyFetchToken);
+  },
+
+  /*
+   * Reset state such that any previous flow is canceled.
+   */
+  abortExistingFlow: function abortExistingFlow() {
+    if (this.currentTimer) {
+      log.debug("Polling aborted; Another user signing in");
+      clearTimeout(this.currentTimer);
+      this.currentTimer = 0;
+    }
+    this.generationCount++;
+    log.debug("generationCount: " + this.generationCount);
+
+    if (this.whenVerifiedPromise) {
+      this.whenVerifiedPromise.reject(
+        new Error("Verification aborted; Another user signing in"));
+      this.whenVerifiedPromise = null;
+    }
+
+    if (this.whenKeysReadyPromise) {
+      this.whenKeysReadyPromise.reject(
+        new Error("KeyFetch aborted; Another user signing in"));
+      this.whenKeysReadyPromise = null;
+    }
+  },
+
+  /**
+   * Fetch encryption keys for the signed-in-user from the FxA API server.
+   *
+   * Not for user consumption.  Exists to cause the keys to be fetch.
+   *
+   * Returns user data so that it can be chained with other methods.
+   *
+   * @return Promise
+   *        The promise resolves to the credentials object of the signed-in user:
+   *        {
+   *          email: The user's email address
+   *          uid: The user's unique id
+   *          sessionToken: Session for the FxA server
+   *          kA: An encryption key from the FxA server
+   *          kB: An encryption key derived from the user's FxA password
+   *          isVerified: email verification status
+   *        }
+   *        or null if no user is signed in
+   */
+  getKeys: function() {
+    return this.getUserAccountData().then((data) => {
+      if (!data) {
+        throw new Error("Can't get keys; User is not signed in");
+      }
+      if (data.kA && data.kB) {
+        return data;
+      }
+      if (!this.whenKeysReadyPromise) {
+        this.whenKeysReadyPromise = Promise.defer();
+        this.fetchAndUnwrapKeys(data.keyFetchToken)
+          .then((data) => {
+            if (this.whenKeysReadyPromise) {
+              this.whenKeysReadyPromise.resolve(data);
+            }
+          });
+      }
+      return this.whenKeysReadyPromise.promise;
+      });
+   },
+
+  fetchAndUnwrapKeys: function(keyFetchToken) {
+    log.debug("fetchAndUnwrapKeys: token: " + keyFetchToken);
+    return Task.spawn(function* task() {
+      // Sign out if we don't have a key fetch token.
+      if (!keyFetchToken) {
+        yield internal.signOut();
+        return null;
+      }
+      let myGenerationCount = internal.generationCount;
+
+      let {kA, wrapKB} = yield internal.fetchKeys(keyFetchToken);
+
+      let data = yield internal.getUserAccountData();
+
+      // Sanity check that the user hasn't changed out from under us
+      if (data.keyFetchToken !== keyFetchToken) {
+        throw new Error("Signed in user changed while fetching keys!");
+      }
+
+      // Next statements must be synchronous until we setUserAccountData
+      // so that we don't risk getting into a weird state.
+      let kB_hex = CryptoUtils.xor(CommonUtils.hexToBytes(data.unwrapBKey),
+                                   wrapKB);
+
+      log.debug("kB_hex: " + kB_hex);
+      data.kA = CommonUtils.bytesAsHex(kA);
+      data.kB = CommonUtils.bytesAsHex(kB_hex);
+
+      delete data.keyFetchToken;
+
+      log.debug("Keys Obtained: kA=" + data.kA + ", kB=" + data.kB);
+
+      // Before writing any data, ensure that a new flow hasn't been
+      // started behind our backs.
+      if (internal.generationCount !== myGenerationCount) {
+        return null;
+      }
+
+      yield internal.setUserAccountData(data);
+
+      // We are now ready for business. This should only be invoked once
+      // per setSignedInUser(), regardless of whether we've rebooted since
+      // setSignedInUser() was called.
+      internal.notifyObservers("fxaccounts:onlogin");
+      return data;
+    }.bind(this));
+  },
+
+  getAssertionFromCert: function(data, keyPair, cert, audience) {
+    log.debug("getAssertionFromCert");
+    let payload = {};
+    let d = Promise.defer();
+    // "audience" should look like "http://123done.org".
+    // The generated assertion will expire in two minutes.
+    jwcrypto.generateAssertion(cert, keyPair, audience, function(err, signed) {
+      if (err) {
+        log.error("getAssertionFromCert: " + err);
+        d.reject(err);
+      } else {
+        log.debug("getAssertionFromCert returning signed: " + signed);
+        d.resolve(signed);
+      }
+    });
+    return d.promise;
+  },
+
+  getCertificate: function(data, keyPair, mustBeValidUntil) {
+    log.debug("getCertificate" + JSON.stringify(this.signedInUserStorage));
+    // TODO: get the lifetime from the cert's .exp field
+    if (this.cert && this.cert.validUntil > mustBeValidUntil) {
+      log.debug(" getCertificate already had one");
+      return Promise.resolve(this.cert.cert);
+    }
+    // else get our cert signed
+    let willBeValidUntil = this.now() + CERT_LIFETIME;
+    return this.getCertificateSigned(data.sessionToken,
+                                     keyPair.serializedPublicKey,
+                                     CERT_LIFETIME)
+      .then((cert) => {
+        this.cert = {
+          cert: cert,
+          validUntil: willBeValidUntil
+        };
+        return cert;
+      }
+    );
+  },
+
+  getCertificateSigned: function(sessionToken, serializedPublicKey, lifetime) {
+    log.debug("getCertificateSigned: " + sessionToken + " " + serializedPublicKey);
+    return this.fxAccountsClient.signCertificate(sessionToken,
+                                                 JSON.parse(serializedPublicKey),
+                                                 lifetime);
+  },
+
+  getKeyPair: function(mustBeValidUntil) {
+    if (this.keyPair && (this.keyPair.validUntil > mustBeValidUntil)) {
+      log.debug("getKeyPair: already have a keyPair");
+      return Promise.resolve(this.keyPair.keyPair);
+    }
+    // Otherwse, create a keypair and set validity limit.
+    let willBeValidUntil = this.now() + KEY_LIFETIME;
+    let d = Promise.defer();
+    jwcrypto.generateKeyPair("DS160", (err, kp) => {
+      if (err) {
+        d.reject(err);
+      } else {
+        this.keyPair = {
+          keyPair: kp,
+          validUntil: willBeValidUntil
+        };
+        log.debug("got keyPair");
+        delete this.cert;
+        d.resolve(this.keyPair.keyPair);
+      }
+    });
+    return d.promise;
+  },
+
+  getUserAccountData: function() {
+    // Skip disk if user is cached.
+    if (this.signedInUser) {
+      return Promise.resolve(this.signedInUser.accountData);
+    }
+
+    let deferred = Promise.defer();
+    this.signedInUserStorage.get()
+      .then((user) => {
+        log.debug("getUserAccountData -> " + JSON.stringify(user));
+        if (user && user.version == this.version) {
+          log.debug("setting signed in user");
+          this.signedInUser = user;
+        }
+        deferred.resolve(user ? user.accountData : null);
+      },
+      (err) => {
+        if (err instanceof OS.File.Error && err.becauseNoSuchFile) {
+          // File hasn't been created yet.  That will be done
+          // on the first call to getSignedInUser
+          deferred.resolve(null);
+        } else {
+          deferred.reject(err);
+        }
+      }
+    );
+
+    return deferred.promise;
+  },
+
+  isUserEmailVerified: function isUserEmailVerified(data) {
+    return !!(data && data.isVerified);
+  },
+
+  /**
+   * Setup for and if necessary do email verification polling.
+   */
+  loadAndPoll: function() {
+    return this.getUserAccountData()
+      .then(data => {
+        if (data && !this.isUserEmailVerified(data)) {
+          this.pollEmailStatus(data.sessionToken, "start");
+        }
+        return data;
+      });
+  },
+
+  startVerifiedCheck: function(data) {
+    log.debug("startVerifiedCheck " + JSON.stringify(data));
+    // Get us to the verified state, then get the keys. This returns a promise
+    // that will fire when we are completely ready.
+    //
+    // Login is truly complete once keys have been fetched, so once getKeys()
+    // obtains and stores kA and kB, it will fire the onlogin observer
+    // notification.
+    return this.whenVerified(data)
+      .then((data) => this.getKeys(data));
+  },
+
+  whenVerified: function(data) {
+    if (data.isVerified) {
+      log.debug("already verified");
+      return Promise.resolve(data);
+    }
+    if (!this.whenVerifiedPromise) {
+      this.whenVerifiedPromise = Promise.defer();
+      log.debug("whenVerified promise starts polling for verified email");
+      this.pollEmailStatus(data.sessionToken, "start");
+    }
+    return this.whenVerifiedPromise.promise;
+  },
+
+  notifyObservers: function(topic) {
+    log.debug("Notifying observers of user login");
+    Services.obs.notifyObservers(null, topic, null);
+  },
+
+  /**
+   * Give xpcshell tests an override point for duration testing. This is
+   * necessary because the tests need to manipulate the date in order to
+   * simulate certificate expiration.
+   */
+  now: function() {
+    return Date.now();
+  },
+
+  pollEmailStatus: function pollEmailStatus(sessionToken, why) {
+    let myGenerationCount = this.generationCount;
+    log.debug("entering pollEmailStatus: " + why + " " + myGenerationCount);
+    if (why == "start") {
+      if (this.currentTimer) {
+        // safety check - this case should have been caught on
+        // entry with setSignedInUser
+        throw new Error("Already polling for email status");
+      }
+      this.pollTimeRemaining = this.POLL_SESSION;
+    }
+
+    this.checkEmailStatus(sessionToken)
+      .then((response) => {
+        log.debug("checkEmailStatus -> " + JSON.stringify(response));
+        // Check to see if we're still current.
+        // If for some ghastly reason we are not, stop processing.
+        if (this.generationCount !== myGenerationCount) {
+          log.debug("generation count differs from " + this.generationCount + " - aborting");
+          log.debug("sessionToken on abort is " + sessionToken);
+          return;
+        }
+
+        if (response && response.verified) {
+          // Bug 947056 - Server should be able to tell FxAccounts.jsm to back
+          // off or stop polling altogether
+          this.getUserAccountData()
+            .then((data) => {
+              data.isVerified = true;
+              return this.setUserAccountData(data);
+            })
+            .then((data) => {
+              // Now that the user is verified, we can proceed to fetch keys
+              if (this.whenVerifiedPromise) {
+                this.whenVerifiedPromise.resolve(data);
+                delete this.whenVerifiedPromise;
+              }
+            });
+        } else {
+          log.debug("polling with step = " + this.POLL_STEP);
+          this.pollTimeRemaining -= this.POLL_STEP;
+          log.debug("time remaining: " + this.pollTimeRemaining);
+          if (this.pollTimeRemaining > 0) {
+            this.currentTimer = setTimeout(() => {
+              this.pollEmailStatus(sessionToken, "timer")}, this.POLL_STEP);
+            log.debug("started timer " + this.currentTimer);
+          } else {
+            if (this.whenVerifiedPromise) {
+              this.whenVerifiedPromise.reject(
+                new Error("User email verification timed out.")
+              );
+              delete this.whenVerifiedPromise;
+            }
+          }
+        }
+      });
+    },
+
+  setUserAccountData: function(accountData) {
+    return this.signedInUserStorage.get().then((record) => {
+      record.accountData = accountData;
+      this.signedInUser = record;
+      return this.signedInUserStorage.set(record)
+        .then(() => accountData);
+    });
+  }
+};
+
+let internal = null;
+
+/**
+ * FxAccounts delegates private methods to an instance of InternalMethods,
+ * which is not exported. The xpcshell tests need two overrides:
+ *  1) Access to the real internal.signedInUserStorage.
+ *  2) The ability to mock InternalMethods.
+ * If mockInternal is undefined, we are live.
+ * If mockInternal.onlySetInternal is present, we are executing the first
+ * case by binding internal to the FxAccounts instance.
+ * Otherwise if we have a mock instance, we are executing the second case.
+ */
+this.FxAccounts = function(mockInternal) {
+  let mocks = mockInternal;
+  if (mocks && mocks.onlySetInternal) {
+    mocks = null;
+  }
+  internal = new InternalMethods(mocks);
+  if (mockInternal) {
+    // Exposes the internal object for testing only.
+    this.internal = internal;
+  }
+}
+this.FxAccounts.prototype = Object.freeze({
+  version: DATA_FORMAT_VERSION,
+
+  // set() makes sure that polling is happening, if necessary.
+  // get() does not wait for verification, and returns an object even if
+  // unverified. The caller of get() must check .isVerified .
+  // The "fxaccounts:onlogin" event will fire only when the verified state
+  // goes from false to true, so callers must register their observer
+  // and then call get(). In particular, it will not fire when the account
+  // was found to be verified in a previous boot: if our stored state says
+  // the account is verified, the event will never fire. So callers must do:
+  //   register notification observer (go)
+  //   userdata = get()
+  //   if (userdata.isVerified()) {go()}
+
+  /**
+   * Set the current user signed in to Firefox Accounts.
+   *
+   * @param credentials
+   *        The credentials object obtained by logging in or creating
+   *        an account on the FxA server:
+   *        {
+   *          email: The users email address
+   *          uid: The user's unique id
+   *          sessionToken: Session for the FxA server
+   *          keyFetchToken: an unused keyFetchToken
+   *          isVerified: true/false
+   *        }
+   * @return Promise
+   *         The promise resolves to null when the data is saved
+   *         successfully and is rejected on error.
+   */
+  setSignedInUser: function setSignedInUser(credentials) {
+    log.debug("setSignedInUser - aborting any existing flows");
+    internal.abortExistingFlow();
+
+    let record = {version: this.version, accountData: credentials };
+    // Cache a clone of the credentials object.
+    internal.signedInUser = JSON.parse(JSON.stringify(record));
+
+    // This promise waits for storage, but not for verification.
+    // We're telling the caller that this is durable now.
+    return internal.signedInUserStorage.set(record)
+      .then(() => {
+        if (!internal.isUserEmailVerified(credentials)) {
+          internal.startVerifiedCheck(credentials);
+        }
+      });
+  },
+
+  /**
+   * Get the user currently signed in to Firefox Accounts.
+   *
+   * @return Promise
+   *        The promise resolves to the credentials object of the signed-in user:
+   *        {
+   *          email: The user's email address
+   *          uid: The user's unique id
+   *          sessionToken: Session for the FxA server
+   *          kA: An encryption key from the FxA server
+   *          kB: An encryption key derived from the user's FxA password
+   *          isVerified: email verification status
+   *        }
+   *        or null if no user is signed in.
+   */
+  getSignedInUser: function getSignedInUser() {
+    return internal.getUserAccountData()
+      .then((data) => {
+        if (!data) {
+          return null;
+        }
+        if (!internal.isUserEmailVerified(data)) {
+          // If the email is not verified, start polling for verification,
+          // but return null right away.  We don't want to return a promise
+          // that might not be fulfilled for a long time.
+          internal.startVerifiedCheck(credentials);
+        }
+        return data;
+      });
+  },
+
+  /**
+   * returns a promise that fires with the assertion.  If there is no verified
+   * signed-in user, fires with null.
+   */
+  getAssertion: function getAssertion(audience) {
+    log.debug("enter getAssertion()");
+    let mustBeValidUntil = internal.now() + ASSERTION_LIFETIME;
+    return internal.getUserAccountData()
+      .then((data) => {
+        if (!data) {
+          // No signed-in user
+          return null;
+        }
+        if (!internal.isUserEmailVerified(data)) {
+          // Signed-in user has not verified email
+          return null;
+        }
+        return internal.getKeyPair(mustBeValidUntil)
+          .then((keyPair) => {
+            return internal.getCertificate(data, keyPair, mustBeValidUntil)
+              .then((cert) => {
+                return internal.getAssertionFromCert(data, keyPair,
+                                                     cert, audience)
+              });
+          });
+      });
+  },
+
+  /**
+   * Sign the current user out.
+   *
+   * @return Promise
+   *         The promise is rejected if a storage error occurs.
+   */
+  signOut: function signOut() {
+    internal.abortExistingFlow();
+    internal.signedInUser = null; // clear in-memory cache
+    return internal.signedInUserStorage.set(null).then(() => {
+      internal.notifyObservers("fxaccounts:onlogout");
+    });
+  },
+
+  // Return the URI of the remote UI flows.
+  getAccountsURI: function() {
+    let url = Services.urlFormatter.formatURLPref("firefox.accounts.remoteUrl");
+    if (!/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
+      throw new Error("Firefox Accounts server must use HTTPS");
+    }
+    return url;
+  }
+});
+
+/**
+ * JSONStorage constructor that creates instances that may set/get
+ * to a specified file, in a directory that will be created if it
+ * doesn't exist.
+ *
+ * @param options {
+ *                  filename: of the file to write to
+ *                  baseDir: directory where the file resides
+ *                }
+ * @return instance
+ */
+function JSONStorage(options) {
+  this.baseDir = options.baseDir;
+  this.path = OS.Path.join(options.baseDir, options.filename);
+};
+
+JSONStorage.prototype = {
+  set: function(contents) {
+    return OS.File.makeDir(this.baseDir, {ignoreExisting: true})
+      .then(CommonUtils.writeJSON.bind(null, contents, this.path));
+  },
+
+  get: function() {
+    return CommonUtils.readJSON(this.path);
+  }
+};
+
+// A getter for the instance to export
+XPCOMUtils.defineLazyGetter(this, "fxAccounts", function() {
+  let a = new FxAccounts();
+
+  // XXX Bug 947061 - We need a strategy for resuming email verification after
+  // browser restart
+  internal.loadAndPoll();
+
+  return a;
+});
+
--- a/services/fxaccounts/moz.build
+++ b/services/fxaccounts/moz.build
@@ -1,8 +1,11 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 TEST_DIRS += ['tests']
-EXTRA_JS_MODULES += ['FxAccountsClient.jsm']
+EXTRA_JS_MODULES += [
+  'FxAccounts.jsm',
+  'FxAccountsClient.jsm'
+]
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -0,0 +1,493 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/FxAccounts.jsm");
+Cu.import("resource://gre/modules/FxAccountsClient.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+
+// XXX until bug 937114 is fixed
+Cu.importGlobalProperties(['atob']);
+
+let log = Log.repository.getLogger("Services.FxAccounts.test");
+log.level = Log.Level.Debug;
+
+// See verbose logging from FxAccounts.jsm
+Services.prefs.setCharPref("identity.fxaccounts.loglevel", "DEBUG");
+
+function run_test() {
+  run_next_test();
+}
+
+/*
+ * The FxAccountsClient communicates with the remote Firefox
+ * Accounts auth server.  Mock the server calls, with a little
+ * lag time to simulate some latency.
+ *
+ * We add the _verified attribute to mock the change in verification
+ * state on the FXA server.
+ */
+function MockFxAccountsClient() {
+  this._email = "nobody@example.com";
+  this._verified = false;
+
+  // mock calls up to the auth server to determine whether the
+  // user account has been verified
+  this.recoveryEmailStatus = function (sessionToken) {
+    // simulate a call to /recovery_email/status
+    let deferred = Promise.defer();
+
+    let response = {
+      email: this._email,
+      verified: this._verified
+    };
+    deferred.resolve(response);
+
+    return deferred.promise;
+  };
+
+  this.accountKeys = function (keyFetchToken) {
+    let deferred = Promise.defer();
+
+    do_timeout(50, () => {
+      let response = {
+        kA: expandBytes("11"),
+        wrapKB: expandBytes("22")
+      };
+      deferred.resolve(response);
+    });
+    return deferred.promise;
+  };
+
+  this.signCertificate = function() { throw "no" };
+
+  FxAccountsClient.apply(this);
+}
+MockFxAccountsClient.prototype = {
+  __proto__: FxAccountsClient.prototype
+}
+
+let MockStorage = function() {
+  this.data = null;
+};
+MockStorage.prototype = Object.freeze({
+  set: function (contents) {
+    this.data = contents;
+    return Promise.resolve(null);
+  },
+  get: function () {
+    return Promise.resolve(this.data);
+  },
+});
+
+/*
+ * We need to mock the FxAccounts module's interfaces to external
+ * services, such as storage and the FxAccounts client.  We also
+ * mock the now() method, so that we can simulate the passing of
+ * time and verify that signatures expire correctly.
+ */
+let MockFxAccounts = function() {
+  this._getCertificateSigned_calls = [];
+  this._d_signCertificate = Promise.defer();
+  this._now_is = new Date();
+
+  let mockInternal = {
+    signedInUserStorage: new MockStorage(),
+    now: () => {
+      return this._now_is;
+    },
+    getCertificateSigned: (sessionToken, serializedPublicKey) => {
+      _("mock getCerificateSigned\n");
+      this._getCertificateSigned_calls.push([sessionToken, serializedPublicKey]);
+      return this._d_signCertificate.promise;
+    },
+    fxAccountsClient: new MockFxAccountsClient()
+  };
+  FxAccounts.apply(this, [mockInternal]);
+};
+MockFxAccounts.prototype = {
+  __proto__: FxAccounts.prototype,
+};
+
+add_test(function test_non_https_remote_server_uri() {
+  Services.prefs.setCharPref(
+    "firefox.accounts.remoteUrl",
+    "http://example.com/browser/browser/base/content/test/general/accounts_testRemoteCommands.html");
+  do_check_throws_message(function () {
+    fxAccounts.getAccountsURI();
+  }, "Firefox Accounts server must use HTTPS");
+
+  Services.prefs.clearUserPref("firefox.accounts.remoteUrl");
+
+  run_next_test();
+});
+
+add_task(function test_get_signed_in_user_initially_unset() {
+  // This test, unlike the rest, uses an un-mocked FxAccounts instance.
+  // However, we still need to pass an object to the constructor to
+  // force it to expose "internal", so we can test the disk storage.
+  let account = new FxAccounts({onlySetInternal: true})
+  let credentials = {
+    email: "foo@example.com",
+    uid: "1234@lcip.org",
+    assertion: "foobar",
+    sessionToken: "dead",
+    kA: "beef",
+    kB: "cafe",
+    isVerified: true
+  };
+
+  let result = yield account.getSignedInUser();
+  do_check_eq(result, null);
+
+  yield account.setSignedInUser(credentials);
+
+  let result = yield account.getSignedInUser();
+  do_check_eq(result.email, credentials.email);
+  do_check_eq(result.assertion, credentials.assertion);
+  do_check_eq(result.kB, credentials.kB);
+
+  // Delete the memory cache and force the user
+  // to be read and parsed from storage (e.g. disk via JSONStorage).
+  delete account.internal.signedInUser;
+  let result = yield account.getSignedInUser();
+  do_check_eq(result.email, credentials.email);
+  do_check_eq(result.assertion, credentials.assertion);
+  do_check_eq(result.kB, credentials.kB);
+
+  // sign out
+  yield account.signOut();
+
+  // user should be undefined after sign out
+  let result = yield account.getSignedInUser();
+  do_check_eq(result, null);
+});
+
+/*
+ * Sanity-check that our mocked client is working correctly
+ */
+add_test(function test_client_mock() {
+  do_test_pending();
+
+  let fxa = new MockFxAccounts();
+  let client = fxa.internal.fxAccountsClient;
+  do_check_eq(client._verified, false);
+  do_check_eq(typeof client.signIn, "function");
+
+  // The recoveryEmailStatus function eventually fulfills its promise
+  client.recoveryEmailStatus()
+    .then(response => {
+      do_check_eq(response.verified, false);
+      do_test_finished();
+      run_next_test();
+    });
+});
+
+/*
+ * Sign in a user, and after a little while, verify the user's email.
+ * Polling should detect that the email is verified, and eventually
+ * 'onlogin' should be observed
+ */
+add_test(function test_verification_poll() {
+  do_test_pending();
+
+  let fxa = new MockFxAccounts();
+  let test_user = getTestUser("francine");
+
+  makeObserver("fxaccounts:onlogin", function() {
+    log.debug("test_verification_poll observed onlogin");
+    // Once email verification is complete, we will observe onlogin
+    fxa.internal.getUserAccountData().then(user => {
+      // And confirm that the user's state has changed
+      do_check_eq(user.isVerified, true);
+      do_check_eq(user.email, test_user.email);
+      do_test_finished();
+      run_next_test();
+    });
+  });
+
+  fxa.setSignedInUser(test_user).then(() => {
+    fxa.internal.getUserAccountData().then(user => {
+      // The user is signing in, but email has not been verified yet
+      do_check_eq(user.isVerified, false);
+      do_timeout(200, function() {
+        // Mock email verification ...
+        fxa.internal.fxAccountsClient._email = test_user.email;
+        fxa.internal.fxAccountsClient._verified = true;
+      });
+    });
+  });
+});
+
+/*
+ * Sign in the user, but never verify the email.  The check-email
+ * poll should time out.  No login event should be observed, and the
+ * internal whenVerified promise should be rejected
+ */
+add_test(function test_polling_timeout() {
+  do_test_pending();
+
+  // This test could be better - the onlogin observer might fire on somebody
+  // else's stack, and we're not making sure that we're not receiving such a
+  // message.  In other words, this tests either failure, or success, but not
+  // both.
+
+  let fxa = new MockFxAccounts();
+  let test_user = getTestUser("carol");
+
+  let removeObserver = makeObserver("fxaccounts:onlogin", function() {
+    do_throw("We should not be getting a login event!");
+  });
+
+  fxa.internal.POLL_SESSION = 1;
+  fxa.internal.POLL_STEP = 2;
+
+  let p = fxa.internal.whenVerified({});
+
+  fxa.setSignedInUser(test_user).then(() => {
+    p.then(
+      (success) => {
+        do_throw("this should not succeed");
+      },
+      (fail) => {
+        removeObserver();
+        do_test_finished();
+        run_next_test();
+      }
+    );
+  });
+});
+
+add_task(function test_getKeys() {
+  let fxa = new MockFxAccounts();
+  let user = getTestUser("eusebius");
+
+  // Once email has been verified, we will be able to get keys
+  user.isVerified = true;
+
+  fxa.setSignedInUser(user).then(() => {
+    fxa.getSignedInUser().then((user) => {
+      // Before getKeys, we have no keys
+      do_check_eq(!!data.kA, false);
+      do_check_eq(!!data.kB, false);
+      // And we still have a key-fetch token to use
+      do_check_eq(!!data.keyFetchToken, true);
+
+      fxa.internal.getKeys().then(() => {
+        fxa.getSignedInUser().then((user) => {
+          // Now we should have keys
+          do_check_eq(fxa.internal.isUserEmailVerified(data), true);
+          do_check_eq(!!data.isVerified, true);
+          do_check_eq(data.kA, expandHex("11"));
+          do_check_eq(data.kB, expandHex("66"));
+          do_check_eq(data.keyFetchToken, undefined);
+        });
+      });
+    });
+  });
+});
+
+/*
+ * Alice (User A) signs up but never verifies her email.  Then Bob (User B)
+ * signs in with a verified email.  Ensure that no sign-in events are triggered
+ * on Alice's behalf.  In the end, Bob should be the signed-in user.
+ */
+add_test(function test_overlapping_signins() {
+  do_test_pending();
+
+  let fxa = new MockFxAccounts();
+  let alice = getTestUser("alice");
+  let bob = getTestUser("bob");
+
+  makeObserver("fxaccounts:onlogin", function() {
+    log.debug("test_overlapping_signins observed onlogin");
+    // Once email verification is complete, we will observe onlogin
+    fxa.internal.getUserAccountData().then(user => {
+      do_check_eq(user.email, bob.email);
+      do_check_eq(user.isVerified, true);
+      do_test_finished();
+      run_next_test();
+    });
+  });
+
+  // Alice is the user signing in; her email is unverified.
+  fxa.setSignedInUser(alice).then(() => {
+    log.debug("Alice signing in ...");
+    fxa.internal.getUserAccountData().then(user => {
+      do_check_eq(user.email, alice.email);
+      do_check_eq(user.isVerified, false);
+      log.debug("Alice has not verified her email ...");
+
+      // Now Bob signs in instead and actually verifies his email
+      log.debug("Bob signing in ...");
+      fxa.setSignedInUser(bob).then(() => {
+        do_timeout(200, function() {
+          // Mock email verification ...
+          log.debug("Bob verifying his email ...");
+          fxa.internal.fxAccountsClient._verified = true;
+        });
+      });
+    });
+  });
+});
+
+add_task(function test_getAssertion() {
+  let fxa = new MockFxAccounts();
+
+  do_check_throws(function() {
+    yield fxa.getAssertion("nonaudience");
+  });
+
+  let creds = {
+    sessionToken: "sessionToken",
+    kA: expandHex("11"),
+    kB: expandHex("66"),
+    isVerified: true
+  };
+  // By putting kA/kB/isVerified in "creds", we skip ahead
+  // to the "we're ready" stage.
+  yield fxa.setSignedInUser(creds);
+
+  _("== ready to go\n");
+  let now = 138000000*1000;
+  let start = Date.now();
+  fxa._now_is = now;
+  let d = fxa.getAssertion("audience.example.com");
+  // At this point, a thread has been spawned to generate the keys.
+  _("-- back from fxa.getAssertion\n");
+  fxa._d_signCertificate.resolve("cert1");
+  let assertion = yield d;
+  let finish = Date.now();
+  do_check_eq(fxa._getCertificateSigned_calls.length, 1);
+  do_check_eq(fxa._getCertificateSigned_calls[0][0], "sessionToken");
+  do_check_neq(assertion, null);
+  _("ASSERTION: "+assertion+"\n");
+  let pieces = assertion.split("~");
+  do_check_eq(pieces[0], "cert1");
+  do_check_neq(fxa.internal.keyPair, undefined);
+  _(fxa.internal.keyPair.validUntil+"\n");
+  let p2 = pieces[1].split(".");
+  let header = JSON.parse(atob(p2[0]));
+  _("HEADER: "+JSON.stringify(header)+"\n");
+  do_check_eq(header.alg, "DS128");
+  let payload = JSON.parse(atob(p2[1]));
+  _("PAYLOAD: "+JSON.stringify(payload)+"\n");
+  do_check_eq(payload.aud, "audience.example.com");
+  // FxAccounts KEY_LIFETIME
+  do_check_eq(fxa.internal.keyPair.validUntil, now + (12*3600*1000));
+  // FxAccounts CERT_LIFETIME
+  do_check_eq(fxa.internal.cert.validUntil, now + (6*3600*1000));
+  _("delta: "+(new Date(payload.exp) - now)+"\n");
+  let exp = Number(payload.exp);
+  // jwcrypto.jsm uses an unmocked Date.now()+2min to decide on the
+  // expiration time, so we test that it's inside a specific timebox
+  do_check_true(start + 2*60*1000 <= exp);
+  do_check_true(exp <= finish + 2*60*1000);
+
+  // Reset for next call.
+  fxa._d_signCertificate = Promise.defer();
+
+  // Getting a new assertion "soon" (i.e. w/o incrementing "now"), even for
+  // a new audience, should not provoke key generation or a signing request.
+  assertion = yield fxa.getAssertion("other.example.com");
+  do_check_eq(fxa._getCertificateSigned_calls.length, 1);
+
+  // But "waiting" (i.e. incrementing "now") will need a new key+signature.
+  fxa._now_is = now + 24*3600*1000;
+  start = Date.now();
+  d = fxa.getAssertion("third.example.com");
+  fxa._d_signCertificate.resolve("cert2");
+  assertion = yield d;
+  finish = Date.now();
+  do_check_eq(fxa._getCertificateSigned_calls.length, 2);
+  do_check_eq(fxa._getCertificateSigned_calls[1][0], "sessionToken");
+  pieces = assertion.split("~");
+  do_check_eq(pieces[0], "cert2");
+  p2 = pieces[1].split(".");
+  header = JSON.parse(atob(p2[0]));
+  payload = JSON.parse(atob(p2[1]));
+  do_check_eq(payload.aud, "third.example.com");
+  // 12*3600*1000 === FxAccounts KEY_LIFETIME
+  do_check_eq(fxa.internal.keyPair.validUntil, now + 24*3600*1000 + (12*3600*1000));
+  // 6*3600*1000 === FxAccounts CERT_LIFETIME
+  do_check_eq(fxa.internal.cert.validUntil, now + 24*3600*1000 + (6*3600*1000));
+  exp = Number(payload.exp);
+  do_check_true(start + 2*60*1000 <= exp);
+  do_check_true(exp <= finish + 2*60*1000);
+
+  _("----- DONE ----\n");
+});
+
+/*
+ * End of tests.
+ * Utility functions follow.
+ */
+
+function expandHex(two_hex) {
+  // Return a 64-character hex string, encoding 32 identical bytes.
+  let eight_hex = two_hex + two_hex + two_hex + two_hex;
+  let thirtytwo_hex = eight_hex + eight_hex + eight_hex + eight_hex;
+  return thirtytwo_hex + thirtytwo_hex;
+};
+
+function expandBytes(two_hex) {
+  return CommonUtils.hexToBytes(expandHex(two_hex));
+};
+
+function getTestUser(name) {
+  return {
+    email: name + "@example.com",
+    uid: "1ad7f502-4cc7-4ec1-a209-071fd2fae348",
+    sessionToken: name + "'s session token",
+    keyFetchToken: name + "'s keyfetch token",
+    unwrapBKey: expandHex("44"),
+    isVerified: false
+  };
+}
+
+function makeObserver(aObserveTopic, aObserveFunc) {
+  let observer = {
+    // nsISupports provides type management in C++
+    // nsIObserver is to be an observer
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
+
+    observe: function (aSubject, aTopic, aData) {
+      log.debug("observed " + aTopic + " " + aData);
+      if (aTopic == aObserveTopic) {
+        removeMe();
+        aObserveFunc(aSubject, aTopic, aData);
+      }
+    }
+  };
+
+  function removeMe() {
+    log.debug("removing observer for " + aObserveTopic);
+    Services.obs.removeObserver(observer, aObserveTopic);
+  }
+
+  Services.obs.addObserver(observer, aObserveTopic, false);
+  return removeMe;
+}
+
+function do_check_throws(func, result, stack)
+{
+  if (!stack)
+    stack = Components.stack.caller;
+
+  try {
+    func();
+  } catch (ex) {
+    if (ex.name == result) {
+      return;
+    }
+    do_throw("Expected result " + result + ", caught " + ex, stack);
+  }
+
+  if (result) {
+    do_throw("Expected result " + result + ", none thrown", stack);
+  }
+}
--- a/services/fxaccounts/tests/xpcshell/xpcshell.ini
+++ b/services/fxaccounts/tests/xpcshell/xpcshell.ini
@@ -1,6 +1,7 @@
 [DEFAULT]
 head = head.js ../../../common/tests/unit/head_helpers.js ../../../common/tests/unit/head_http.js
 tail =
 
+[test_accounts.js]
 [test_client.js]
 
--- a/services/healthreport/modules-testing/utils.jsm
+++ b/services/healthreport/modules-testing/utils.jsm
@@ -2,17 +2,16 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = [
   "getAppInfo",
   "updateAppInfo",
-  "makeFakeAppDir",
   "createFakeCrash",
   "InspectedHealthReporter",
   "getHealthReporter",
 ];
 
 
 const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
 
@@ -80,107 +79,16 @@ this.updateAppInfo = function (obj) {
 
       return obj.QueryInterface(iid);
     },
   };
 
   registrar.registerFactory(id, "XULAppInfo", cid, factory);
 };
 
-// Reference needed in order for fake app dir provider to be active.
-let gFakeAppDirectoryProvider;
-
-/**
- * Installs a fake UAppData directory.
- *
- * This is needed by tests because a UAppData directory typically isn't
- * present in the test environment.
- *
- * This function is suitable for use in different components. If we ever
- * establish a central location for convenient test helpers, this should
- * go there.
- *
- * We create the new UAppData directory under the profile's directory
- * because the profile directory is automatically cleaned as part of
- * test shutdown.
- *
- * This returns a promise that will be resolved once the new directory
- * is created and installed.
- */
-this.makeFakeAppDir = function () {
-  let dirMode = OS.Constants.libc.S_IRWXU;
-  let dirService = Cc["@mozilla.org/file/directory_service;1"]
-                     .getService(Ci.nsIProperties);
-  let baseFile = dirService.get("ProfD", Ci.nsIFile);
-  let appD = baseFile.clone();
-  appD.append("UAppData");
-
-  if (gFakeAppDirectoryProvider) {
-    return Promise.resolve(appD.path);
-  }
-
-  function makeDir(f) {
-    if (f.exists()) {
-      return;
-    }
-
-    dump("Creating directory: " + f.path + "\n");
-    f.create(Ci.nsIFile.DIRECTORY_TYPE, dirMode);
-  }
-
-  makeDir(appD);
-
-  let reportsD = appD.clone();
-  reportsD.append("Crash Reports");
-
-  let pendingD = reportsD.clone();
-  pendingD.append("pending");
-  let submittedD = reportsD.clone();
-  submittedD.append("submitted");
-
-  makeDir(reportsD);
-  makeDir(pendingD);
-  makeDir(submittedD);
-
-  let provider = {
-    getFile: function (prop, persistent) {
-      persistent.value = true;
-      if (prop == "UAppData") {
-        return appD.clone();
-      }
-
-      throw Cr.NS_ERROR_FAILURE;
-    },
-
-    QueryInterace: function (iid) {
-      if (iid.equals(Ci.nsIDirectoryServiceProvider) ||
-          iid.equals(Ci.nsISupports)) {
-        return this;
-      }
-
-      throw Cr.NS_ERROR_NO_INTERFACE;
-    },
-  };
-
-  // Register the new provider.
-  dirService.QueryInterface(Ci.nsIDirectoryService)
-            .registerProvider(provider);
-
-  // And undefine the old one.
-  try {
-    dirService.undefine("UAppData");
-  } catch (ex) {};
-
-  gFakeAppDirectoryProvider = provider;
-
-  dump("Successfully installed fake UAppDir\n");
-  return Promise.resolve(appD.path);
-};
-
-
 /**
  * Creates a fake crash in the Crash Reports directory.
  *
  * Currently, we just create a dummy file. A more robust implementation would
  * create something that actually resembles a crash report file.
  *
  * This is very similar to code in crashreporter/tests/browser/head.js.
  *
--- a/services/healthreport/tests/xpcshell/test_healthreporter.js
+++ b/services/healthreport/tests/xpcshell/test_healthreporter.js
@@ -15,16 +15,17 @@ let bsp = Cu.import("resource://gre/modu
 Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
 Cu.import("resource://gre/modules/services/datareporting/policy.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://testing-common/httpd.js");
 Cu.import("resource://testing-common/services-common/bagheeraserver.js");
 Cu.import("resource://testing-common/services/metrics/mocks.jsm");
 Cu.import("resource://testing-common/services/healthreport/utils.jsm");
+Cu.import("resource://testing-common/AppData.jsm");
 
 
 const DUMMY_URI = "http://localhost:62013/";
 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
 
 const HealthReporterState = bsp.HealthReporterState;
 
 
--- a/services/healthreport/tests/xpcshell/test_provider_crashes.js
+++ b/services/healthreport/tests/xpcshell/test_provider_crashes.js
@@ -4,16 +4,17 @@
 "use strict";
 
 const {utils: Cu} = Components;
 
 
 Cu.import("resource://gre/modules/Metrics.jsm");
 Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
 Cu.import("resource://testing-common/services/healthreport/utils.jsm");
+Cu.import("resource://testing-common/AppData.jsm");
 
 
 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
 
 
 function run_test() {
   run_next_test();
 }
new file mode 100644
--- /dev/null
+++ b/testing/modules/AppData.jsm
@@ -0,0 +1,101 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+  "makeFakeAppDir",
+];
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+
+// Reference needed in order for fake app dir provider to be active.
+let gFakeAppDirectoryProvider;
+
+/**
+ * Installs a fake UAppData directory.
+ *
+ * This is needed by tests because a UAppData directory typically isn't
+ * present in the test environment.
+ *
+ * We create the new UAppData directory under the profile's directory
+ * because the profile directory is automatically cleaned as part of
+ * test shutdown.
+ *
+ * This returns a promise that will be resolved once the new directory
+ * is created and installed.
+ */
+this.makeFakeAppDir = function () {
+  let dirMode = OS.Constants.libc.S_IRWXU;
+  let dirService = Cc["@mozilla.org/file/directory_service;1"]
+                     .getService(Ci.nsIProperties);
+  let baseFile = dirService.get("ProfD", Ci.nsIFile);
+  let appD = baseFile.clone();
+  appD.append("UAppData");
+
+  if (gFakeAppDirectoryProvider) {
+    return Promise.resolve(appD.path);
+  }
+
+  function makeDir(f) {
+    if (f.exists()) {
+      return;
+    }
+
+    dump("Creating directory: " + f.path + "\n");
+    f.create(Ci.nsIFile.DIRECTORY_TYPE, dirMode);
+  }
+
+  makeDir(appD);
+
+  let reportsD = appD.clone();
+  reportsD.append("Crash Reports");
+
+  let pendingD = reportsD.clone();
+  pendingD.append("pending");
+  let submittedD = reportsD.clone();
+  submittedD.append("submitted");
+
+  makeDir(reportsD);
+  makeDir(pendingD);
+  makeDir(submittedD);
+
+  let provider = {
+    getFile: function (prop, persistent) {
+      persistent.value = true;
+      if (prop == "UAppData") {
+        return appD.clone();
+      }
+
+      throw Cr.NS_ERROR_FAILURE;
+    },
+
+    QueryInterace: function (iid) {
+      if (iid.equals(Ci.nsIDirectoryServiceProvider) ||
+          iid.equals(Ci.nsISupports)) {
+        return this;
+      }
+
+      throw Cr.NS_ERROR_NO_INTERFACE;
+    },
+  };
+
+  // Register the new provider.
+  dirService.QueryInterface(Ci.nsIDirectoryService)
+            .registerProvider(provider);
+
+  // And undefine the old one.
+  try {
+    dirService.undefine("UAppData");
+  } catch (ex) {};
+
+  gFakeAppDirectoryProvider = provider;
+
+  dump("Successfully installed fake UAppDir\n");
+  return Promise.resolve(appD.path);
+};
+
--- a/testing/modules/Makefile.in
+++ b/testing/modules/Makefile.in
@@ -1,8 +1,9 @@
 # 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/.
 
 TESTING_JS_MODULES := \
+  AppData.jsm \
   AppInfo.jsm \
   Assert.jsm \
   $(NULL)
--- a/toolkit/devtools/server/actors/root.js
+++ b/toolkit/devtools/server/actors/root.js
@@ -376,40 +376,16 @@ RootActor.prototype = {
     let e = Services.wm.getEnumerator(null);
     while (e.hasMoreElements()) {
       let win = e.getNext();
       let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
                            .getInterface(Ci.nsIDOMWindowUtils);
       windowUtils.resumeTimeouts();
       windowUtils.suppressEventHandling(false);
     }
-  },
-
-  /* ChromeDebuggerActor hooks. */
-
-  /**
-   * Add the specified actor to the default actor pool connection, in order to
-   * keep it alive as long as the server is. This is used by breakpoints in the
-   * thread and chrome debugger actors.
-   *
-   * @param actor aActor
-   *        The actor object.
-   */
-  addToParentPool: function(aActor) {
-    this.conn.addActor(aActor);
-  },
-
-  /**
-   * Remove the specified actor from the default actor pool.
-   *
-   * @param BreakpointActor aActor
-   *        The actor object.
-   */
-  removeFromParentPool: function(aActor) {
-    this.conn.removeActor(aActor);
   }
-}
+};
 
 RootActor.prototype.requestTypes = {
   "listTabs": RootActor.prototype.onListTabs,
   "listAddons": RootActor.prototype.onListAddons,
   "echo": RootActor.prototype.onEcho
 };
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -398,19 +398,17 @@ EventLoop.prototype = {
 /**
  * Creates a ThreadActor.
  *
  * ThreadActors manage a JSInspector object and manage execution/inspection
  * of debuggees.
  *
  * @param aHooks object
  *        An object with preNest and postNest methods for calling when entering
- *        and exiting a nested event loop, addToParentPool and
- *        removeFromParentPool methods for handling the lifetime of actors that
- *        will outlive the thread, like breakpoints.
+ *        and exiting a nested event loop.
  * @param aGlobal object [optional]
  *        An optional (for content debugging only) reference to the content
  *        window.
  */
 function ThreadActor(aHooks, aGlobal)
 {
   this._state = "detached";
   this._frameActors = [];
@@ -1363,17 +1361,17 @@ ThreadActor.prototype = {
     if (storedBp.actor) {
       actor = storedBp.actor;
     } else {
       storedBp.actor = actor = new BreakpointActor(this, {
         url: aLocation.url,
         line: aLocation.line,
         column: aLocation.column
       });
-      this._hooks.addToParentPool(actor);
+      this.threadLifetimePool.addActor(actor);
     }
 
     // Find all scripts matching the given location
     let scripts = this.dbg.findScripts(aLocation);
     if (scripts.length == 0) {
       return {
         error: "noScript",
         actor: actor.actorID
@@ -3563,17 +3561,17 @@ BreakpointActor.prototype = {
    * Handle a protocol request to remove this breakpoint.
    *
    * @param aRequest object
    *        The protocol request object.
    */
   onDelete: function (aRequest) {
     // Remove from the breakpoint store.
     this.threadActor.breakpointStore.removeBreakpoint(this.location);
-    this.threadActor._hooks.removeFromParentPool(this);
+    this.threadActor.threadLifetimePool.removeActor(this);
     // Remove the actual breakpoint from the associated scripts.
     this.removeScripts();
     return { from: this.actorID };
   }
 };
 
 BreakpointActor.prototype.requestTypes = {
   "delete": BreakpointActor.prototype.onDelete
@@ -3813,19 +3811,17 @@ Object.defineProperty(Debugger.Frame.pro
  *
  * @param aConnection object
  *        The DebuggerServerConnection with which this ChromeDebuggerActor
  *        is associated. (Currently unused, but required to make this
  *        constructor usable with addGlobalActor.)
  *
  * @param aHooks object
  *        An object with preNest and postNest methods for calling when entering
- *        and exiting a nested event loop and also addToParentPool and
- *        removeFromParentPool methods for handling the lifetime of actors that
- *        will outlive the thread, like breakpoints.
+ *        and exiting a nested event loop.
  */
 function ChromeDebuggerActor(aConnection, aHooks)
 {
   ThreadActor.call(this, aHooks);
 }
 
 ChromeDebuggerActor.prototype = Object.create(ThreadActor.prototype);
 
--- a/toolkit/devtools/server/actors/webbrowser.js
+++ b/toolkit/devtools/server/actors/webbrowser.js
@@ -498,38 +498,16 @@ BrowserTabActor.prototype = {
   _tabPool: null,
   get tabActorPool() { return this._tabPool; },
 
   _contextPool: null,
   get contextActorPool() { return this._contextPool; },
 
   _pendingNavigation: null,
 
-  /**
-   * Add the specified actor to the default actor pool connection, in order to
-   * keep it alive as long as the server is. This is used by breakpoints in the
-   * thread actor.
-   *
-   * @param actor aActor
-   *        The actor object.
-   */
-  addToParentPool: function BTA_addToParentPool(aActor) {
-    this.conn.addActor(aActor);
-  },
-
-  /**
-   * Remove the specified actor from the default actor pool.
-   *
-   * @param BreakpointActor aActor
-   *        The actor object.
-   */
-  removeFromParentPool: function BTA_removeFromParentPool(aActor) {
-    this.conn.removeActor(aActor);
-  },
-
   // A constant prefix that will be used to form the actor ID by the server.
   actorPrefix: "tab",
 
   /**
    * Getter for the tab title.
    * @return string
    *         Tab title.
    */
--- a/toolkit/devtools/server/tests/unit/testactors.js
+++ b/toolkit/devtools/server/tests/unit/testactors.js
@@ -100,24 +100,15 @@ TestTabActor.prototype = {
     if (!this._attached) {
       return { "error":"wrongState" };
     }
     return { type: "detached" };
   },
 
   /* Support for DebuggerServer.addTabActor. */
   _createExtraActors: CommonCreateExtraActors,
-  _appendExtraActors: CommonAppendExtraActors,
-
-  // Hooks for use by TestTabActors.
-  addToParentPool: function(aActor) {
-    this.conn.addActor(aActor);
-  },
-
-  removeFromParentPool: function(aActor) {
-    this.conn.removeActor(aActor);
-  }
+  _appendExtraActors: CommonAppendExtraActors
 };
 
 TestTabActor.prototype.requestTypes = {
   "attach": TestTabActor.prototype.onAttach,
   "detach": TestTabActor.prototype.onDetach
 };
--- a/toolkit/mozapps/installer/packager.mk
+++ b/toolkit/mozapps/installer/packager.mk
@@ -393,22 +393,24 @@ INNER_BACKGROUND_TESTS_PACKAGE= \
   $(call RELEASE_SIGN_ANDROID_APK,$(BACKGROUND_TESTS_PATH)/background-debug-unsigned-unaligned.apk,$(_ABS_DIST)/background.apk)
 endif
 else
 INNER_ROBOCOP_PACKAGE=echo 'Testing is disabled - No Android Robocop for you'
 INNER_BACKGROUND_TESTS_PACKAGE=echo 'Testing is disabled - No Android Background tests for you'
 endif
 
 # Create geckoview_library/geckoview_{assets,library}.zip for third-party GeckoView consumers.
+ifdef NIGHTLY_BUILD
 ifndef MOZ_DISABLE_GECKOVIEW
 INNER_MAKE_GECKOVIEW_LIBRARY= \
   $(MAKE) -C ../mobile/android/geckoview_library package ABI_DIR=$(ABI_DIR)
 else
 INNER_MAKE_GECKOVIEW_LIBRARY=echo 'GeckoView library packaging is disabled'
 endif
+endif
 
 ifdef MOZ_OMX_PLUGIN
 DIST_FILES += libomxplugin.so libomxplugingb.so libomxplugingb235.so libomxpluginhc.so libomxpluginfroyo.so
 endif
 
 SO_LIBRARIES := $(filter %.so,$(DIST_FILES))
 # These libraries are placed in the assets/ directory by packager.py.
 ASSET_SO_LIBRARIES := $(addprefix assets/,$(filter-out libmozglue.so $(MOZ_CHILD_PROCESS_NAME),$(SO_LIBRARIES)))