Merge fx-team to m-c
authorWes Kocher <wkocher@mozilla.com>
Thu, 13 Feb 2014 17:28:56 -0800
changeset 168714 d275eebfae041f3c495890e9a9e215494576bc72
parent 168682 5f1d4098333f2b45e5250f0ca8d5337d75a5aaed (current diff)
parent 168713 5748e84048b32afacf180a0cc44b3455509cdf46 (diff)
child 168715 c164916b5dc8774d4a4972b8586cbb608d1b5f93
child 168746 6687d299c464f312e6cb50f8985afd25a0a7994e
child 168755 cf1a3ec2a7c76892e99bdb7d4349a975f458670a
child 168775 27878856bd4c8b5f2ec40e185ed54b04f1759d18
push id39779
push userkwierso@gmail.com
push dateFri, 14 Feb 2014 02:50:19 +0000
treeherdermozilla-inbound@c164916b5dc8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone30.0a1
first release with
nightly linux32
d275eebfae04 / 30.0a1 / 20140214030202 / files
nightly linux64
d275eebfae04 / 30.0a1 / 20140214030202 / files
nightly mac
d275eebfae04 / 30.0a1 / 20140214030202 / files
nightly win32
d275eebfae04 / 30.0a1 / 20140214030202 / files
nightly win64
d275eebfae04 / 30.0a1 / 20140214030202 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c
mobile/android/base/resources/layout/home_empty_page.xml
mobile/android/base/resources/layout/home_empty_reading_page.xml
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -241,16 +241,17 @@ skip-if = os == "mac" # Intermittent fai
 [browser_bug880101.js]
 [browser_bug882977.js]
 [browser_bug902156.js]
 [browser_bug906190.js]
 [browser_canonizeURL.js]
 [browser_clearplugindata.js]
 [browser_contentAreaClick.js]
 [browser_contextSearchTabPosition.js]
+skip-if = os == "mac" # bug 967013, bug 926729
 [browser_ctrlTab.js]
 [browser_customize_popupNotification.js]
 [browser_datareporting_notification.js]
 run-if = datareporting
 [browser_discovery.js]
 [browser_duplicateIDs.js]
 [browser_drag.js]
 skip-if = true # browser_drag.js is disabled, as it needs to be updated for the new behavior from bug 320638.
@@ -300,16 +301,17 @@ skip-if = true # bug 432425
 [browser_sanitize-timespans.js]
 [browser_sanitizeDialog.js]
 [browser_sanitizeDialog_treeView.js]
 skip-if = true  # disabled until the tree view is added
                 # back to the clear recent history dialog (sanitize.xul), if
                 # it ever is (bug 480169)
 [browser_save_link-perwindowpb.js]
 [browser_save_private_link_perwindowpb.js]
+skip-if = os == "linux" # bug 857427
 [browser_save_video.js]
 [browser_scope.js]
 [browser_selectTabAtIndex.js]
 [browser_tabDrop.js]
 [browser_tabMatchesInAwesomebar_perwindowpb.js]
 [browser_tab_drag_drop_perwindow.js]
 [browser_tab_dragdrop.js]
 [browser_tab_dragdrop2.js]
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -107,23 +107,23 @@
                        label="&viewBookmarksToolbar.label;"
                        type="checkbox"
                        toolbarId="PersonalToolbar"
                        class="subviewbutton"
                        oncommand="onViewToolbarCommand(event); PanelUI.hide();"/>
         <toolbarseparator/>
         <toolbarbutton id="panelMenu_bookmarksToolbar"
                        label="&personalbarCmd.label;"
-                       class="subviewbutton"
+                       class="subviewbutton cui-withicon"
                        oncommand="PlacesCommandHook.showPlacesOrganizer('BookmarksToolbar'); PanelUI.hide();"/>
         <toolbarbutton id="panelMenu_unsortedBookmarks"
                        label="&unsortedBookmarksCmd.label;"
-                       class="subviewbutton"
+                       class="subviewbutton cui-withicon"
                        oncommand="PlacesCommandHook.showPlacesOrganizer('UnfiledBookmarks'); PanelUI.hide();"/>
-        <toolbarseparator/>
+        <toolbarseparator class="small-separator"/>
         <toolbaritem id="panelMenu_bookmarksMenu"
                      orient="vertical"
                      smoothscroll="false"
                      onclick="if (event.button == 1) BookmarkingUI.onPanelMenuViewCommand(event, this._placesView);"
                      oncommand="BookmarkingUI.onPanelMenuViewCommand(event, this._placesView);"
                      flatList="true"
                      tooltip="bhTooltip">
           <!-- bookmarks menu items will go here -->
--- a/browser/components/customizableui/src/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/src/CustomizableWidgets.jsm
@@ -657,16 +657,17 @@ const CustomizableWidgets = [{
 
       containerElem.addEventListener("command", this.onCommand, false);
 
       let list = this.charsetInfo[aSection];
 
       for (let item of list) {
         let elem = aDocument.createElementNS(kNSXUL, "toolbarbutton");
         elem.setAttribute("label", item.label);
+        elem.setAttribute("type", "checkbox");
         elem.section = aSection == "detectors" ? "detectors" : "charsets";
         elem.value = item.id;
         elem.setAttribute("class", "subviewbutton");
         containerElem.appendChild(elem);
       }
     },
     updateCurrentCharset: function(aDocument) {
       let content = aDocument.defaultView.content;
@@ -701,19 +702,19 @@ const CustomizableWidgets = [{
       let disabled = this.maybeDisableMenu(aElements[0].ownerDocument);
       for (let elem of aElements) {
         if (disabled) {
           elem.setAttribute("disabled", "true");
         } else {
           elem.removeAttribute("disabled");
         }
         if (elem.value.toLowerCase() == aCurrentItem.toLowerCase()) {
-          elem.setAttribute("current", "true");
+          elem.setAttribute("checked", "true");
         } else {
-          elem.removeAttribute("current");
+          elem.removeAttribute("checked");
         }
       }
     },
     onViewShowing: function(aEvent) {
       let document = aEvent.target.ownerDocument;
 
       let autoDetectLabelId = "PanelUI-characterEncodingView-autodetect-label";
       let autoDetectLabel = document.getElementById(autoDetectLabelId);
--- a/browser/components/customizableui/src/CustomizeMode.jsm
+++ b/browser/components/customizableui/src/CustomizeMode.jsm
@@ -1117,18 +1117,20 @@ CustomizeMode.prototype = {
       // before this fires, which leaves us with placeholders inserted after
       // we've exited. So we need to check that we are indeed customizing.
       if (this._customizing && !this._transitioning) {
         item.hidden = true;
         this._showPanelCustomizationPlaceholders();
         DragPositionManager.start(this.window);
         if (item.nextSibling) {
           this._setDragActive(item.nextSibling, "before", draggedItem.id, isInToolbar);
+          this._dragOverItem = item.nextSibling;
         } else if (isInToolbar && item.previousSibling) {
           this._setDragActive(item.previousSibling, "after", draggedItem.id, isInToolbar);
+          this._dragOverItem = item.previousSibling;
         }
       }
       this._initializeDragAfterMove = null;
       this.window.clearTimeout(this._dragInitializeTimeout);
     }.bind(this);
     this._dragInitializeTimeout = this.window.setTimeout(this._initializeDragAfterMove, 0);
   },
 
--- a/browser/components/places/content/browserPlacesViews.js
+++ b/browser/components/places/content/browserPlacesViews.js
@@ -288,16 +288,17 @@ PlacesViewBase.prototype = {
   _createMenuItemForPlacesNode:
   function PVB__createMenuItemForPlacesNode(aPlacesNode) {
     this._domNodes.delete(aPlacesNode);
 
     let element;
     let type = aPlacesNode.type;
     if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
       element = document.createElement("menuseparator");
+      element.setAttribute("class", "small-separator");
     }
     else {
       let itemId = aPlacesNode.itemId;
       if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI) {
         element = document.createElement("menuitem");
         element.className = "menuitem-iconic bookmark-item menuitem-with-favicon";
         element.setAttribute("scheme",
                              PlacesUIUtils.guessUrlSchemeForUI(aPlacesNode.uri));
@@ -1803,16 +1804,17 @@ PlacesPanelMenuView.prototype = {
   _insertNewItem:
   function PAMV__insertNewItem(aChild, aBefore) {
     this._domNodes.delete(aChild);
 
     let type = aChild.type;
     let button;
     if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
       button = document.createElement("toolbarseparator");
+      button.setAttribute("class", "small-separator");
     }
     else {
       button = document.createElement("toolbarbutton");
       let className = "bookmark-item";
       if (typeof this.options.extraClasses.mainLevel == "string")
         className += " " + this.options.extraClasses.mainLevel;
       button.className = className;
       button.setAttribute("label", aChild.title);
--- a/browser/components/places/content/menu.xml
+++ b/browser/components/places/content/menu.xml
@@ -484,17 +484,17 @@
       ]]></handler>
 
     </handlers>
   </binding>
 
   <!-- Most of this is copied from the arrowpanel binding in popup.xml -->
   <binding id="places-popup-arrow"
            extends="chrome://browser/content/places/menu.xml#places-popup-base">
-    <content flip="both" side="top" position="bottomcenter topleft">
+    <content flip="both" side="top" position="bottomcenter topright">
       <xul:vbox anonid="container" class="panel-arrowcontainer" flex="1"
                xbl:inherits="side,panelopen">
         <xul:box anonid="arrowbox" class="panel-arrowbox">
           <xul:image anonid="arrow" class="panel-arrow" xbl:inherits="side"/>
         </xul:box>
         <xul:box class="panel-arrowcontent" xbl:inherits="side,align,dir,orient,pack" flex="1">
           <xul:vbox class="menupopup-drop-indicator-bar" hidden="true">
             <xul:image class="menupopup-drop-indicator" mousethrough="always"/>
--- a/browser/devtools/netmonitor/netmonitor-controller.js
+++ b/browser/devtools/netmonitor/netmonitor-controller.js
@@ -52,16 +52,19 @@ const EVENTS = {
   RECEIVED_RESPONSE_CONTENT: "NetMonitor:NetworkEventUpdated:ResponseContent",
 
   // When the request post params are displayed in the UI.
   REQUEST_POST_PARAMS_DISPLAYED: "NetMonitor:RequestPostParamsAvailable",
 
   // When the response body is displayed in the UI.
   RESPONSE_BODY_DISPLAYED: "NetMonitor:ResponseBodyAvailable",
 
+  // When the html response preview is displayed in the UI.
+  RESPONSE_HTML_PREVIEW_DISPLAYED: "NetMonitor:ResponseHtmlPreviewAvailable",
+
   // When `onTabSelect` is fired and subsequently rendered.
   TAB_UPDATED: "NetMonitor:TabUpdated",
 
   // Fired when Sidebar has finished being populated.
   SIDEBAR_POPULATED: "NetMonitor:SidebarPopulated",
 
   // Fired when NetworkDetailsView has finished being populated.
   NETWORKDETAILSVIEW_POPULATED: "NetMonitor:NetworkDetailsViewPopulated",
@@ -407,18 +410,17 @@ TargetEventsHandler.prototype = {
    * @param object aPacket
    *        Packet received from the server.
    */
   _onTabNavigated: function(aType, aPacket) {
     switch (aType) {
       case "will-navigate": {
         // Reset UI.
         NetMonitorView.RequestsMenu.reset();
-        NetMonitorView.Sidebar.reset();
-        NetMonitorView.NetworkDetails.reset();
+        NetMonitorView.Sidebar.toggle(false);
 
         // Switch to the default network traffic inspector view.
         if (NetMonitorController.getCurrentActivity() == ACTIVITY_TYPE.NONE) {
           NetMonitorView.showNetworkInspectorView();
         }
 
         window.emit(EVENTS.TARGET_WILL_NAVIGATE);
         break;
--- a/browser/devtools/netmonitor/netmonitor-view.js
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -50,18 +50,17 @@ const DEFAULT_EDITOR_CONFIG = {
 const GENERIC_VARIABLES_VIEW_SETTINGS = {
   lazyEmpty: true,
   lazyEmptyDelay: 10, // ms
   searchEnabled: true,
   editableValueTooltip: "",
   editableNameTooltip: "",
   preventDisableOnChange: true,
   preventDescriptorModifiers: true,
-  eval: () => {},
-  switch: () => {}
+  eval: () => {}
 };
 const NETWORK_ANALYSIS_PIE_CHART_DIAMETER = 200; // px
 
 /**
  * Object defining the network monitor view components.
  */
 let NetMonitorView = {
   /**
@@ -1046,16 +1045,19 @@ RequestsMenuView.prototype = Heritage.ex
     // Freshly added requests may not yet contain all the information required
     // for sorting and filtering predicates, so this is done each time the
     // network requests table is flushed (don't worry, events are drained first
     // so this doesn't happen once per network event update).
     this.sortContents();
     this.filterContents();
     this.refreshSummary();
     this.refreshZebra();
+
+    // Rescale all the waterfalls so that everything is visible at once.
+    this._flushWaterfallViews();
   },
 
   /**
    * Customization function for creating an item's UI.
    *
    * @param string aMethod
    *        Specifies the request method (e.g. "GET", "POST", etc.)
    * @param string aUrl
@@ -1180,33 +1182,31 @@ RequestsMenuView.prototype = Heritage.ex
       // One millisecond == one unscaled pixel.
       if (width > 0) {
         let timingBox = document.createElement("hbox");
         timingBox.className = "requests-menu-timings-box " + key;
         timingBox.setAttribute("width", width);
         timingsNode.insertBefore(timingBox, timingsTotal);
       }
     }
-
-    // Don't paint things while the waterfall view isn't even visible.
-    if (NetMonitorView.currentFrontendMode != "network-inspector-view") {
-      return;
-    }
-
-    // Rescale all the waterfalls so that everything is visible at once.
-    this._flushWaterfallViews();
   },
 
   /**
    * Rescales and redraws all the waterfall views in this container.
    *
    * @param boolean aReset
    *        True if this container's width was changed.
    */
   _flushWaterfallViews: function(aReset) {
+    // Don't paint things while the waterfall view isn't even visible,
+    // or there are no items added to this container.
+    if (NetMonitorView.currentFrontendMode != "network-inspector-view" || !this.itemCount) {
+      return;
+    }
+
     // To avoid expensive operations like getBoundingClientRect() and
     // rebuilding the waterfall background each time a new request comes in,
     // stuff is cached. However, in certain scenarios like when the window
     // is resized, this needs to be invalidated.
     if (aReset) {
       this._cachedWaterfallWidth = 0;
       this._hideOverflowingColumns();
     }
@@ -1424,21 +1424,16 @@ RequestsMenuView.prototype = Heritage.ex
       NetMonitorView.Sidebar.toggle(false);
     }
   },
 
   /**
    * The resize listener for this container's window.
    */
   _onResize: function(e) {
-    // Don't paint things while the waterfall view isn't even visible.
-    if (NetMonitorView.currentFrontendMode != "network-inspector-view") {
-      return;
-    }
-
     // Allow requests to settle down first.
     setNamedTimeout(
       "resize-events", RESIZE_REFRESH_RATE, () => this._flushWaterfallViews(true));
   },
 
   /**
    * Handle the context menu opening. Hide items if no request is selected.
    */
@@ -1629,23 +1624,16 @@ SidebarView.prototype = {
     let view = isCustom ?
       NetMonitorView.CustomRequest :
       NetMonitorView.NetworkDetails;
 
     return view.populate(aData).then(() => {
       $("#details-pane").selectedIndex = isCustom ? 0 : 1
       window.emit(EVENTS.SIDEBAR_POPULATED);
     });
-  },
-
-  /**
-   * Hides this container.
-   */
-  reset: function() {
-    this.toggle(false);
   }
 }
 
 /**
  * Functions handling the custom request view.
  */
 function CustomRequestView() {
   dumpn("CustomRequestView was instantiated");
@@ -1833,39 +1821,44 @@ NetworkDetailsView.prototype = {
   /**
    * Destruction function, called when the network monitor is closed.
    */
   destroy: function() {
     dumpn("Destroying the NetworkDetailsView");
   },
 
   /**
-   * Resets this container (removes all the networking information).
-   */
-  reset: function() {
-    this._dataSrc = null;
-  },
-
-  /**
    * Populates this view with the specified data.
    *
    * @param object aData
    *        The data source (this should be the attachment of a request item).
    * @return object
    *        Returns a promise that resolves upon population the view.
    */
   populate: function(aData) {
     $("#request-params-box").setAttribute("flex", "1");
     $("#request-params-box").hidden = false;
     $("#request-post-data-textarea-box").hidden = true;
     $("#response-content-info-header").hidden = true;
     $("#response-content-json-box").hidden = true;
     $("#response-content-textarea-box").hidden = true;
     $("#response-content-image-box").hidden = true;
 
+    let isHtml = RequestsMenuView.prototype.isHtml({ attachment: aData });
+
+    // Show the "Preview" tabpanel only for plain HTML responses.
+    $("#preview-tab").hidden = !isHtml;
+    $("#preview-tabpanel").hidden = !isHtml;
+
+    // Switch to the "Headers" tabpanel if the "Preview" previously selected
+    // and this is not an HTML response.
+    if (!isHtml && this.widget.selectedIndex == 5) {
+      this.widget.selectedIndex = 0;
+    }
+
     this._headers.empty();
     this._cookies.empty();
     this._params.empty();
     this._json.empty();
 
     this._dataSrc = { src: aData, populated: [] };
     this._onTabSelect();
     window.emit(EVENTS.NETWORKDETAILSVIEW_POPULATED);
@@ -1902,16 +1895,19 @@ NetworkDetailsView.prototype = {
           yield view._setRequestPostParams(src.requestHeaders, src.requestPostData);
           break;
         case 3: // "Response"
           yield view._setResponseBody(src.url, src.responseContent);
           break;
         case 4: // "Timings"
           yield view._setTimingsInformation(src.eventTimings);
           break;
+        case 5: // "Preview"
+          yield view._setHtmlPreview(src.responseContent);
+          break;
       }
       populated[tab] = true;
       window.emit(EVENTS.TAB_UPDATED);
     });
   },
 
   /**
    * Sets the network request summary shown in this view.
@@ -2099,42 +2095,55 @@ NetworkDetailsView.prototype = {
    *        The "requestPostData" message received from the server.
    * @return object
    *        A promise that is resolved when the request post params are set.
    */
   _setRequestPostParams: function(aHeadersResponse, aPostDataResponse) {
     if (!aHeadersResponse || !aPostDataResponse) {
       return promise.resolve();
     }
-    return gNetwork.getString(aPostDataResponse.postData.text).then(aString => {
-      // Handle query strings (poor man's forms, e.g. "?foo=bar&baz=42").
-      let cType = aHeadersResponse.headers.filter(({ name }) => name == "Content-Type")[0];
-      let cString = cType ? cType.value : "";
-      if (cString.contains("x-www-form-urlencoded") ||
-          aString.contains("x-www-form-urlencoded")) {
-        let formDataGroups = aString.split(/\r\n|\n|\r/);
-        for (let group of formDataGroups) {
-          this._addParams(this._paramsFormData, group);
+    return gNetwork.getString(aPostDataResponse.postData.text).then(aPostData => {
+      let contentTypeHeader = aHeadersResponse.headers.filter(({ name }) => name == "Content-Type")[0];
+      let contentTypeLongString = contentTypeHeader ? contentTypeHeader.value : "";
+
+      return gNetwork.getString(contentTypeLongString).then(aContentType => {
+        let urlencoded = "x-www-form-urlencoded";
+
+        // Handle query strings (poor man's forms, e.g. "?foo=bar&baz=42").
+        if (aContentType.contains(urlencoded)) {
+          let formDataGroups = aPostData.split(/\r\n|\r|\n/);
+          for (let group of formDataGroups) {
+            this._addParams(this._paramsFormData, group);
+          }
         }
-      }
-      // Handle actual forms ("multipart/form-data" content type).
-      else {
-        // This is really awkward, but hey, it works. Let's show an empty
-        // scope in the params view and place the source editor containing
-        // the raw post data directly underneath.
-        $("#request-params-box").removeAttribute("flex");
-        let paramsScope = this._params.addScope(this._paramsPostPayload);
-        paramsScope.expanded = true;
-        paramsScope.locked = true;
+        // Handle actual forms ("multipart/form-data" content type).
+        else {
+          // This is really awkward, but hey, it works. Let's show an empty
+          // scope in the params view and place the source editor containing
+          // the raw post data directly underneath.
+          $("#request-params-box").removeAttribute("flex");
+          let paramsScope = this._params.addScope(this._paramsPostPayload);
+          paramsScope.expanded = true;
+          paramsScope.locked = true;
 
-        $("#request-post-data-textarea-box").hidden = false;
-        return NetMonitorView.editor("#request-post-data-textarea").then(aEditor => {
-          aEditor.setText(aString);
-        });
-      }
+          $("#request-post-data-textarea-box").hidden = false;
+          return NetMonitorView.editor("#request-post-data-textarea").then(aEditor => {
+            // Most POST bodies are usually JSON, so they can be neatly
+            // syntax highlighted as JS. Otheriwse, fall back to plain text.
+            try {
+              JSON.parse(aPostData);
+              aEditor.setMode(Editor.modes.js);
+            } catch (e) {
+              aEditor.setMode(Editor.modes.text);
+            } finally {
+              aEditor.setText(aPostData);
+            }
+          });
+        }
+      });
     }).then(() => window.emit(EVENTS.REQUEST_POST_PARAMS_DISPLAYED));
   },
 
   /**
    * Populates the params container in this view with the specified data.
    *
    * @param string aName
    *        The type of params to populate (get or post).
@@ -2145,18 +2154,18 @@ NetworkDetailsView.prototype = {
     let paramsArray = parseQueryString(aQueryString);
     if (!paramsArray) {
       return;
     }
     let paramsScope = this._params.addScope(aName);
     paramsScope.expanded = true;
 
     for (let param of paramsArray) {
-      let headerVar = paramsScope.addItem(param.name, {}, true);
-      headerVar.setGrip(param.value);
+      let paramVar = paramsScope.addItem(param.name, {}, true);
+      paramVar.setGrip(param.value);
     }
   },
 
   /**
    * Sets the network response body shown in this view.
    *
    * @param string aUrl
    *        The request's url.
@@ -2340,16 +2349,40 @@ NetworkDetailsView.prototype = {
     $("#timings-summary-send .requests-menu-timings-total")
       .style.transform = "translateX(" + (scale * (blocked + dns + connect)) + "px)";
     $("#timings-summary-wait .requests-menu-timings-total")
       .style.transform = "translateX(" + (scale * (blocked + dns + connect + send)) + "px)";
     $("#timings-summary-receive .requests-menu-timings-total")
       .style.transform = "translateX(" + (scale * (blocked + dns + connect + send + wait)) + "px)";
   },
 
+  /**
+   * Sets the preview for HTML responses shown in this view.
+   *
+   * @param object aResponse
+   *        The message received from the server.
+   * @return object
+   *        A promise that is resolved when the response body is set
+   */
+  _setHtmlPreview: function(aResponse) {
+    if (!aResponse) {
+      return promise.resolve();
+    }
+    let { text } = aResponse.content;
+    let iframe = $("#response-preview");
+
+    return gNetwork.getString(text).then(aString => {
+      // Always disable JS when previewing HTML responses.
+      iframe.contentDocument.docShell.allowJavascript = false;
+      iframe.contentDocument.documentElement.innerHTML = aString;
+
+      window.emit(EVENTS.RESPONSE_HTML_PREVIEW_DISPLAYED);
+    });
+  },
+
   _dataSrc: null,
   _headers: null,
   _cookies: null,
   _params: null,
   _json: null,
   _paramsQueryString: "",
   _paramsFormData: "",
   _paramsPostPayload: "",
@@ -2565,24 +2598,26 @@ nsIURL.store = new Map();
  *
  * @param string aQueryString
  *        The query part of a url
  * @return array
  *         Array of query params {name, value}
  */
 function parseQueryString(aQueryString) {
   // Make sure there's at least one param available.
-  if (!aQueryString || !aQueryString.contains("=")) {
+  // Be careful here, params don't necessarily need to have values, so
+  // no need to verify the existence of a "=".
+  if (!aQueryString) {
     return;
   }
   // Turn the params string into an array containing { name: value } tuples.
   let paramsArray = aQueryString.replace(/^[?&]/, "").split("&").map(e =>
     let (param = e.split("=")) {
-      name: NetworkHelper.convertToUnicode(unescape(param[0])),
-      value: NetworkHelper.convertToUnicode(unescape(param[1]))
+      name: param[0] ? NetworkHelper.convertToUnicode(unescape(param[0])) : "",
+      value: param[1] ? NetworkHelper.convertToUnicode(unescape(param[1])) : ""
     });
   return paramsArray;
 }
 
 /**
  * Parse text representation of HTTP headers.
  *
  * @param string aText
--- a/browser/devtools/netmonitor/netmonitor.xul
+++ b/browser/devtools/netmonitor/netmonitor.xul
@@ -8,17 +8,18 @@
 <?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/devtools/netmonitor.css" type="text/css"?>
 <!DOCTYPE window [
   <!ENTITY % netmonitorDTD SYSTEM "chrome://browser/locale/devtools/netmonitor.dtd">
   %netmonitorDTD;
 ]>
 
-<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml">
 
   <script type="application/javascript;version=1.8"
           src="chrome://browser/content/devtools/theme-switching.js"/>
   <script type="text/javascript" src="netmonitor-controller.js"/>
   <script type="text/javascript" src="netmonitor-view.js"/>
 
   <popupset id="networkPopupSet">
     <menupopup id="network-request-popup">
@@ -233,24 +234,31 @@
                        wrap="off"
                        data-key="body"/>
             </vbox>
           </vbox>
           <tabbox id="event-details-pane"
                   class="devtools-sidebar-tabs"
                   handleCtrlTab="false">
             <tabs>
-              <tab label="&netmonitorUI.tab.headers;"/>
-              <tab label="&netmonitorUI.tab.cookies;"/>
-              <tab label="&netmonitorUI.tab.params;"/>
-              <tab label="&netmonitorUI.tab.response;"/>
-              <tab label="&netmonitorUI.tab.timings;"/>
+              <tab id="headers-tab"
+                   label="&netmonitorUI.tab.headers;"/>
+              <tab id="cookies-tab"
+                   label="&netmonitorUI.tab.cookies;"/>
+              <tab id="params-tab"
+                   label="&netmonitorUI.tab.params;"/>
+              <tab id="response-tab"
+                   label="&netmonitorUI.tab.response;"/>
+              <tab id="timings-tab"
+                   label="&netmonitorUI.tab.timings;"/>
+              <tab id="preview-tab"
+                   label="&netmonitorUI.tab.preview;"/>
             </tabs>
             <tabpanels flex="1">
-              <tabpanel id="headers-tabppanel"
+              <tabpanel id="headers-tabpanel"
                         class="tabpanel-content">
                 <vbox flex="1">
                   <hbox id="headers-summary-url"
                         class="tabpanel-summary-container"
                         align="center">
                     <label class="plain tabpanel-summary-label"
                            value="&netmonitorUI.summary.url;"/>
                     <label id="headers-summary-url-value"
@@ -408,16 +416,22 @@
                         align="center">
                     <label class="plain tabpanel-summary-label"
                            value="&netmonitorUI.timings.receive;"/>
                     <hbox class="requests-menu-timings-box receive"/>
                     <label class="plain requests-menu-timings-total"/>
                   </hbox>
                 </vbox>
               </tabpanel>
+              <tabpanel id="preview-tabpanel"
+                        class="tabpanel-content">
+                <html:iframe id="response-preview"
+                             frameborder="0"
+                             sandbox=""/>
+              </tabpanel>
             </tabpanels>
           </tabbox>
         </deck>
       </hbox>
 
       <hbox id="requests-menu-footer">
         <button id="requests-menu-filter-all-button"
                 class="requests-menu-filter-button requests-menu-footer-button"
--- a/browser/devtools/netmonitor/test/browser.ini
+++ b/browser/devtools/netmonitor/test/browser.ini
@@ -8,16 +8,17 @@ support-files =
   html_filter-test-page.html
   html_infinite-get-page.html
   html_json-custom-mime-test-page.html
   html_json-long-test-page.html
   html_json-malformed-test-page.html
   html_json-text-mime-test-page.html
   html_jsonp-test-page.html
   html_navigate-test-page.html
+  html_params-test-page.html
   html_post-data-test-page.html
   html_post-raw-test-page.html
   html_simple-test-page.html
   html_sorting-test-page.html
   html_statistics-test-page.html
   html_status-codes-test-page.html
   sjs_content-type-test-server.sjs
   sjs_simple-test-server.sjs
@@ -30,25 +31,27 @@ support-files =
 [browser_net_accessibility-02.js]
 [browser_net_autoscroll.js]
 [browser_net_charts-01.js]
 [browser_net_charts-02.js]
 [browser_net_charts-03.js]
 [browser_net_charts-04.js]
 [browser_net_charts-05.js]
 [browser_net_clear.js]
+[browser_net_complex-params.js]
 [browser_net_content-type.js]
+[browser_net_copy_image_as_data_uri.js]
 [browser_net_copy_url.js]
-[browser_net_copy_image_as_data_uri.js]
 [browser_net_cyrillic-01.js]
 [browser_net_cyrillic-02.js]
 [browser_net_filter-01.js]
 [browser_net_filter-02.js]
 [browser_net_filter-03.js]
 [browser_net_footer-summary.js]
+[browser_net_html-preview.js]
 [browser_net_json-long.js]
 [browser_net_json-malformed.js]
 [browser_net_json_custom_mime.js]
 [browser_net_json_text_mime.js]
 [browser_net_jsonp.js]
 [browser_net_large-response.js]
 [browser_net_open_request_in_tab.js]
 [browser_net_page-nav.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_complex-params.js
@@ -0,0 +1,148 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests whether complex request params and payload sent via POST are
+ * displayed correctly.
+ */
+
+function test() {
+  initNetMonitor(PARAMS_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let { document, L10N, EVENTS, Editor, NetMonitorView } = aMonitor.panelWin;
+    let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+    RequestsMenu.lazyUpdate = false;
+    NetworkDetails._params.lazyEmpty = false;
+
+    Task.spawn(function () {
+      yield waitForNetworkEvents(aMonitor, 0, 6);
+
+      EventUtils.sendMouseEvent({ type: "mousedown" },
+        document.getElementById("details-pane-toggle"));
+      EventUtils.sendMouseEvent({ type: "mousedown" },
+        document.querySelectorAll("#details-pane tab")[2]);
+
+      yield waitFor(aMonitor.panelWin, EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+      yield testParamsTab1('a', '""', '{ "foo": "bar" }', '""');
+
+      RequestsMenu.selectedIndex = 1;
+      yield waitFor(aMonitor.panelWin, EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+      yield testParamsTab1('a', '"b"', '{ "foo": "bar" }', '""');
+
+      RequestsMenu.selectedIndex = 2;
+      yield waitFor(aMonitor.panelWin, EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+      yield testParamsTab1('a', '"b"', 'foo', '"bar"');
+
+      RequestsMenu.selectedIndex = 3;
+      yield waitFor(aMonitor.panelWin, EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+      yield testParamsTab2('a', '""', '{ "foo": "bar" }', "js");
+
+      RequestsMenu.selectedIndex = 4;
+      yield waitFor(aMonitor.panelWin, EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+      yield testParamsTab2('a', '"b"', '{ "foo": "bar" }', "js");
+
+      RequestsMenu.selectedIndex = 5;
+      yield waitFor(aMonitor.panelWin, EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+      yield testParamsTab2('a', '"b"', '?foo=bar', "text");
+
+      yield teardown(aMonitor);
+      finish();
+    });
+
+    function testParamsTab1(
+      aQueryStringParamName, aQueryStringParamValue, aFormDataParamName, aFormDataParamValue)
+    {
+      let tab = document.querySelectorAll("#details-pane tab")[2];
+      let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+
+      is(tabpanel.querySelectorAll(".variables-view-scope").length, 2,
+        "The number of param scopes displayed in this tabpanel is incorrect.");
+      is(tabpanel.querySelectorAll(".variable-or-property").length, 2,
+        "The number of param values displayed in this tabpanel is incorrect.");
+      is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+        "The empty notice should not be displayed in this tabpanel.");
+
+      is(tabpanel.querySelector("#request-params-box")
+        .hasAttribute("hidden"), false,
+        "The request params box should not be hidden.");
+      is(tabpanel.querySelector("#request-post-data-textarea-box")
+        .hasAttribute("hidden"), true,
+        "The request post data textarea box should be hidden.");
+
+      let paramsScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+      let formDataScope = tabpanel.querySelectorAll(".variables-view-scope")[1];
+
+      is(paramsScope.querySelector(".name").getAttribute("value"),
+        L10N.getStr("paramsQueryString"),
+        "The params scope doesn't have the correct title.");
+      is(formDataScope.querySelector(".name").getAttribute("value"),
+        L10N.getStr("paramsFormData"),
+        "The form data scope doesn't have the correct title.");
+
+      is(paramsScope.querySelectorAll(".variables-view-variable .name")[0].getAttribute("value"),
+        aQueryStringParamName,
+        "The first query string param name was incorrect.");
+      is(paramsScope.querySelectorAll(".variables-view-variable .value")[0].getAttribute("value"),
+        aQueryStringParamValue,
+        "The first query string param value was incorrect.");
+
+      is(formDataScope.querySelectorAll(".variables-view-variable .name")[0].getAttribute("value"),
+        aFormDataParamName,
+        "The first form data param name was incorrect.");
+      is(formDataScope.querySelectorAll(".variables-view-variable .value")[0].getAttribute("value"),
+        aFormDataParamValue,
+        "The first form data param value was incorrect.");
+    }
+
+    function testParamsTab2(
+      aQueryStringParamName, aQueryStringParamValue, aRequestPayload, aEditorMode)
+    {
+      let tab = document.querySelectorAll("#details-pane tab")[2];
+      let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+
+      is(tabpanel.querySelectorAll(".variables-view-scope").length, 2,
+        "The number of param scopes displayed in this tabpanel is incorrect.");
+      is(tabpanel.querySelectorAll(".variable-or-property").length, 1,
+        "The number of param values displayed in this tabpanel is incorrect.");
+      is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+        "The empty notice should not be displayed in this tabpanel.");
+
+      is(tabpanel.querySelector("#request-params-box")
+        .hasAttribute("hidden"), false,
+        "The request params box should not be hidden.");
+      is(tabpanel.querySelector("#request-post-data-textarea-box")
+        .hasAttribute("hidden"), false,
+        "The request post data textarea box should not be hidden.");
+
+      let paramsScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+      let payloadScope = tabpanel.querySelectorAll(".variables-view-scope")[1];
+
+      is(paramsScope.querySelector(".name").getAttribute("value"),
+        L10N.getStr("paramsQueryString"),
+        "The params scope doesn't have the correct title.");
+      is(payloadScope.querySelector(".name").getAttribute("value"),
+        L10N.getStr("paramsPostPayload"),
+        "The request payload scope doesn't have the correct title.");
+
+      is(paramsScope.querySelectorAll(".variables-view-variable .name")[0].getAttribute("value"),
+        aQueryStringParamName,
+        "The first query string param name was incorrect.");
+      is(paramsScope.querySelectorAll(".variables-view-variable .value")[0].getAttribute("value"),
+        aQueryStringParamValue,
+        "The first query string param value was incorrect.");
+
+      return NetMonitorView.editor("#request-post-data-textarea").then((aEditor) => {
+        is(aEditor.getText(), aRequestPayload,
+          "The text shown in the source editor is incorrect.");
+        is(aEditor.getMode(), Editor.modes[aEditorMode],
+          "The mode active in the source editor is incorrect.");
+
+        teardown(aMonitor).then(finish);
+      });
+    }
+
+    aDebuggee.performRequests();
+  });
+}
--- a/browser/devtools/netmonitor/test/browser_net_content-type.js
+++ b/browser/devtools/netmonitor/test/browser_net_content-type.js
@@ -1,17 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests if different response content types are handled correctly.
  */
 
 function test() {
-  initNetMonitor(CONTENT_TYPE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+  initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL).then(([aTab, aDebuggee, aMonitor]) => {
     info("Starting test... ");
 
     let { document, L10N, Editor, NetMonitorView } = aMonitor.panelWin;
     let { RequestsMenu } = NetMonitorView;
 
     RequestsMenu.lazyUpdate = false;
 
     waitForNetworkEvents(aMonitor, 6).then(() => {
@@ -57,16 +57,17 @@ function test() {
           statusText: "Not Found",
           type: "html",
           fullMimeType: "text/html; charset=utf-8",
           size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02),
           time: true
         });
       verifyRequestItemTarget(RequestsMenu.getItemAtIndex(5),
         "GET", TEST_IMAGE, {
+          fuzzyUrl: true,
           status: 200,
           statusText: "OK",
           type: "png",
           fullMimeType: "image/png",
           size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.75),
           time: true
         });
 
--- a/browser/devtools/netmonitor/test/browser_net_copy_image_as_data_uri.js
+++ b/browser/devtools/netmonitor/test/browser_net_copy_image_as_data_uri.js
@@ -6,17 +6,19 @@
  */
 
 function test() {
   initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL).then(([aTab, aDebuggee, aMonitor]) => {
     info("Starting test... ");
 
     let { NetMonitorView } = aMonitor.panelWin;
     let { RequestsMenu } = NetMonitorView;
+
     RequestsMenu.lazyUpdate = false;
+
     let imageDataUri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==";
 
     waitForNetworkEvents(aMonitor, 6).then(() => {
       let requestItem = RequestsMenu.getItemAtIndex(5);
       RequestsMenu.selectedItem = requestItem;
 
       waitForClipboard(imageDataUri, function setup() {
         RequestsMenu.copyImageAsDataUri();
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_html-preview.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if html responses show and properly populate a "Preview" tab.
+ */
+
+function test() {
+  initNetMonitor(CONTENT_TYPE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let { $, document, NetMonitorView } = aMonitor.panelWin;
+    let { RequestsMenu } = NetMonitorView;
+
+    RequestsMenu.lazyUpdate = false;
+
+    waitForNetworkEvents(aMonitor, 6).then(() => {
+      EventUtils.sendMouseEvent({ type: "mousedown" },
+        document.getElementById("details-pane-toggle"));
+
+      is($("#event-details-pane").selectedIndex, 0,
+        "The first tab in the details pane should be selected.");
+      is($("#preview-tab").hidden, true,
+        "The preview tab should be hidden for non html responses.");
+      is($("#preview-tabpanel").hidden, true,
+        "The preview tabpanel should be hidden for non html responses.");
+
+      RequestsMenu.selectedIndex = 4;
+      NetMonitorView.toggleDetailsPane({ visible: true, animated: false }, 5);
+
+      is($("#event-details-pane").selectedIndex, 5,
+        "The fifth tab in the details pane should be selected.");
+      is($("#preview-tab").hidden, false,
+        "The preview tab should be visible now.");
+      is($("#preview-tabpanel").hidden, false,
+        "The preview tabpanel should be visible now.");
+
+      let RESPONSE_HTML_PREVIEW_DISPLAYED = aMonitor.panelWin.EVENTS.RESPONSE_HTML_PREVIEW_DISPLAYED;
+      waitFor(aMonitor.panelWin, RESPONSE_HTML_PREVIEW_DISPLAYED).then(() => {
+        let iframe = $("#response-preview");
+        ok(iframe,
+          "There should be a response preview iframe available.");
+        ok(iframe.contentDocument,
+          "The iframe's content document should be available.");
+        is(iframe.contentDocument.querySelector("blink").textContent, "Not Found",
+          "The iframe's content document should be loaded and correct.");
+
+        RequestsMenu.selectedIndex = 5;
+
+        is($("#event-details-pane").selectedIndex, 0,
+          "The first tab in the details pane should be selected again.");
+        is($("#preview-tab").hidden, true,
+          "The preview tab should be hidden again for non html responses.");
+        is($("#preview-tabpanel").hidden, true,
+          "The preview tabpanel should be hidden again for non html responses.");
+
+        teardown(aMonitor).then(finish);
+      });
+    });
+
+    aDebuggee.performRequests();
+  });
+}
--- a/browser/devtools/netmonitor/test/browser_net_post-data-02.js
+++ b/browser/devtools/netmonitor/test/browser_net_post-data-02.js
@@ -50,15 +50,16 @@ function test() {
         is(postScope.querySelectorAll(".variables-view-variable .name")[0].getAttribute("value"),
           "foo", "The first query param name was incorrect.");
         is(postScope.querySelectorAll(".variables-view-variable .value")[0].getAttribute("value"),
           "\"bar\"", "The first query param value was incorrect.");
         is(postScope.querySelectorAll(".variables-view-variable .name")[1].getAttribute("value"),
           "baz", "The second query param name was incorrect.");
         is(postScope.querySelectorAll(".variables-view-variable .value")[1].getAttribute("value"),
           "\"123\"", "The second query param value was incorrect.");
+
         teardown(aMonitor).then(finish);
       });
     });
 
     aDebuggee.performRequests();
   });
 }
--- a/browser/devtools/netmonitor/test/head.js
+++ b/browser/devtools/netmonitor/test/head.js
@@ -17,16 +17,17 @@ const EXAMPLE_URL = "http://example.com/
 const SIMPLE_URL = EXAMPLE_URL + "html_simple-test-page.html";
 const NAVIGATE_URL = EXAMPLE_URL + "html_navigate-test-page.html";
 const CONTENT_TYPE_URL = EXAMPLE_URL + "html_content-type-test-page.html";
 const CONTENT_TYPE_WITHOUT_CACHE_URL = EXAMPLE_URL + "html_content-type-without-cache-test-page.html";
 const CYRILLIC_URL = EXAMPLE_URL + "html_cyrillic-test-page.html";
 const STATUS_CODES_URL = EXAMPLE_URL + "html_status-codes-test-page.html";
 const POST_DATA_URL = EXAMPLE_URL + "html_post-data-test-page.html";
 const POST_RAW_URL = EXAMPLE_URL + "html_post-raw-test-page.html";
+const PARAMS_URL = EXAMPLE_URL + "html_params-test-page.html";
 const JSONP_URL = EXAMPLE_URL + "html_jsonp-test-page.html";
 const JSON_LONG_URL = EXAMPLE_URL + "html_json-long-test-page.html";
 const JSON_MALFORMED_URL = EXAMPLE_URL + "html_json-malformed-test-page.html";
 const JSON_CUSTOM_MIME_URL = EXAMPLE_URL + "html_json-custom-mime-test-page.html";
 const JSON_TEXT_MIME_URL = EXAMPLE_URL + "html_json-text-mime-test-page.html";
 const SORTING_URL = EXAMPLE_URL + "html_sorting-test-page.html";
 const FILTERING_URL = EXAMPLE_URL + "html_filter-test-page.html";
 const INFINITE_GET_URL = EXAMPLE_URL + "html_infinite-get-page.html";
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_params-test-page.html
@@ -0,0 +1,54 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Network Monitor test page</title>
+  </head>
+
+  <body>
+    <p>Request params type test</p>
+
+    <script type="text/javascript">
+      function post(aAddress, aQuery, aContentType, aPostBody) {
+        var xhr = new XMLHttpRequest();
+        xhr.open("POST", aAddress + aQuery, true);
+        xhr.setRequestHeader("content-type", aContentType);
+        xhr.send(aPostBody);
+      }
+
+      function performRequests() {
+        var urlencoded = "application/x-www-form-urlencoded";
+
+        setTimeout(function() {
+          post("baz", "?a", urlencoded, '{ "foo": "bar" }');
+
+          setTimeout(function() {
+            post("baz", "?a=b", urlencoded, '{ "foo": "bar" }');
+
+            setTimeout(function() {
+              post("baz", "?a=b", urlencoded, '?foo=bar');
+
+              setTimeout(function() {
+                post("baz", "?a", undefined, '{ "foo": "bar" }');
+
+                setTimeout(function() {
+                  post("baz", "?a=b", undefined, '{ "foo": "bar" }');
+
+                  setTimeout(function() {
+                    post("baz", "?a=b", undefined, '?foo=bar');
+
+                    // Done.
+                  }, 10);
+                }, 10);
+              }, 10);
+            }, 10);
+          }, 10);
+        }, 10);
+      }
+    </script>
+  </body>
+
+</html>
--- a/browser/devtools/netmonitor/test/html_post-raw-test-page.html
+++ b/browser/devtools/netmonitor/test/html_post-raw-test-page.html
@@ -10,27 +10,28 @@
 
   <body>
     <p>POST raw test</p>
 
     <script type="text/javascript">
       function post(aAddress, aMessage, aCallback) {
         var xhr = new XMLHttpRequest();
         xhr.open("POST", aAddress, true);
+        xhr.setRequestHeader("content-type", "application/x-www-form-urlencoded");
 
         xhr.onreadystatechange = function() {
           if (this.readyState == this.DONE) {
             aCallback();
           }
         };
         xhr.send(aMessage);
       }
 
       function performRequests() {
-        var rawData = "Content-Type: application/x-www-form-urlencoded\r\n\r\nfoo=bar&baz=123";
+        var rawData = "foo=bar&baz=123";
         post("sjs_simple-test-server.sjs", rawData, function() {
           // Done.
         });
       }
     </script>
   </body>
 
 </html>
--- a/browser/devtools/sourceeditor/editor.js
+++ b/browser/devtools/sourceeditor/editor.js
@@ -148,18 +148,18 @@ function Editor(config) {
     styleActiveLine:   true,
     autoCloseBrackets: "()[]{}''\"\"",
     autoCloseEnabled:  useAutoClose,
     theme:             "mozilla"
   };
 
   // Additional shortcuts.
   this.config.extraKeys[Editor.keyFor("jumpToLine")] = () => this.jumpToLine();
-  this.config.extraKeys[Editor.keyFor("moveLineUp")] = () => this.moveLineUp();
-  this.config.extraKeys[Editor.keyFor("moveLineDown")] = () => this.moveLineDown();
+  this.config.extraKeys[Editor.keyFor("moveLineUp", { noaccel: true })] = () => this.moveLineUp();
+  this.config.extraKeys[Editor.keyFor("moveLineDown", { noaccel: true })] = () => this.moveLineDown();
   this.config.extraKeys[Editor.keyFor("toggleComment")] = "toggleComment";
 
   // Disable ctrl-[ and ctrl-] because toolbox uses those shortcuts.
   this.config.extraKeys[Editor.keyFor("indentLess")] = false;
   this.config.extraKeys[Editor.keyFor("indentMore")] = false;
 
   // If alternative keymap is provided, use it.
   if (keyMap === "emacs" || keyMap === "vim")
@@ -839,22 +839,23 @@ CM_MAPPING.forEach(function (name) {
 Editor.accel = function (key, modifiers={}) {
   return (modifiers.shift ? "Shift-" : "") +
          (Services.appinfo.OS == "Darwin" ? "Cmd-" : "Ctrl-") +
          (modifiers.alt ? "Alt-" : "") + key;
 };
 
 /**
  * Returns a string representation of a shortcut for a
- * specified command 'cmd'. Cmd- for macs, Ctrl- for other
- * platforms. Useful when overwriting or disabling default
- * shortcuts.
+ * specified command 'cmd'. Append Cmd- for macs, Ctrl- for other
+ * platforms unless noaccel is specified in the options. Useful when overwriting
+ * or disabling default shortcuts.
  */
-Editor.keyFor = function (cmd) {
-  return Editor.accel(L10N.GetStringFromName(cmd + ".commandkey"));
+Editor.keyFor = function (cmd, opts={ noaccel: false }) {
+  let key = L10N.GetStringFromName(cmd + ".commandkey");
+  return opts.noaccel ? key : Editor.accel(key);
 };
 
 // Since Gecko already provide complete and up to date list of CSS property
 // names, values and color names, we compute them so that they can replace
 // the ones used in CodeMirror while initiating an editor object. This is done
 // here instead of the file codemirror/css.js so as to leave that file untouched
 // and easily upgradable.
 function getCSSKeywords() {
--- a/browser/devtools/styleeditor/test/browser.ini
+++ b/browser/devtools/styleeditor/test/browser.ini
@@ -26,16 +26,17 @@ support-files =
   sourcemaps.css.map
   sourcemaps.scss
   sourcemaps.html
   test_private.css
   test_private.html
 
 [browser_styleeditor_autocomplete.js]
 [browser_styleeditor_bug_740541_iframes.js]
+skip-if = os == "linux" || "mac" # bug 949355
 [browser_styleeditor_bug_851132_middle_click.js]
 [browser_styleeditor_bug_870339.js]
 [browser_styleeditor_cmd_edit.js]
 [browser_styleeditor_enabled.js]
 [browser_styleeditor_filesave.js]
 [browser_styleeditor_import.js]
 [browser_styleeditor_import_rule.js]
 [browser_styleeditor_init.js]
--- a/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd
@@ -63,16 +63,20 @@
 <!-- LOCALIZATION NOTE (debuggerUI.tab.response): This is the label displayed
   -  in the network details pane identifying the response tab. -->
 <!ENTITY netmonitorUI.tab.response        "Response">
 
 <!-- LOCALIZATION NOTE (debuggerUI.tab.timings): This is the label displayed
   -  in the network details pane identifying the timings tab. -->
 <!ENTITY netmonitorUI.tab.timings         "Timings">
 
+<!-- LOCALIZATION NOTE (debuggerUI.tab.preview): This is the label displayed
+  -  in the network details pane identifying the preview tab. -->
+<!ENTITY netmonitorUI.tab.preview         "Preview">
+
 <!-- LOCALIZATION NOTE (debuggerUI.footer.filterAll): This is the label displayed
   -  in the network details footer for the "All" filtering button. -->
 <!ENTITY netmonitorUI.footer.filterAll    "All">
 
 <!-- LOCALIZATION NOTE (debuggerUI.footer.filterHTML): This is the label displayed
   -  in the network details footer for the "HTML" filtering button. -->
 <!ENTITY netmonitorUI.footer.filterHTML   "HTML">
 
--- a/browser/locales/en-US/chrome/browser/devtools/sourceeditor.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/sourceeditor.properties
@@ -44,43 +44,41 @@ annotation.breakpoint.title=Breakpoint: 
 annotation.currentLine=Current line
 
 # LOCALIZATION NOTE  (annotation.debugLocation.title): This is the text shown in
 # a tooltip displayed in any of the editor gutters when the user hovers the
 # current debugger location. The debugger can pause the JavaScript execution at
 # user-defined lines.
 annotation.debugLocation.title=Current step: %S
 
-# LOCALIZATION NOTE  (jumpToLine.commandkey): This the key to use in
+# LOCALIZATION NOTE  (jumpToLine.commandkey): This is the key to use in
 # conjunction with accel (Command on Mac or Ctrl on other platforms) to jump to
 # a specific line in the editor.
 jumpToLine.commandkey=J
 
-# LOCALIZATION NOTE  (toggleComment.commandkey): This the key to use in
+# LOCALIZATION NOTE  (toggleComment.commandkey): This is the key to use in
 # conjunction with accel (Command on Mac or Ctrl on other platforms) to either
 # comment or uncomment selected lines in the editor.
 toggleComment.commandkey=/
 
-# LOCALIZATION NOTE  (toolboxPrevTool.commandkey): This the key to use in
+# LOCALIZATION NOTE  (indentLess.commandkey): This is the key to use in
 # conjunction with accel (Command on Mac or Ctrl on other platforms) to reduce
 # indentation level in CodeMirror. However, its default value also used by
 # the Toolbox to switch between tools so we disable it.
 #
 # DO NOT translate this key without proper synchronization with toolbox.dtd.
 indentLess.commandkey=[
 
-# LOCALIZATION NOTE  (toolboxPrevTool.commandkey): This the key to use in
+# LOCALIZATION NOTE  (indentMore.commandkey): This is the key to use in
 # conjunction with accel (Command on Mac or Ctrl on other platforms) to increase
 # indentation level in CodeMirror. However, its default value also used by
 # the Toolbox to switch between tools
 #
 # DO NOT translate this key without proper synchronization with toolbox.dtd.
 indentMore.commandkey=]
 
-# LOCALIZATION NOTE  (moveLineUp.commandkey): This the key to use in
-# conjunction with accel (Command on Mac or Ctrl on other platforms) to move
+# LOCALIZATION NOTE  (moveLineUp.commandkey): This is the key to use to move
 # the selected lines up.
 moveLineUp.commandkey=Alt-Up
 
-# LOCALIZATION NOTE  (moveLineDown.commandkey): This the key to use in
-# conjunction with accel (Command on Mac or Ctrl on other platforms) to move
+# LOCALIZATION NOTE  (moveLineDown.commandkey): This is the key to use to move
 # the selected lines down.
 moveLineDown.commandkey=Alt-Down
--- a/browser/metro/base/content/bindings/console.xml
+++ b/browser/metro/base/content/bindings/console.xml
@@ -1,61 +1,50 @@
 <?xml version="1.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/. -->
 
-
 <!DOCTYPE bindings [
 <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
 %browserDTD;
 ]>
 
 <bindings
     xmlns="http://www.mozilla.org/xbl"
     xmlns:xbl="http://www.mozilla.org/xbl"
     xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
-
   <binding id="error" extends="chrome://browser/content/bindings/bindings.xml#richlistitem">
     <content orient="vertical">
       <xul:hbox class="console-row-internal-box" flex="1">
         <xul:vbox class="console-row-content" flex="1">
           <xul:hbox class="console-row-msg" align="start">
             <xul:label class="label title" xbl:inherits="value=typetext"/>
             <xul:description class="console-error-msg title" xbl:inherits="xbl:text=msg" flex="1"/>
           </xul:hbox>
-          
           <xul:hbox class="console-row-file" xbl:inherits="hidden=hideSource">
             <xul:label class="label title" value="&consoleErrFile.label;"/>
             <xul:label class="title" xbl:inherits="value=href" crop="right"/>
             <xul:spacer flex="1"/>
             <xul:hbox class="lineNumberRow" xbl:inherits="line">
               <xul:label class="label title" value="&consoleErrLine.label;"/>
               <xul:label class="label title" xbl:inherits="value=line"/>
             </xul:hbox>
           </xul:hbox>
-          
           <xul:vbox class="console-row-code" xbl:inherits="hidden=hideCode">
             <xul:label class="monospace console-code" xbl:inherits="value=code" crop="end"/>
-            <xul:hbox xbl:inherits="hidden=hideCaret">
-              <xul:label class="monospace console-dots title" xbl:inherits="value=errorDots"/>
-              <xul:label class="monospace console-caret title" xbl:inherits="value=errorCaret"/>
-              <xul:spacer flex="1"/>
-            </xul:hbox>
           </xul:vbox>
         </xul:vbox>
       </xul:hbox>
     </content>
   </binding>
-    
   <binding id="message" extends="chrome://browser/content/bindings/bindings.xml#richlistitem">
     <content>
       <xul:hbox class="console-internal-box" flex="1">
         <xul:vbox class="console-row-content" flex="1">
           <xul:vbox class="console-row-msg" flex="1">
             <xul:description class="console-msg-text title" xbl:inherits="xbl:text=msg"/>
           </xul:vbox>
         </xul:vbox>
       </xul:hbox>
     </content>
   </binding>
-  
 </bindings>
--- a/browser/metro/base/content/browser.xul
+++ b/browser/metro/base/content/browser.xul
@@ -583,17 +583,19 @@ Desktop browser's sync prefs.
                        onkeypress="ConsolePanelView.onEvalKeyPress(event)"/>
               <button id="console-button-eval"
                       class="show-text"
                       label="&consoleEvaluate.label;"
                       oncommand="ConsolePanelView.evaluateTypein()"/>
             </hbox>
             <hbox align="center"
                   pack="end">
-              <radiogroup id="console-filter"
+              <checkbox id="console-follow-checkbox" label="&consoleFollowCheckbox.label;" checked="true"/>
+              <spacer flex="1"/>
+              <radiogroup id="console-filter" orient="horizontal"
                           oncommand="ConsolePanelView.changeMode();">
                 <radio id="console-filter-all"
                        value="all"
                        selected="true"
                        label="&consoleAll.label;"/>
                 <radio id="console-filter-messages"
                        value="message"
                        label="&consoleMessages.label;" />
@@ -603,16 +605,20 @@ Desktop browser's sync prefs.
                 <radio id="console-filter-errors"
                        value="error"
                        label="&consoleErrors.label;"/>
               </radiogroup>
               <button id="console-clear"
                       class="show-text"
                       label="&consoleClear.label;"
                       oncommand="ConsolePanelView.clearConsole();"/>
+              <button id="console-copy"
+                      class="show-text"
+                      label="&consoleCopyAll.label;"
+                      oncommand="ConsolePanelView.copyAll();"/>
             </hbox>
           </vbox>
 
           <richlistbox id="console-box"
                        class="panel-list console-box"
                        flex="1"
                        onkeypress="ConsolePanelView.onConsoleBoxKeyPress(event)"
                        oncontextmenu="ConsolePanelView.onContextMenu(event);"/>
--- a/browser/metro/base/content/console.js
+++ b/browser/metro/base/content/console.js
@@ -8,26 +8,35 @@ let ConsolePanelView = {
   _inited: false,
   _evalTextbox: null,
   _evalFrame: null,
   _evalCode: "",
   _bundle: null,
   _showChromeErrors: -1,
   _enabledPref: "devtools.errorconsole.enabled",
 
+  get enabled() {
+    return Services.prefs.getBoolPref(this._enabledPref);
+  },
+
+  get follow() {
+    return document.getElementById("console-follow-checkbox").checked;
+  },
+
   init: function cv_init() {
     if (this._list)
       return;
 
     this._list = document.getElementById("console-box");
     this._evalTextbox = document.getElementById("console-eval-textbox");
     this._bundle = Strings.browser;
 
     this._count = 0;
     this.limit = 250;
+    this.fieldMaxLength = 140;
 
     try {
       // update users using the legacy pref
       if (Services.prefs.getBoolPref("browser.console.showInPanel")) {
         Services.prefs.setBoolPref(this._enabledPref, true);
         Services.prefs.clearUserPref("browser.console.showInPanel");
       }
     } catch(ex) {
@@ -58,20 +67,16 @@ let ConsolePanelView = {
 
   uninit: function cv_uninit() {
     if (this._inited)
       Services.console.unregisterListener(this);
 
     Services.prefs.removeObserver(this._enabledPref, this, false);
   },
 
-  get enabled() {
-    return Services.prefs.getBoolPref(this._enabledPref);
-  },
-
   observe: function(aSubject, aTopic, aData) {
     if (aTopic == "nsPref:changed") {
       // We may choose to create a new menu in v2
       }
     else
       this.appendItem(aSubject);
   },
 
@@ -84,40 +89,56 @@ let ConsolePanelView = {
       return this._showChromeErrors = pref.getBoolPref("javascript.options.showInConsole");
     }
     catch(ex) {
       return this._showChromeErrors = false;
     }
   },
 
   appendItem: function cv_appendItem(aObject) {
+    let index = -1;
     try {
       // Try to QI it to a script error to get more info
       let scriptError = aObject.QueryInterface(Ci.nsIScriptError);
 
       // filter chrome urls
       if (!this.showChromeErrors && scriptError.sourceName.substr(0, 9) == "chrome://")
         return;
-      this.appendError(scriptError);
+      index = this.appendError(scriptError);
     }
     catch (ex) {
       try {
         // Try to QI it to a console message
         let msg = aObject.QueryInterface(Ci.nsIConsoleMessage);
 
         if (msg.message)
-          this.appendMessage(msg.message);
+          index = this.appendMessage(msg.message);
         else // observed a null/"clear" message
           this.clearConsole();
       }
       catch (ex2) {
         // Give up and append the object itself as a string
-        this.appendMessage(aObject);
+        index = this.appendMessage(aObject);
       }
     }
+    if (this.follow) {
+      this._list.ensureIndexIsVisible(index);
+    }
+  },
+
+  truncateIfNecessary: function (aString) {
+    if (!aString || aString.length <= this.fieldMaxLength) {
+      return aString;
+    }
+    let truncatedString = aString.substring(0, this.fieldMaxLength);
+    let Ci = Components.interfaces;
+    let ellipsis = Services.prefs.getComplexValue("intl.ellipsis",
+                                                  Ci.nsIPrefLocalizedString).data;
+    truncatedString = truncatedString + ellipsis;
+    return truncatedString;
   },
 
   appendError: function cv_appendError(aObject) {
     let row = this.createConsoleRow();
     let nsIScriptError = Ci.nsIScriptError;
 
     // Is this error actually just a non-fatal warning?
     let warning = aObject.flags & nsIScriptError.warningFlag != 0;
@@ -129,100 +150,128 @@ let ConsolePanelView = {
     row.setAttribute("category", aObject.category);
     if (aObject.lineNumber || aObject.sourceName) {
       row.setAttribute("href", aObject.sourceName);
       row.setAttribute("line", aObject.lineNumber);
     }
     else {
       row.setAttribute("hideSource", "true");
     }
+    // hide code by default, otherwise initial item display will
+    // hang the browser.
+    row.setAttribute("hideCode", "true");
+    row.setAttribute("hideCaret", "true");
+
     if (aObject.sourceLine) {
-      row.setAttribute("code", aObject.sourceLine.replace(/\s/g, " "));
+      row.setAttribute("code", this.truncateIfNecessary(aObject.sourceLine.replace(/\s/g, " ")));
       if (aObject.columnNumber) {
         row.setAttribute("col", aObject.columnNumber);
-        row.setAttribute("errorDots", this.repeatChar(" ", aObject.columnNumber));
-        row.setAttribute("errorCaret", " ");
       }
-      else {
-        row.setAttribute("hideCaret", "true");
-      }
-    }
-    else {
-      row.setAttribute("hideCode", "true");
     }
 
     let mode = document.getElementById("console-filter").value;
-    if (mode != "all" && mode != row.getAttribute("type"))
+    if (mode != "all" && mode != row.getAttribute("type")) {
       row.collapsed = true;
+    }
 
+    row.setAttribute("onclick", "ConsolePanelView.onRowClick(this)");
     this.appendConsoleRow(row);
+    return this._list.getIndexOfItem(row);
   },
 
   appendMessage: function cv_appendMessage (aMessage) {
     let row = this.createConsoleRow();
     row.setAttribute("type", "message");
     row.setAttribute("msg", aMessage);
 
     let mode = document.getElementById("console-filter").value;
     if (mode != "all" && mode != "message")
       row.collapsed = true;
 
     this.appendConsoleRow(row);
+    return this._list.getIndexOfItem(row);
   },
 
   createConsoleRow: function cv_createConsoleRow() {
     let row = document.createElement("richlistitem");
     row.setAttribute("class", "console-row");
     return row;
   },
 
   appendConsoleRow: function cv_appendConsoleRow(aRow) {
     this._list.appendChild(aRow);
-    if (++this._count > this.limit)
+    if (++this._count > this.limit) {
       this.deleteFirst();
+    }
   },
 
   deleteFirst: function cv_deleteFirst() {
     let node = this._list.firstChild;
     this._list.removeChild(node);
     --this._count;
   },
 
   appendInitialItems: function cv_appendInitialItems() {
+    this._list.collapsed = true;
     let messages = Services.console.getMessageArray();
 
     // In case getMessageArray returns 0-length array as null
     if (!messages)
       messages = [];
 
     let limit = messages.length - this.limit;
     if (limit < 0)
       limit = 0;
 
     // Checks if console ever been cleared
-    for (var i = messages.length - 1; i >= limit; --i)
-      if (!messages[i].message)
+    for (var i = messages.length - 1; i >= limit; --i) {
+      if (!messages[i].message) {
         break;
+      }
+    }
 
     // Populate with messages after latest "clear"
-    while (++i < messages.length)
+    while (++i < messages.length) {
       this.appendItem(messages[i]);
+    }
+    this._list.collapsed = false;
   },
 
   clearConsole: function cv_clearConsole() {
     if (this._count == 0) // already clear
       return;
     this._count = 0;
 
     let newRows = this._list.cloneNode(false);
     this._list.parentNode.replaceChild(newRows, this._list);
     this._list = newRows;
     this.selectedItem = null;
   },
 
+  copyAll: function () {
+    let mode = document.getElementById("console-filter").value;
+    let rows = this._list.childNodes;
+    let copyText = "";
+    for (let i=0; i < rows.length; i++) {
+      let row = rows[i];
+      if (mode == "all" || row.getAttribute ("type") == mode) {
+        let text = "* " + row.getAttribute("msg");
+        if (row.hasAttribute("href")) {
+          text += "\r\n " + row.getAttribute("href") + " line:" + row.getAttribute("line");
+        }
+        if (row.hasAttribute("code")) {
+          text += "\r\n " + row.getAttribute("code") + " col:" + row.getAttribute("col");
+        }
+        copyText += text + "\r\n";
+      }
+    }
+    let clip = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
+    clip.copyString(copyText, document);
+  },
+
   changeMode: function cv_changeMode() {
     let mode = document.getElementById("console-filter").value;
     if (this._list.getAttribute("mode") != mode) {
       let rows = this._list.childNodes;
       for (let i=0; i < rows.length; i++) {
         let row = rows[i];
         if (mode == "all" || row.getAttribute ("type") == mode)
           row.collapsed = false;
@@ -232,29 +281,38 @@ let ConsolePanelView = {
       this._list.mode = mode;
       this._list.scrollToIndex(0);
     }
   },
 
   onContextMenu: function cv_onContextMenu(aEvent) {
     let row = aEvent.target;
     let text = ["msg", "href", "line", "code", "col"].map(function(attr) row.getAttribute(attr))
-               .filter(function(x) x).join("\n");
+               .filter(function(x) x).join("\r\n");
 
     ContextMenuUI.showContextMenu({
       target: row,
       json: {
         types: ["copy"],
         string: text,
         xPos: aEvent.clientX,
         yPos: aEvent.clientY
       }
     });
   },
 
+  onRowClick: function (aRow) {
+    if (aRow.hasAttribute("code")) {
+      aRow.setAttribute("hideCode", "false");
+    }
+    if (aRow.hasAttribute("col")) {
+      aRow.setAttribute("hideCaret", "false");
+    }
+  },
+
   onEvalKeyPress: function cv_onEvalKeyPress(aEvent) {
     if (aEvent.keyCode == 13)
       this.evaluateTypein();
   },
 
   onConsoleBoxKeyPress: function cv_onConsoleBoxKeyPress(aEvent) {
     if ((aEvent.charCode == 99 || aEvent.charCode == 67) && aEvent.ctrlKey && this._list && this._list.selectedItem) {
       let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
--- a/browser/metro/locales/en-US/chrome/browser.dtd
+++ b/browser/metro/locales/en-US/chrome/browser.dtd
@@ -54,20 +54,22 @@
 
 <!ENTITY consoleHeader.label       "Error Console">
 <!ENTITY consoleAll.label          "All">
 <!ENTITY consoleErrors.label       "Errors">
 <!ENTITY consoleWarnings.label     "Warnings">
 <!ENTITY consoleMessages.label     "Messages">
 <!ENTITY consoleCodeEval.label     "Code:">
 <!ENTITY consoleClear.label        "Clear">
+<!ENTITY consoleCopyAll.label      "Copy">
 <!ENTITY consoleEvaluate.label     "…">
 <!ENTITY consoleErrFile.label      "Source File:">
 <!ENTITY consoleErrLine.label      "Line:">
 <!ENTITY consoleErrColumn.label    "Column:">
+<!ENTITY consoleFollowCheckbox.label "Follow">
 
 <!--  TEXT CONTEXT MENU -->
 <!ENTITY contextTextCut.label              "Cut">
 <!ENTITY contextTextCopy.label             "Copy">
 <!ENTITY contextTextPaste.label            "Paste">
 <!-- unique item that is only added to the url bar context menu -->
 <!ENTITY contextTextPasteAndGo.label       "Paste &amp; go">
 <!ENTITY contextTextSelect.label           "Select">
--- a/browser/metro/theme/platform.css
+++ b/browser/metro/theme/platform.css
@@ -917,8 +917,13 @@ appbar toolbar[labelled] toolbarbutton >
   width: 346px;
 }
 
 /* Some elements don't resize to fit their container properly for some reason.
  * Setting max-width on the element or a child fixes it. */
 .flyout-narrow .flyoutpanel-hack {
   max-width: calc(346px - 2 * 40px);
 }
+
+.console-row-code {
+   padding-top: 2px;
+   font-size: small;
+}
\ No newline at end of file
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -1461,16 +1461,21 @@ richlistitem[type~="action"][actiontype=
 }
 
 #BMB_bookmarksPopup[side="left"],
 #BMB_bookmarksPopup[side="right"] {
   margin-top: -16px;
   margin-bottom: -16px;
 }
 
+#nav-bar .toolbarbutton-1 > menupopup[side="top"].cui-widget-panel,
+#nav-bar .toolbarbutton-1 > menupopup[side="bottom"].cui-widget-panel {
+  margin-top: -4px;
+}
+
 /* Bookmarking panel */
 #editBookmarkPanelStarIcon {
   list-style-image: url("chrome://browser/skin/places/starred48.png");
   width: 48px;
   height: 48px;
 }
 
 #editBookmarkPanelStarIcon[unstarred] {
--- a/browser/themes/linux/customizableui/panelUIOverlay.css
+++ b/browser/themes/linux/customizableui/panelUIOverlay.css
@@ -11,8 +11,15 @@
 .widget-overflow-list .toolbarbutton-1 > .toolbarbutton-menubutton-button {
   -moz-appearance: none;
   border: 0;
 }
 
 .widget-overflow-list .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker {
   -moz-margin-start: 0;
 }
+
+.PanelUI-subView toolbarseparator,
+.PanelUI-subView menuseparator,
+.cui-widget-panelview menuseparator,
+#PanelUI-footer-inner > toolbarseparator {
+  -moz-appearance: none !important;
+}
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1267,16 +1267,20 @@ toolbarbutton[sdk-button="true"][cui-are
 .toolbarbutton-1 > .toolbarbutton-menubutton-button > .toolbarbutton-text {
   margin: 2px 0 0;
 }
 
 .toolbarbutton-1 > menupopup {
   margin-top: 1px;
 }
 
+.toolbarbutton-1 > menupopup.cui-widget-panel {
+  margin-top: -5px;
+}
+
 /* Common back and forward button styles */
 
 #back-button,
 #forward-button {
   background: linear-gradient(rgba(255,255,255,0.5),
                               rgba(255,255,255,0.2) 50%,
                               rgba(255,255,255,0.1) 50%,
                               rgba(255,255,255,0.2)) repeat-x;
--- a/browser/themes/osx/customizableui/panelUIOverlay.css
+++ b/browser/themes/osx/customizableui/panelUIOverlay.css
@@ -61,8 +61,13 @@
 /* Override OSX-specific toolkit styles for the bookmarks panel */
 #BMB_bookmarksPopup > menu > .menu-right {
   -moz-margin-end: 0;
 }
 
 .widget-overflow-list .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker {
   -moz-margin-start: 4px;
 }
+
+.PanelUI-subView menuseparator,
+.cui-widget-panelview menuseparator {
+  padding: 0 !important;
+}
--- a/browser/themes/shared/customizableui/panelUIOverlay.inc.css
+++ b/browser/themes/shared/customizableui/panelUIOverlay.inc.css
@@ -49,24 +49,29 @@
 .panel-viewstack[viewtype="main"] > .panel-subviews:-moz-locale-dir(rtl) {
   transform: translateX(-@menuPanelWidth@);
 }
 
 .panel-viewstack:not([viewtype="main"]) > .panel-mainview > #PanelUI-mainView {
   -moz-box-flex: 1;
 }
 
+.subviewbutton:not(:-moz-any([image],[targetURI],.cui-withicon)) > .toolbarbutton-text {
+  -moz-margin-start: 0;
+}
+
 .panel-subview-body {
   overflow-y: auto;
   overflow-x: hidden;
   -moz-box-flex: 1;
 }
 
 #PanelUI-popup .panel-subview-body {
   margin: -4px;
+  padding: 2px 4px;
 }
 
 .panel-subview-header,
 .subviewbutton.panel-subview-footer {
   padding: 12px;
   background-color: hsla(210,4%,10%,.04);
 }
 
@@ -149,17 +154,16 @@
 #PanelUI-contents,
 .panel-mainview:not([panelid="PanelUI-popup"]) {
   max-width: @menuPanelWidth@;
 }
 
 panelview:not([mainview]) .toolbarbutton-text,
 .cui-widget-panel toolbarbutton > .toolbarbutton-text {
   text-align: start;
-  -moz-padding-start: 8px;
   display: -moz-box;
 }
 
 .cui-widget-panel > .panel-arrowcontainer > .panel-arrowcontent {
   padding: 4px 0;
 }
 
 .cui-widget-panel.cui-widget-panelWithFooter > .panel-arrowcontainer > .panel-arrowcontent {
@@ -330,16 +334,17 @@ toolbarpaletteitem[place="palette"] > to
   display: flex;
   box-shadow: 0 -1px 0 rgba(0,0,0,.15);
 }
 
 #PanelUI-footer-inner > toolbarseparator {
   border: 0;
   border-left: 1px solid rgba(0,0,0,0.1);
   margin: 7px 0 7px;
+  -moz-appearance: none;
 }
 
 #PanelUI-footer-inner:hover > toolbarseparator {
   margin: 0;
 }
 
 #PanelUI-help,
 #PanelUI-fxa-status,
@@ -524,22 +529,16 @@ panelview .toolbarbutton-1,
   font-size: 1.1em;
 }
 
 .cui-widget-panelview .subviewbutton:not(.panel-subview-footer) {
   margin-left: 4px;
   margin-right: 4px;
 }
 
-.PanelUI-subView menuseparator,
-.PanelUI-subView toolbarseparator {
-  -moz-margin-start: -5px;
-  -moz-margin-end: -4px;
-}
-
 panelview .toolbarbutton-1,
 .widget-overflow-list .toolbarbutton-1 {
   margin-top: 6px;
 }
 
 panelview .toolbarbutton-1@buttonStateHover@,
 .subviewbutton@buttonStateHover@,
 .widget-overflow-list .toolbarbutton-1@buttonStateHover@,
@@ -575,22 +574,41 @@ panelview .toolbarbutton-1@buttonStateAc
 
 #BMB_bookmarksPopup > .panel-arrowcontainer > .panel-arrowcontent > .popup-internal-box > .autorepeatbutton-up,
 #BMB_bookmarksPopup > .panel-arrowcontainer > .panel-arrowcontent > .popup-internal-box > .autorepeatbutton-down {
   -moz-appearance: none;
   margin-top: 0;
   margin-bottom: 0;
 }
 
-panelview toolbarseparator,
-#BMB_bookmarksPopup > menuseparator {
+.PanelUI-subView menuseparator,
+.PanelUI-subView toolbarseparator,
+.cui-widget-panelview menuseparator {
   -moz-appearance: none;
   min-height: 0;
-  border-top: 1px solid ThreeDShadow;
-  margin: 5px 0;
+  border-top: 1px solid hsla(210,4%,10%,.15);
+  margin: 2px 0;
+  padding: 0;
+}
+
+.PanelUI-subView menuseparator,
+.PanelUI-subView toolbarseparator {
+  -moz-margin-start: -5px;
+  -moz-margin-end: -4px;
+}
+
+.PanelUI-subView menuseparator.small-separator,
+.PanelUI-subView toolbarseparator.small-separator {
+  margin-left: 5px;
+  margin-right: 5px;
+}
+
+.cui-widget-panelview menuseparator.small-separator {
+  margin-left: 10px;
+  margin-right: 10px;
 }
 
 .subviewbutton > .menu-accel-container {
   -moz-box-pack: start;
   -moz-margin-start: 10px;
   -moz-margin-end: auto;
   color: hsl(0,0%,50%);
 }
@@ -777,43 +795,27 @@ toolbarpaletteitem[place="palette"] > #s
   background-image: linear-gradient(hsla(210,54%,20%,.2) 0, hsla(210,54%,20%,.2) 18px);
   background-clip: padding-box;
   background-position: center;
   background-repeat: no-repeat;
   background-size: 1px 18px;
   box-shadow: 0 0 0 1px hsla(0,0%,100%,.2);
 }
 
-#PanelUI-developerItems > toolbarbutton[checked="true"],
-#PanelUI-bookmarks > toolbarbutton[checked="true"],
-#PanelUI-history > toolbarbutton[checked="true"],
-.PanelUI-characterEncodingView-list > toolbarbutton[current] {
+.PanelUI-subView toolbarbutton[checked="true"] {
   -moz-padding-start: 4px;
 }
 
-#PanelUI-developerItems > toolbarbutton[checked="true"] > .toolbarbutton-text,
-#PanelUI-bookmarks > toolbarbutton[checked="true"] > .toolbarbutton-text,
-#PanelUI-history > toolbarbutton[checked="true"] > .toolbarbutton-text,
-.PanelUI-characterEncodingView-list > toolbarbutton[current] > .toolbarbutton-text,
-.cui-widget-panel .PanelUI-characterEncodingView-list > toolbarbutton[current] > .toolbarbutton-text {
+.PanelUI-subView toolbarbutton[checked="true"] > .toolbarbutton-text {
   -moz-padding-start: 0px;
 }
 
-#BMB_bookmarksPopup > menuitem[checked="true"]::before,
-#PanelUI-bookmarks > toolbarbutton[checked="true"]::before,
-#PanelUI-history > toolbarbutton[checked="true"]::before,
-#PanelUI-developerItems > toolbarbutton[checked="true"]::before,
-.PanelUI-characterEncodingView-list > toolbarbutton[current]::before {
+.PanelUI-subView menuitem[checked="true"]::before,
+.PanelUI-subView toolbarbutton[checked="true"]::before {
   content: "✓";
   display: -moz-box;
   width: 12px;
-}
-
-#PanelUI-bookmarks > toolbarbutton[checked="true"]::before,
-#PanelUI-history > toolbarbutton[checked="true"]::before,
-#PanelUI-developerItems > toolbarbutton[checked="true"]::before,
-.PanelUI-characterEncodingView-list > toolbarbutton[current]::before {
-  -moz-margin-end: -2px;
+  margin: 0 2px;
 }
 
 #BMB_bookmarksPopup > menuitem[checked="true"] > .menu-iconic-left {
   display: none;
 }
--- a/browser/themes/shared/devtools/netmonitor.inc.css
+++ b/browser/themes/shared/devtools/netmonitor.inc.css
@@ -453,16 +453,27 @@ box.requests-menu-status {
 }
 
 #response-content-image {
   background: #fff;
   border: 1px dashed GrayText;
   margin-bottom: 10px;
 }
 
+/* Preview tabpanel */
+
+#preview-tabpanel {
+  background: #fff;
+}
+
+#response-preview {
+  display: -moz-box;
+  -moz-box-flex: 1;
+}
+
 /* Timings tabpanel */
 
 #timings-tabpanel .tabpanel-summary-label {
   width: 10em;
 }
 
 #timings-tabpanel .requests-menu-timings-box {
   transition: transform 0.2s ease-out;
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -500,16 +500,20 @@ menuitem.bookmark-item {
   padding-left: 5px;
   padding-right: 5px;
 }
 
 #nav-bar .toolbarbutton-1 > menupopup {
   margin-top: -3px;
 }
 
+#nav-bar .toolbarbutton-1 > menupopup.cui-widget-panel {
+  margin-top: -8px;
+}
+
 #nav-bar .toolbarbutton-1 > .toolbarbutton-menubutton-button {
   -moz-padding-end: 0;
 }
 
 #nav-bar .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker {
   -moz-padding-start: 0;
   -moz-box-align: center;
 }
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -440,17 +440,17 @@ pref("browser.ui.zoom.force-user-scalabl
 // in 1/240-inch pixels:
 pref("browser.ui.touch.left", 32);
 pref("browser.ui.touch.right", 32);
 pref("browser.ui.touch.top", 48);
 pref("browser.ui.touch.bottom", 16);
 pref("browser.ui.touch.weight.visited", 120); // percentage
 
 // The percentage of the screen that needs to be scrolled before margins are exposed.
-pref("browser.ui.show-margins-threshold", 20);
+pref("browser.ui.show-margins-threshold", 10);
 
 // Maximum distance from the point where the user pressed where we still
 // look for text to select
 pref("browser.ui.selection.distance", 250);
 
 // plugins
 pref("plugin.disable", false);
 pref("dom.ipc.plugins.enabled", false);
--- a/mobile/android/base/home/HomeConfig.java
+++ b/mobile/android/base/home/HomeConfig.java
@@ -464,88 +464,156 @@ public final class HomeConfig {
 
             @Override
             public ViewType[] newArray(final int size) {
                 return new ViewType[size];
             }
         };
     }
 
+    public static enum ItemHandler implements Parcelable {
+        BROWSER("browser"),
+        INTENT("intent");
+
+        private final String mId;
+
+        ItemHandler(String id) {
+            mId = id;
+        }
+
+        public static ItemHandler fromId(String id) {
+            if (id == null) {
+                throw new IllegalArgumentException("Could not convert null String to ItemHandler");
+            }
+
+            for (ItemHandler itemHandler : ItemHandler.values()) {
+                if (TextUtils.equals(itemHandler.mId, id.toLowerCase())) {
+                    return itemHandler;
+                }
+            }
+
+            throw new IllegalArgumentException("Could not convert String id to ItemHandler");
+        }
+
+        @Override
+        public String toString() {
+            return mId;
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(ordinal());
+        }
+
+        public static final Creator<ItemHandler> CREATOR = new Creator<ItemHandler>() {
+            @Override
+            public ItemHandler createFromParcel(final Parcel source) {
+                return ItemHandler.values()[source.readInt()];
+            }
+
+            @Override
+            public ItemHandler[] newArray(final int size) {
+                return new ItemHandler[size];
+            }
+        };
+    }
+
     public static class ViewConfig implements Parcelable {
         private final ViewType mType;
         private final String mDatasetId;
+        private final ItemHandler mItemHandler;
 
         private static final String JSON_KEY_TYPE = "type";
         private static final String JSON_KEY_DATASET = "dataset";
+        private static final String JSON_KEY_ITEM_HANDLER = "itemHandler";
 
         public ViewConfig(JSONObject json) throws JSONException, IllegalArgumentException {
             mType = ViewType.fromId(json.getString(JSON_KEY_TYPE));
             mDatasetId = json.getString(JSON_KEY_DATASET);
+            mItemHandler = ItemHandler.fromId(json.getString(JSON_KEY_ITEM_HANDLER));
 
             validate();
         }
 
         @SuppressWarnings("unchecked")
         public ViewConfig(Parcel in) {
             mType = (ViewType) in.readParcelable(getClass().getClassLoader());
             mDatasetId = in.readString();
+            mItemHandler = (ItemHandler) in.readParcelable(getClass().getClassLoader());
 
             validate();
         }
 
         public ViewConfig(ViewConfig viewConfig) {
             mType = viewConfig.mType;
             mDatasetId = viewConfig.mDatasetId;
+            mItemHandler = viewConfig.mItemHandler;
 
             validate();
         }
 
-        public ViewConfig(ViewType type, String datasetId) {
+        public ViewConfig(ViewType type, String datasetId, ItemHandler itemHandler) {
             mType = type;
             mDatasetId = datasetId;
+            mItemHandler = itemHandler;
 
             validate();
         }
 
         private void validate() {
             if (mType == null) {
                 throw new IllegalArgumentException("Can't create ViewConfig with null type");
             }
 
             if (TextUtils.isEmpty(mDatasetId)) {
                 throw new IllegalArgumentException("Can't create ViewConfig with empty dataset ID");
             }
+
+            if (mItemHandler == null) {
+                throw new IllegalArgumentException("Can't create ViewConfig with null item handler");
+            }
         }
 
         public ViewType getType() {
             return mType;
         }
 
         public String getDatasetId() {
             return mDatasetId;
         }
 
+        public ItemHandler getItemHandler() {
+            return mItemHandler;
+        }
+
         public JSONObject toJSON() throws JSONException {
             final JSONObject json = new JSONObject();
 
             json.put(JSON_KEY_TYPE, mType.toString());
             json.put(JSON_KEY_DATASET, mDatasetId);
+            json.put(JSON_KEY_ITEM_HANDLER, mItemHandler.toString());
 
             return json;
         }
 
         @Override
         public int describeContents() {
             return 0;
         }
 
         @Override
         public void writeToParcel(Parcel dest, int flags) {
             dest.writeParcelable(mType, 0);
             dest.writeString(mDatasetId);
+            dest.writeParcelable(mItemHandler, 0);
         }
 
         public static final Creator<ViewConfig> CREATOR = new Creator<ViewConfig>() {
             @Override
             public ViewConfig createFromParcel(final Parcel in) {
                 return new ViewConfig(in);
             }
 
--- a/mobile/android/base/home/HomePager.java
+++ b/mobile/android/base/home/HomePager.java
@@ -9,16 +9,17 @@ import org.mozilla.gecko.R;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.home.HomeAdapter.OnAddPanelListener;
 import org.mozilla.gecko.home.HomeConfig.PanelConfig;
 import org.mozilla.gecko.home.HomeConfig.PanelType;
 import org.mozilla.gecko.util.HardwareUtils;
 
 import android.content.Context;
+import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.os.Bundle;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
 import android.support.v4.app.LoaderManager;
 import android.support.v4.app.LoaderManager.LoaderCallbacks;
 import android.support.v4.content.Loader;
 import android.support.v4.view.ViewPager;
@@ -46,16 +47,19 @@ public class HomePager extends ViewPager
     private final HomeConfig mConfig;
     private ConfigLoaderCallbacks mConfigLoaderCallbacks;
 
     private String mInitialPanelId;
 
     // Whether or not we need to restart the loader when we show the HomePager.
     private boolean mRestartLoader;
 
+    // Cached original ViewPager background.
+    private final Drawable mOriginalBackground;
+
     // This is mostly used by UI tests to easily fetch
     // specific list views at runtime.
     static final String LIST_TAG_HISTORY = "history";
     static final String LIST_TAG_BOOKMARKS = "bookmarks";
     static final String LIST_TAG_READING_LIST = "reading_list";
     static final String LIST_TAG_TOP_SITES = "top_sites";
     static final String LIST_TAG_MOST_RECENT = "most_recent";
     static final String LIST_TAG_LAST_TABS = "last_tabs";
@@ -117,16 +121,18 @@ public class HomePager extends ViewPager
         setOffscreenPageLimit(3);
 
         //  We can call HomePager.requestFocus to steal focus from the URL bar and drop the soft
         //  keyboard. However, if there are no focusable views (e.g. an empty reading list), the
         //  URL bar will be refocused. Therefore, we make the HomePager container focusable to
         //  ensure there is always a focusable view. This would ordinarily be done via an XML
         //  attribute, but it is not working properly.
         setFocusableInTouchMode(true);
+
+        mOriginalBackground = getBackground();
     }
 
     @Override
     public void addView(View child, int index, ViewGroup.LayoutParams params) {
         if (child instanceof Decor) {
             ((ViewPager.LayoutParams) params).isDecor = true;
             mDecor = (Decor) child;
             mTabStrip = child;
@@ -305,19 +311,28 @@ public class HomePager extends ViewPager
             if (!panelConfig.isDisabled()) {
                 enabledPanels.add(panelConfig);
             }
         }
 
         // Update the adapter with the new panel configs
         adapter.update(enabledPanels);
 
-        // Hide the tab strip if the new configuration contains no panels.
         final int count = enabledPanels.size();
-        mTabStrip.setVisibility(count > 0 ? View.VISIBLE : View.INVISIBLE);
+        if (count == 0) {
+            // Set firefox watermark as background.
+            setBackgroundResource(R.drawable.home_pager_empty_state);
+            // Hide the tab strip as there are no panels.
+            mTabStrip.setVisibility(View.INVISIBLE);
+        } else {
+            mTabStrip.setVisibility(View.VISIBLE);
+            // Restore original background.
+            setBackgroundDrawable(mOriginalBackground);
+        }
+
         // Re-install the adapter with the final state
         // in the pager.
         setAdapter(adapter);
 
         // Use the default panel as defined in the HomePager's configuration
         // if the initial panel wasn't explicitly set by the show() caller,
         // or if the initial panel is not found in the adapter.
         final int itemPosition = (mInitialPanelId == null) ? -1 : adapter.getItemPosition(mInitialPanelId);
--- a/mobile/android/base/home/PanelGridView.java
+++ b/mobile/android/base/home/PanelGridView.java
@@ -2,16 +2,17 @@
  * 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.home;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract.HomeItems;
+import org.mozilla.gecko.home.HomeConfig.ItemHandler;
 import org.mozilla.gecko.home.HomeConfig.ViewConfig;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.home.PanelLayout.DatasetBacked;
 import org.mozilla.gecko.home.PanelLayout.PanelView;
 
 import android.content.Context;
 import android.database.Cursor;
 import android.support.v4.widget.CursorAdapter;
@@ -22,21 +23,23 @@ import android.widget.AdapterView;
 import android.widget.GridView;
 
 import java.util.EnumSet;
 
 public class PanelGridView extends GridView
                            implements DatasetBacked, PanelView {
     private static final String LOGTAG = "GeckoPanelGridView";
 
+    private final ViewConfig mViewConfig;
     private final PanelGridViewAdapter mAdapter;
     protected OnUrlOpenListener mUrlOpenListener;
 
     public PanelGridView(Context context, ViewConfig viewConfig) {
         super(context, null, R.attr.panelGridViewStyle);
+        mViewConfig = viewConfig;
         mAdapter = new PanelGridViewAdapter(context);
         setAdapter(mAdapter);
         setOnItemClickListener(new PanelGridItemClickListener());
     }
 
     @Override
     public void onDetachedFromWindow() {
         super.onDetachedFromWindow();
@@ -77,12 +80,17 @@ public class PanelGridView extends GridV
             Cursor cursor = mAdapter.getCursor();
             if (cursor == null || !cursor.moveToPosition(position)) {
                 throw new IllegalStateException("Couldn't move cursor to position " + position);
             }
 
             int urlIndex = cursor.getColumnIndexOrThrow(HomeItems.URL);
             final String url = cursor.getString(urlIndex);
 
-            mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.OPEN_WITH_INTENT));
+            EnumSet<OnUrlOpenListener.Flags> flags = EnumSet.noneOf(OnUrlOpenListener.Flags.class);
+            if (mViewConfig.getItemHandler() == ItemHandler.INTENT) {
+                flags.add(OnUrlOpenListener.Flags.OPEN_WITH_INTENT);
+            }
+
+            mUrlOpenListener.onUrlOpen(url, flags);
         }
     }
 }
--- a/mobile/android/base/home/PanelListView.java
+++ b/mobile/android/base/home/PanelListView.java
@@ -1,16 +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.home;
 
 import org.mozilla.gecko.R;
+import org.mozilla.gecko.home.HomeConfig.ItemHandler;
 import org.mozilla.gecko.home.HomeConfig.ViewConfig;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.home.PanelLayout.DatasetBacked;
 import org.mozilla.gecko.home.PanelLayout.PanelView;
 import org.mozilla.gecko.db.BrowserContract.HomeItems;
 
 import android.content.Context;
 import android.database.Cursor;
@@ -68,12 +69,17 @@ public class PanelListView extends HomeL
             Cursor cursor = mAdapter.getCursor();
             if (cursor == null || !cursor.moveToPosition(position)) {
                 throw new IllegalStateException("Couldn't move cursor to position " + position);
             }
 
             int urlIndex = cursor.getColumnIndexOrThrow(HomeItems.URL);
             final String url = cursor.getString(urlIndex);
 
-            mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.OPEN_WITH_INTENT));
+            EnumSet<OnUrlOpenListener.Flags> flags = EnumSet.noneOf(OnUrlOpenListener.Flags.class);
+            if (mViewConfig.getItemHandler() == ItemHandler.INTENT) {
+                flags.add(OnUrlOpenListener.Flags.OPEN_WITH_INTENT);
+            }
+
+            mUrlOpenListener.onUrlOpen(url, flags);
         }
     }
 }
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e59641f68a4dcb3e9404c68b405394ca17fa9b64
GIT binary patch
literal 5057
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
K{;Ex2`o93aWpaoB
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..18ae78dcd11cbc79310e45f97c04a36abd68fd22
GIT binary patch
literal 3154
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
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e4d44619b9d4d3121a054eda8bd9c31aba14eadb
GIT binary patch
literal 6289
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
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/home_pager_empty_state.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
+            xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+    <item android:maxLevel="0" android:drawable="@android:color/white"/>
+
+    <item>
+      <bitmap android:src="@drawable/icon_home_empty_firefox"
+              android:gravity="center"/>
+    </item>
+
+</layer-list>
\ No newline at end of file
--- a/mobile/android/base/resources/layout-large-land-v11/home_history_list.xml
+++ b/mobile/android/base/resources/layout-large-land-v11/home_history_list.xml
@@ -1,17 +1,17 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
 <merge xmlns:android="http://schemas.android.com/apk/res/android">
 
     <ViewStub android:id="@+id/home_empty_view_stub"
-              android:layout="@layout/home_empty_page"
+              android:layout="@layout/home_empty_panel"
               android:layout_width="fill_parent"
               android:layout_height="fill_parent"/>
 
     <org.mozilla.gecko.home.HomeListView
             android:id="@+id/list"
             style="@style/Widget.Home.HistoryListView"
             android:layout_width="fill_parent"
             android:layout_height="0dp"
--- a/mobile/android/base/resources/layout-xlarge-v11/home_history_list.xml
+++ b/mobile/android/base/resources/layout-xlarge-v11/home_history_list.xml
@@ -1,17 +1,17 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
 <merge xmlns:android="http://schemas.android.com/apk/res/android">
 
     <ViewStub android:id="@+id/home_empty_view_stub"
-              android:layout="@layout/home_empty_page"
+              android:layout="@layout/home_empty_panel"
               android:layout_width="fill_parent"
               android:layout_height="fill_parent"/>
 
     <org.mozilla.gecko.home.HomeListView
             android:id="@+id/list"
             style="@style/Widget.Home.HistoryListView"
             android:layout_width="fill_parent"
             android:layout_height="0dp"
--- a/mobile/android/base/resources/layout/home_bookmarks_panel.xml
+++ b/mobile/android/base/resources/layout/home_bookmarks_panel.xml
@@ -3,17 +3,17 @@
    - 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/. -->
 
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent">
 
     <ViewStub android:id="@+id/home_empty_view_stub"
-              android:layout="@layout/home_empty_page"
+              android:layout="@layout/home_empty_panel"
               android:layout_width="fill_parent"
               android:layout_height="fill_parent"/>
 
     <org.mozilla.gecko.home.BookmarksListView
             android:id="@+id/bookmarks_list"
             android:layout_width="fill_parent"
             android:layout_height="fill_parent"/>
 
rename from mobile/android/base/resources/layout/home_empty_page.xml
rename to mobile/android/base/resources/layout/home_empty_panel.xml
rename from mobile/android/base/resources/layout/home_empty_reading_page.xml
rename to mobile/android/base/resources/layout/home_empty_reading_panel.xml
--- a/mobile/android/base/resources/layout/home_history_list.xml
+++ b/mobile/android/base/resources/layout/home_history_list.xml
@@ -1,17 +1,17 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
 <merge xmlns:android="http://schemas.android.com/apk/res/android">
 
     <ViewStub android:id="@+id/home_empty_view_stub"
-              android:layout="@layout/home_empty_page"
+              android:layout="@layout/home_empty_panel"
               android:layout_width="fill_parent"
               android:layout_height="fill_parent"/>
 
     <TextView android:id="@+id/title"
               style="@style/Widget.Home.HistoryPanelTitle"
               android:visibility="gone"/>
 
     <org.mozilla.gecko.home.HomeListView
--- a/mobile/android/base/resources/layout/home_reading_list_panel.xml
+++ b/mobile/android/base/resources/layout/home_reading_list_panel.xml
@@ -4,17 +4,17 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
               android:layout_width="fill_parent"
               android:layout_height="fill_parent"
               android:orientation="vertical">
 
     <ViewStub android:id="@+id/home_empty_view_stub"
-              android:layout="@layout/home_empty_reading_page"
+              android:layout="@layout/home_empty_reading_panel"
               android:layout_width="fill_parent"
               android:layout_height="fill_parent"/>
 
     <org.mozilla.gecko.home.HomeListView android:id="@+id/list"
                                          style="@style/Widget.ReadingListView"
                                          android:layout_width="fill_parent"
                                          android:layout_height="wrap_content"/>
 </LinearLayout>
--- a/mobile/android/modules/Home.jsm
+++ b/mobile/android/modules/Home.jsm
@@ -164,16 +164,22 @@ let HomePanels = Object.freeze({
   }),
 
   // Valid actions for a panel.
   Action: Object.freeze({
     INSTALL: "install",
     REFRESH: "refresh"
   }),
 
+  // Valid item handlers for a panel view.
+  ItemHandler: Object.freeze({
+    BROWSER: "browser",
+    INTENT: "intent"
+  }),
+
   // Holds the currrent set of registered panels.
   _panels: {},
 
   _panelToJSON : function(panel) {
     return {
       id: panel.id,
       title: panel.title,
       layout: panel.layout,
@@ -220,16 +226,23 @@ let HomePanels = Object.freeze({
       throw "Home.panels: Invalid layout for panel: panel.id = " + panel.id + ", panel.layout =" + panel.layout;
     }
 
     for (let view of panel.views) {
       if (!this._valueExists(this.View, view.type)) {
         throw "Home.panels: Invalid view type: panel.id = " + panel.id + ", view.type = " + view.type;
       }
 
+      if (!view.itemHandler) {
+        // Use BROWSER item handler by default
+        view.itemHandler = this.ItemHandler.BROWSER;
+      } else if (!this._valueExists(this.ItemHandler, view.itemHandler)) {
+        throw "Home.panels: Invalid item handler: panel.id = " + panel.id + ", view.itemHandler = " + view.itemHandler;
+      }
+
       if (!view.dataset) {
         throw "Home.panels: No dataset provided for view: panel.id = " + panel.id + ", view.type = " + view.type;
       }
     }
 
     this._panels[panel.id] = panel;
 
     if (action) {
--- a/services/common/tests/unit/test_hawk.js
+++ b/services/common/tests/unit/test_hawk.js
@@ -1,11 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://services-common/hawk.js");
 
 const SECOND_MS = 1000;
 const MINUTE_MS = SECOND_MS * 60;
 const HOUR_MS = MINUTE_MS * 60;
 
 const TEST_CREDS = {
@@ -15,17 +17,16 @@ const TEST_CREDS = {
 };
 
 initTestLogging("Trace");
 
 add_task(function test_now() {
   let client = new HawkClient("https://example.com");
 
   do_check_true(client.now() - Date.now() < SECOND_MS);
-  run_next_test();
 });
 
 add_task(function test_updateClockOffset() {
   let client = new HawkClient("https://example.com");
 
   let now = new Date();
   let serverDate = now.toUTCString();
 
@@ -36,18 +37,16 @@ add_task(function test_updateClockOffset
 
   // Check that they're close; there will likely be a one-second rounding
   // error, so checking strict equality will likely fail.
   //
   // localtimeOffsetMsec is how many milliseconds to add to the local clock so
   // that it agrees with the server.  We are one hour ahead of the server, so
   // our offset should be -1 hour.
   do_check_true(Math.abs(client.localtimeOffsetMsec + HOUR_MS) <= SECOND_MS);
-
-  run_next_test();
 });
 
 add_task(function test_authenticated_get_request() {
   let message = "{\"msg\": \"Great Success!\"}";
   let method = "GET";
 
   let server = httpd_setup({"/foo": (request, response) => {
       do_check_true(request.hasHeader("Authorization"));
@@ -100,16 +99,17 @@ add_task(function test_credentials_optio
       response.setHeader("Content-Type", "application/json");
       response.bodyOutputStream.write(message, message.length);
     }
   });
 
   let client = new HawkClient(server.baseURI);
   let result = yield client.request("/foo", method); // credentials undefined
   do_check_eq(JSON.parse(result).msg, "you're in the friend zone");
+
   yield deferredStop(server);
 });
 
 add_task(function test_server_error() {
   let message = "Ohai!";
   let method = "GET";
 
   let server = httpd_setup({"/foo": (request, response) => {
@@ -117,16 +117,17 @@ add_task(function test_server_error() {
       response.bodyOutputStream.write(message, message.length);
     }
   });
 
   let client = new HawkClient(server.baseURI);
 
   try {
     yield client.request("/foo", method, TEST_CREDS);
+    do_throw("Expected an error");
   } catch(err) {
     do_check_eq(418, err.code);
     do_check_eq("I am a Teapot", err.message);
   }
 
   yield deferredStop(server);
 });
 
@@ -139,16 +140,17 @@ add_task(function test_server_error_json
       response.bodyOutputStream.write(message, message.length);
     }
   });
 
   let client = new HawkClient(server.baseURI);
 
   try {
     yield client.request("/foo", method, TEST_CREDS);
+    do_throw("Expected an error");
   } catch(err) {
     do_check_eq("Cannot get ye flask.", err.error);
   }
 
   yield deferredStop(server);
 });
 
 add_task(function test_offset_after_request() {
@@ -238,17 +240,16 @@ add_task(function test_2xx_success() {
   let client = new HawkClient(server.baseURI);
 
   let response = yield client.request("/foo", method, credentials);
 
   // Shouldn't be any content in a 202
   do_check_eq(response, "");
 
   yield deferredStop(server);
-
 });
 
 add_task(function test_retry_request_on_fail() {
   let attempts = 0;
   let credentials = {
     id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
     key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
     algorithm: "sha256"
@@ -264,24 +265,26 @@ add_task(function test_retry_request_on_
 
       let delta = getTimestampDelta(request.getHeader("Authorization"));
 
       // First time through, we should have a bad timestamp
       if (attempts === 1) {
         do_check_true(delta > MINUTE_MS);
         let message = "never!!!";
         response.setStatusLine(request.httpVersion, 401, "Unauthorized");
-        return response.bodyOutputStream.write(message, message.length);
+        response.bodyOutputStream.write(message, message.length);
+        return;
       }
 
       // Second time through, timestamp should be corrected by client
       do_check_true(delta < MINUTE_MS);
       let message = "i love you!!!";
       response.setStatusLine(request.httpVersion, 200, "OK");
       response.bodyOutputStream.write(message, message.length);
+      return;
     }
   });
 
   let client = new HawkClient(server.baseURI);
   function getOffset() {
     return client.localtimeOffsetMsec;
   }
 
@@ -345,27 +348,25 @@ add_task(function test_multiple_401_retr
   do_check_eq(attempts, 2);
 
   yield deferredStop(server);
 });
 
 add_task(function test_500_no_retry() {
   // If we get a 500 error, the client should not retry (as it would with a
   // 401)
-  let attempts = 0;
   let credentials = {
     id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
     key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
     algorithm: "sha256"
   };
   let method = "GET";
 
   let server = httpd_setup({
     "/no-shutup": function() {
-      attempts += 1;
       let message = "Cannot get ye flask.";
       response.setStatusLine(request.httpVersion, 500, "Internal server error");
       response.bodyOutputStream.write(message, message.length);
     }
   });
 
   let client = new HawkClient(server.baseURI);
   function getOffset() {
@@ -376,23 +377,22 @@ add_task(function test_500_no_retry() {
   // it could
   client.now = () => {
     return Date.now() - 12 * HOUR_MS;
   };
 
   // Request will 500; no retries
   try {
     yield client.request("/no-shutup", method, credentials);
+    do_throw("Expected an error");
   } catch(err) {
     do_check_eq(err.code, 500);
   }
-  do_check_eq(attempts, 1);
 
   yield deferredStop(server);
-
 });
 
 add_task(function test_401_then_500() {
   // Like test_multiple_401_retry_once, but return a 500 to the
   // second request, ensuring that the promise is properly rejected
   // in client.request.
   let attempts = 0;
   let credentials = {
@@ -412,25 +412,27 @@ add_task(function test_401_then_500() {
       let delta = getTimestampDelta(request.getHeader("Authorization"));
 
       // First time through, we should have a bad timestamp
       // Client will retry
       if (attempts === 1) {
         do_check_true(delta > MINUTE_MS);
         let message = "never!!!";
         response.setStatusLine(request.httpVersion, 401, "Unauthorized");
-        return response.bodyOutputStream.write(message, message.length);
+        response.bodyOutputStream.write(message, message.length);
+        return;
       }
 
       // Second time through, timestamp should be corrected by client
       // And fail on the client
       do_check_true(delta < MINUTE_MS);
       let message = "Cannot get ye flask.";
       response.setStatusLine(request.httpVersion, 500, "Internal server error");
       response.bodyOutputStream.write(message, message.length);
+      return;
     }
   });
 
   let client = new HawkClient(server.baseURI);
   function getOffset() {
     return client.localtimeOffsetMsec;
   }
 
@@ -448,23 +450,22 @@ add_task(function test_401_then_500() {
     do_check_eq(err.code, 500);
   }
   do_check_eq(attempts, 2);
 
   yield deferredStop(server);
 });
 
 add_task(function throw_if_not_json_body() {
-  do_test_pending();
   let client = new HawkClient("https://example.com");
   try {
     yield client.request("/bogus", "GET", {}, "I am not json");
+    do_throw("Expected an error");
   } catch(err) {
     do_check_true(!!err.message);
-    do_test_finished();
   }
 });
 
 // End of tests.
 // Utility functions follow
 
 function getTimestampDelta(authHeader, now=Date.now()) {
   let tsMS = new Date(
--- a/testing/mochitest/b2g-debug.json
+++ b/testing/mochitest/b2g-debug.json
@@ -269,16 +269,18 @@
     "dom/browser-element/mochitest/test_browserElement_inproc_AppFramePermission.html":"",
     "dom/browser-element/mochitest/test_browserElement_inproc_AppWindowNamespace.html":"",
     "dom/browser-element/mochitest/test_browserElement_inproc_Auth.html":"",
     "dom/browser-element/mochitest/test_browserElement_inproc_BrowserWindowNamespace.html":"",
     "dom/browser-element/mochitest/test_browserElement_inproc_CloseApp.html":"",
     "dom/browser-element/mochitest/test_browserElement_inproc_CloseFromOpener.html":"",
     "dom/browser-element/":"",
 
+    "dom/downloads/tests/test_downloads_pause_resume.html":"bug 947167",
+
     "dom/events/test/test_bug226361.xhtml":"",
     "dom/events/test/test_bug238987.html":"",
     "dom/events/test/test_bug409604.html":"",
     "dom/events/test/test_bug457672.html":"",
     "dom/events/test/test_bug574663.html":"",
     "dom/events/test/test_bug607464.html":"",
     "dom/events/test/test_wheel_default_action.html":"",
 
--- a/testing/mochitest/b2g.json
+++ b/testing/mochitest/b2g.json
@@ -257,16 +257,19 @@
     "dom/browser-element/mochitest/test_browserElement_oop_OpenMixedProcess.html":"",
     "dom/browser-element/mochitest/test_browserElement_oop_OpenNamed.html":"",
     "dom/browser-element/mochitest/test_browserElement_oop_OpenWindow.html":"",
     "dom/browser-element/mochitest/test_browserElement_oop_OpenWindowDifferentOrigin.html":"",
     "dom/browser-element/mochitest/test_browserElement_oop_OpenWindowInFrame.html":"",
     "dom/browser-element/mochitest/test_browserElement_oop_OpenWindowRejected.html":"",
     "dom/browser-element/mochitest/test_browserElement_oop_SecurityChange.html":"",
     "dom/browser-element/mochitest/test_browserElement_oop_TargetBlank.html":"",
+
+    "dom/downloads/tests/test_downloads_pause_resume.html":"bug 947167",
+
     "dom/events/test/test_bug226361.xhtml":"",
     "dom/events/test/test_bug238987.html":"",
     "dom/events/test/test_bug409604.html":"",
     "dom/events/test/test_bug457672.html":"",
     "dom/events/test/test_bug574663.html":"",
     "dom/events/test/test_bug607464.html":"",
     "dom/events/test/test_wheel_default_action.html":"",
 
new file mode 100644
--- /dev/null
+++ b/toolkit/content/aboutAbout.js
@@ -0,0 +1,47 @@
+/* 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/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+var gProtocols = [];
+var gContainer;
+window.onload = function () {
+  gContainer = document.getElementById("abouts");
+  findAbouts();
+}
+
+function findAbouts() {
+  var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+  for (var cid in Cc) {
+    var result = cid.match(/@mozilla.org\/network\/protocol\/about;1\?what\=(.*)$/);
+    if (result) {
+      var aboutType = result[1];
+      var contract = "@mozilla.org/network/protocol/about;1?what=" + aboutType;
+      try {
+        var am = Cc[contract].getService(Ci.nsIAboutModule);
+        var uri = ios.newURI("about:"+aboutType, null, null);
+        var flags = am.getURIFlags(uri);
+        if (!(flags & Ci.nsIAboutModule.HIDE_FROM_ABOUTABOUT)) {
+          gProtocols.push(aboutType);
+        }
+      } catch (e) {
+        // getService might have thrown if the component doesn't actually
+        // implement nsIAboutModule
+      }
+    }
+  }
+  gProtocols.sort().forEach(createProtocolListing);
+}
+
+function createProtocolListing(aProtocol) {
+  var uri = "about:" + aProtocol;
+  var li = document.createElement("li");
+  var link = document.createElement("a");
+  var text = document.createTextNode(uri);
+
+  link.href = uri;
+  link.appendChild(text);
+  li.appendChild(link);
+  gContainer.appendChild(li);
+}
--- a/toolkit/content/aboutAbout.xhtml
+++ b/toolkit/content/aboutAbout.xhtml
@@ -9,61 +9,17 @@
 <!-- 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/. -->
 
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
   <title>&aboutAbout.title;</title>
   <link rel="stylesheet" href="chrome://global/skin/about.css" type="text/css"/>
-  <script type="application/javascript"><![CDATA[
-    const Cc = Components.classes;
-    const Ci = Components.interfaces;
-    var gProtocols = [];
-    var gContainer;
-    window.onload = function () {
-      gContainer = document.getElementById("abouts");
-      findAbouts();
-    }
-
-    function findAbouts() {
-      var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
-      for (var cid in Cc) {
-        var result = cid.match(/@mozilla.org\/network\/protocol\/about;1\?what\=(.*)$/);
-        if (result) {
-          var aboutType = result[1];
-          var contract = "@mozilla.org/network/protocol/about;1?what=" + aboutType;
-          try {
-            var am = Cc[contract].getService(Ci.nsIAboutModule);
-            var uri = ios.newURI("about:"+aboutType, null, null);
-            var flags = am.getURIFlags(uri);
-            if (!(flags & Ci.nsIAboutModule.HIDE_FROM_ABOUTABOUT)) {
-              gProtocols.push(aboutType);
-            }
-          } catch (e) {
-            // getService might have thrown if the component doesn't actually
-            // implement nsIAboutModule
-          }
-        }
-      }
-      gProtocols.sort().forEach(createProtocolListing);
-    }
-
-    function createProtocolListing(aProtocol) {
-      var uri = "about:" + aProtocol;
-      var li = document.createElement("li");
-      var link = document.createElement("a");
-      var text = document.createTextNode(uri);
-
-      link.href = uri;
-      link.appendChild(text);
-      li.appendChild(link);
-      gContainer.appendChild(li);
-    }
-  ]]></script>
+  <script type="application/javascript" src="chrome://global/content/aboutAbout.js"></script>
 </head>
 
 <body dir="&locale.dir;">
   <h1>&aboutAbout.title;</h1>
   <p><em>&aboutAbout.note;</em></p>
   <ul id="abouts" class="columns"></ul>
 </body>
 </html>
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -7,16 +7,17 @@ toolkit.jar:
 %  override chrome://global/content/nsTransferable.js chrome://global/content/nsDragAndDrop.js
 *  content/global/license.html                (license.html)
    content/global/XPCNativeWrapper.js         (XPCNativeWrapper.js)
 *  content/global/xul.css                     (xul.css)
    content/global/textbox.css                 (textbox.css)
    content/global/menulist.css                (menulist.css)
    content/global/about.js                    (about.js)
    content/global/about.xhtml                 (about.xhtml)
+   content/global/aboutAbout.js               (aboutAbout.js)
    content/global/aboutAbout.xhtml            (aboutAbout.xhtml)
    content/global/aboutRights.xhtml           (aboutRights.xhtml)
    content/global/aboutRights-unbranded.xhtml (aboutRights-unbranded.xhtml)
    content/global/aboutNetworking.js
    content/global/aboutNetworking.xhtml
    content/global/aboutWebrtc.xhtml
 *  content/global/aboutSupport.js
 *  content/global/aboutSupport.xhtml
--- a/toolkit/devtools/pretty-fast/pretty-fast.js
+++ b/toolkit/devtools/pretty-fast/pretty-fast.js
@@ -459,28 +459,35 @@
   /**
    * Make sure that we put "\n" into the output instead of actual newlines.
    */
   function sanitizeNewlines(str) {
     return str.replace(/\n/g, "\\n");
   }
 
   /**
+   * Make sure that we put "\'" into the single-quoted output instead of raw single quotes.
+   */
+  function sanitizeSingleQuotes(str) {
+    return str.replace(/\'/g, "\\'");
+  }
+
+  /**
    * Add the given token to the pretty printed results.
    *
    * @param Object token
    *        The token to add.
    * @param Function write
    *        The function to write pretty printed code to the result SourceNode.
    * @param Object options
    *        The options object.
    */
   function addToken(token, write, options) {
     if (token.type.type == "string") {
-      write("'" + sanitizeNewlines(token.value) + "'",
+      write("'" + sanitizeSingleQuotes(sanitizeNewlines(token.value)) + "'",
             token.startLoc.line,
             token.startLoc.column);
     } else {
       write(String(token.value != null ? token.value : token.type.type),
             token.startLoc.line,
             token.startLoc.column);
     }
   }
--- a/toolkit/devtools/pretty-fast/tests/unit/test.js
+++ b/toolkit/devtools/pretty-fast/tests/unit/test.js
@@ -174,17 +174,17 @@ var testCases = [
     name: "String with semicolon",
     input: "var foo = ';';\n",
     output: "var foo = ';';\n"
   },
 
   {
     name: "String with quote",
     input: "var foo = \"'\";\n",
-    output: "var foo = '\'';\n"
+    output: "var foo = '\\'';\n"
   },
 
   {
     name: "Function calls",
     input: "var result=func(a,b,c,d);",
     output: "var result = func(a, b, c, d);\n"
   },
 
--- a/toolkit/modules/AsyncShutdown.jsm
+++ b/toolkit/modules/AsyncShutdown.jsm
@@ -118,25 +118,27 @@ function safeGetState(state) {
   }
   try {
     // Evaluate state(), normalize the result into something that we can
     // safely stringify or upload.
     let string = JSON.stringify(state());
     let data = JSON.parse(string);
     // Simplify the rest of the code by ensuring that we can simply
     // concatenate the result to a message.
-    data.toString = function() {
-      return string;
-    };
+    if (data && typeof data == "object") {
+      data.toString = function() {
+        return string;
+      };
+    }
     return data;
   } catch (ex) {
     try {
-      return "Error getting state: " + ex;
+      return "Error getting state: " + ex + " at " + ex.stack;
     } catch (ex2) {
-      return "Could not display error";
+      return "Error getting state but could not display error";
     }
   }
 }
 
 /**
  * Countdown for a given duration, skipping beats if the computer is too busy,
  * sleeping or otherwise unavailable.
  *
--- a/toolkit/modules/Promise.jsm
+++ b/toolkit/modules/Promise.jsm
@@ -287,125 +287,247 @@ Services.obs.addObserver(function observ
 // The following error types are considered programmer errors, which should be
 // reported (possibly redundantly) so as to let programmers fix their code.
 const ERRORS_TO_REPORT = ["EvalError", "RangeError", "ReferenceError", "TypeError"];
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Promise
 
 /**
- * This object provides the public module functions.
+ * The Promise constructor. Creates a new promise given an executor callback.
+ * The executor callback is called with the resolve and reject handlers.
+ *
+ * @param aExecutor
+ *        The callback that will be called with resolve and reject.
  */
-this.Promise = Object.freeze({
-  /**
-   * Creates a new pending promise and provides methods to resolve or reject it.
-   *
-   * @return A new object, containing the new promise in the "promise" property,
-   *         and the methods to change its state in the "resolve" and "reject"
-   *         properties.  See the Deferred documentation for details.
-   */
-  defer: function ()
-  {
-    return new Deferred();
-  },
+this.Promise = function Promise(aExecutor)
+{
+  if (typeof(aExecutor) != "function") {
+    throw new TypeError("Promise constructor must be called with an executor.");
+  }
 
-  /**
-   * Creates a new promise resolved with the specified value, or propagates the
-   * state of an existing promise.
-   *
-   * @param aValue
-   *        If this value is not a promise, including "undefined", it becomes
-   *        the resolution value of the returned promise.  If this value is a
-   *        promise, then the returned promise will eventually assume the same
-   *        state as the provided promise.
-   *
-   * @return A promise that can be pending, resolved, or rejected.
+  /*
+   * Internal status of the promise.  This can be equal to STATUS_PENDING,
+   * STATUS_RESOLVED, or STATUS_REJECTED.
    */
-  resolve: function (aValue)
-  {
-    let promise = new PromiseImpl();
-    PromiseWalker.completePromise(promise, STATUS_RESOLVED, aValue);
-    return promise;
-  },
+  Object.defineProperty(this, N_STATUS, { value: STATUS_PENDING,
+                                          writable: true });
+
+  /*
+   * When the N_STATUS property is STATUS_RESOLVED, this contains the final
+   * resolution value, that cannot be a promise, because resolving with a
+   * promise will cause its state to be eventually propagated instead.  When the
+   * N_STATUS property is STATUS_REJECTED, this contains the final rejection
+   * reason, that could be a promise, even if this is uncommon.
+   */
+  Object.defineProperty(this, N_VALUE, { writable: true });
+
+  /*
+   * Array of Handler objects registered by the "then" method, and not processed
+   * yet.  Handlers are removed when the promise is resolved or rejected.
+   */
+  Object.defineProperty(this, N_HANDLERS, { value: [] });
 
   /**
-   * Creates a new promise rejected with the specified reason.
-   *
-   * @param aReason
-   *        The rejection reason for the returned promise.  Although the reason
-   *        can be "undefined", it is generally an Error object, like in
-   *        exception handling.
-   *
-   * @return A rejected promise.
-   *
-   * @note The aReason argument should not be a promise.  Using a rejected
-   *       promise for the value of aReason would make the rejection reason
-   *       equal to the rejected promise itself, and not its rejection reason.
+   * When the N_STATUS property is STATUS_REJECTED and until there is
+   * a rejection callback, this contains an array
+   * - {string} id An id for use with |PendingErrors|;
+   * - {FinalizationWitness} witness A witness broadcasting |id| on
+   *   notification "promise-finalization-witness".
    */
-  reject: function (aReason)
-  {
-    let promise = new PromiseImpl();
-    PromiseWalker.completePromise(promise, STATUS_REJECTED, aReason);
-    return promise;
-  },
+  Object.defineProperty(this, N_WITNESS, { writable: true });
+
+  Object.seal(this);
+
+  let resolve = PromiseWalker.completePromise
+                             .bind(PromiseWalker, this, STATUS_RESOLVED);
+  let reject = PromiseWalker.completePromise
+                            .bind(PromiseWalker, this, STATUS_REJECTED);
+
+  try {
+    Function.prototype.call.call(aExecutor, this, resolve, reject);
+  } catch (ex) {
+    reject(ex);
+  }
+}
 
-  /**
-   * Returns a promise that is resolved or rejected when all values are
-   * resolved or any is rejected.
-   *
-   * @param aValues
-   *        Array of promises that may be pending, resolved, or rejected. When
-   *        all are resolved or any is rejected, the returned promise will be
-   *        resolved or rejected as well.
-   *
-   * @return A new promise that is fulfilled when all values are resolved or
-   *         that is rejected when any of the values are rejected. Its
-   *         resolution value will be an array of all resolved values in the
-   *         given order, or undefined if aValues is an empty array. The reject
-   *         reason will be forwarded from the first promise in the list of
-   *         given promises to be rejected.
-   */
-  all: function (aValues)
-  {
-    if (!Array.isArray(aValues)) {
-      throw new Error("Promise.all() expects an array of promises or values.");
+/**
+ * Calls one of the provided functions as soon as this promise is either
+ * resolved or rejected.  A new promise is returned, whose state evolves
+ * depending on this promise and the provided callback functions.
+ *
+ * The appropriate callback is always invoked after this method returns, even
+ * if this promise is already resolved or rejected.  You can also call the
+ * "then" method multiple times on the same promise, and the callbacks will be
+ * invoked in the same order as they were registered.
+ *
+ * @param aOnResolve
+ *        If the promise is resolved, this function is invoked with the
+ *        resolution value of the promise as its only argument, and the
+ *        outcome of the function determines the state of the new promise
+ *        returned by the "then" method.  In case this parameter is not a
+ *        function (usually "null"), the new promise returned by the "then"
+ *        method is resolved with the same value as the original promise.
+ *
+ * @param aOnReject
+ *        If the promise is rejected, this function is invoked with the
+ *        rejection reason of the promise as its only argument, and the
+ *        outcome of the function determines the state of the new promise
+ *        returned by the "then" method.  In case this parameter is not a
+ *        function (usually left "undefined"), the new promise returned by the
+ *        "then" method is rejected with the same reason as the original
+ *        promise.
+ *
+ * @return A new promise that is initially pending, then assumes a state that
+ *         depends on the outcome of the invoked callback function:
+ *          - If the callback returns a value that is not a promise, including
+ *            "undefined", the new promise is resolved with this resolution
+ *            value, even if the original promise was rejected.
+ *          - If the callback throws an exception, the new promise is rejected
+ *            with the exception as the rejection reason, even if the original
+ *            promise was resolved.
+ *          - If the callback returns a promise, the new promise will
+ *            eventually assume the same state as the returned promise.
+ *
+ * @note If the aOnResolve callback throws an exception, the aOnReject
+ *       callback is not invoked.  You can register a rejection callback on
+ *       the returned promise instead, to process any exception occurred in
+ *       either of the callbacks registered on this promise.
+ */
+Promise.prototype.then = function (aOnResolve, aOnReject)
+{
+  let handler = new Handler(this, aOnResolve, aOnReject);
+  this[N_HANDLERS].push(handler);
+
+  // Ensure the handler is scheduled for processing if this promise is already
+  // resolved or rejected.
+  if (this[N_STATUS] != STATUS_PENDING) {
+
+    // This promise is not the last in the chain anymore. Remove any watchdog.
+    if (this[N_WITNESS] != null) {
+      let [id, witness] = this[N_WITNESS];
+      this[N_WITNESS] = null;
+      witness.forget();
+      PendingErrors.unregister(id);
     }
 
-    if (!aValues.length) {
-      return Promise.resolve([]);
-    }
+    PromiseWalker.schedulePromise(this);
+  }
+
+  return handler.nextPromise;
+};
+
 
-    let countdown = aValues.length;
-    let deferred = Promise.defer();
-    let resolutionValues = new Array(countdown);
+/**
+ * Creates a new pending promise and provides methods to resolve or reject it.
+ *
+ * @return A new object, containing the new promise in the "promise" property,
+ *         and the methods to change its state in the "resolve" and "reject"
+ *         properties.  See the Deferred documentation for details.
+ */
+Promise.defer = function ()
+{
+  return new Deferred();
+};
 
-    function checkForCompletion(aValue, aIndex) {
-      resolutionValues[aIndex] = aValue;
+/**
+ * Creates a new promise resolved with the specified value, or propagates the
+ * state of an existing promise.
+ *
+ * @param aValue
+ *        If this value is not a promise, including "undefined", it becomes
+ *        the resolution value of the returned promise.  If this value is a
+ *        promise, then the returned promise will eventually assume the same
+ *        state as the provided promise.
+ *
+ * @return A promise that can be pending, resolved, or rejected.
+ */
+Promise.resolve = function (aValue)
+{
+  return new Promise((aResolve) => aResolve(aValue));
+};
 
-      if (--countdown === 0) {
-        deferred.resolve(resolutionValues);
-      }
-    }
+/**
+ * Creates a new promise rejected with the specified reason.
+ *
+ * @param aReason
+ *        The rejection reason for the returned promise.  Although the reason
+ *        can be "undefined", it is generally an Error object, like in
+ *        exception handling.
+ *
+ * @return A rejected promise.
+ *
+ * @note The aReason argument should not be a promise.  Using a rejected
+ *       promise for the value of aReason would make the rejection reason
+ *       equal to the rejected promise itself, and not its rejection reason.
+ */
+Promise.reject = function (aReason)
+{
+  return new Promise((_, aReject) => aReject(aReason));
+};
 
-    for (let i = 0; i < aValues.length; i++) {
-      let index = i;
-      let value = aValues[i];
-      let resolve = val => checkForCompletion(val, index);
+/**
+ * Returns a promise that is resolved or rejected when all values are
+ * resolved or any is rejected.
+ *
+ * @param aValues
+ *        Iterable of promises that may be pending, resolved, or rejected. When
+ *        all are resolved or any is rejected, the returned promise will be
+ *        resolved or rejected as well.
+ *
+ * @return A new promise that is fulfilled when all values are resolved or
+ *         that is rejected when any of the values are rejected. Its
+ *         resolution value will be an array of all resolved values in the
+ *         given order, or undefined if aValues is an empty array. The reject
+ *         reason will be forwarded from the first promise in the list of
+ *         given promises to be rejected.
+ */
+Promise.all = function (aValues)
+{
+  if (aValues == null || typeof(aValues["@@iterator"]) != "function") {
+    throw new Error("Promise.all() expects an iterable.");
+  }
+
+  if (!Array.isArray(aValues)) {
+    aValues = [...aValues];
+  }
+
+  if (!aValues.length) {
+    return Promise.resolve([]);
+  }
 
-      if (value && typeof(value.then) == "function") {
-        value.then(resolve, deferred.reject);
-      } else {
-        // Given value is not a promise, forward it as a resolution value.
-        resolve(value);
-      }
+  let countdown = aValues.length;
+  let deferred = Promise.defer();
+  let resolutionValues = new Array(countdown);
+
+  function checkForCompletion(aValue, aIndex) {
+    resolutionValues[aIndex] = aValue;
+
+    if (--countdown === 0) {
+      deferred.resolve(resolutionValues);
     }
+  }
 
-    return deferred.promise;
-  },
-});
+  for (let i = 0; i < aValues.length; i++) {
+    let index = i;
+    let value = aValues[i];
+    let resolve = val => checkForCompletion(val, index);
+
+    if (value && typeof(value.then) == "function") {
+      value.then(resolve, deferred.reject);
+    } else {
+      // Given value is not a promise, forward it as a resolution value.
+      resolve(value);
+    }
+  }
+
+  return deferred.promise;
+};
+
+Object.freeze(Promise);
 
 ////////////////////////////////////////////////////////////////////////////////
 //// PromiseWalker
 
 /**
  * This singleton object invokes the handlers registered on resolved and
  * rejected promises, ensuring that processing is not recursive and is done in
  * the same order as registration occurred on each promise.
@@ -540,20 +662,20 @@ PromiseWalker.walkerLoop = PromiseWalker
 //// Deferred
 
 /**
  * Returned by "Promise.defer" to provide a new promise along with methods to
  * change its state.
  */
 function Deferred()
 {
-  this.promise = new PromiseImpl();
-  this.resolve = this.resolve.bind(this);
-  this.reject = this.reject.bind(this);
-
+  this.promise = new Promise((aResolve, aReject) => {
+    this.resolve = aResolve;
+    this.reject = aReject;
+  });
   Object.freeze(this);
 }
 
 Deferred.prototype = {
   /**
    * A newly created promise, initially in the pending state.
    */
   promise: null,
@@ -571,19 +693,17 @@ Deferred.prototype = {
    *        the resolution value of the associated promise.  If this value is a
    *        promise, then the associated promise will eventually assume the same
    *        state as the provided promise.
    *
    * @note Calling this method with a pending promise as the aValue argument,
    *       and then calling it again with another value before the promise is
    *       resolved or rejected, has unspecified behavior and should be avoided.
    */
-  resolve: function (aValue) {
-    PromiseWalker.completePromise(this.promise, STATUS_RESOLVED, aValue);
-  },
+  resolve: null,
 
   /**
    * Rejects the associated promise with the specified reason.  If the promise
    * has already been resolved or rejected, this method does nothing.
    *
    * This function is bound to its associated promise when "Promise.defer" is
    * called, and can be called with any value of "this".
    *
@@ -592,144 +712,31 @@ Deferred.prototype = {
    *        reason can be "undefined", it is generally an Error object, like in
    *        exception handling.
    *
    * @note The aReason argument should not generally be a promise.  In fact,
    *       using a rejected promise for the value of aReason would make the
    *       rejection reason equal to the rejected promise itself, not to the
    *       rejection reason of the rejected promise.
    */
-  reject: function (aReason) {
-    PromiseWalker.completePromise(this.promise, STATUS_REJECTED, aReason);
-  },
-};
-
-////////////////////////////////////////////////////////////////////////////////
-//// PromiseImpl
-
-/**
- * The promise object implementation.  This includes the public "then" method,
- * as well as private state properties.
- */
-function PromiseImpl()
-{
-  /*
-   * Internal status of the promise.  This can be equal to STATUS_PENDING,
-   * STATUS_RESOLVED, or STATUS_REJECTED.
-   */
-  Object.defineProperty(this, N_STATUS, { value: STATUS_PENDING,
-                                          writable: true });
-
-  /*
-   * When the N_STATUS property is STATUS_RESOLVED, this contains the final
-   * resolution value, that cannot be a promise, because resolving with a
-   * promise will cause its state to be eventually propagated instead.  When the
-   * N_STATUS property is STATUS_REJECTED, this contains the final rejection
-   * reason, that could be a promise, even if this is uncommon.
-   */
-  Object.defineProperty(this, N_VALUE, { writable: true });
-
-  /*
-   * Array of Handler objects registered by the "then" method, and not processed
-   * yet.  Handlers are removed when the promise is resolved or rejected.
-   */
-  Object.defineProperty(this, N_HANDLERS, { value: [] });
-
-  /**
-   * When the N_STATUS property is STATUS_REJECTED and until there is
-   * a rejection callback, this contains an array
-   * - {string} id An id for use with |PendingErrors|;
-   * - {FinalizationWitness} witness A witness broadcasting |id| on
-   *   notification "promise-finalization-witness".
-   */
-  Object.defineProperty(this, N_WITNESS, { writable: true });
-
-  Object.seal(this);
-}
-
-PromiseImpl.prototype = {
-  /**
-   * Calls one of the provided functions as soon as this promise is either
-   * resolved or rejected.  A new promise is returned, whose state evolves
-   * depending on this promise and the provided callback functions.
-   *
-   * The appropriate callback is always invoked after this method returns, even
-   * if this promise is already resolved or rejected.  You can also call the
-   * "then" method multiple times on the same promise, and the callbacks will be
-   * invoked in the same order as they were registered.
-   *
-   * @param aOnResolve
-   *        If the promise is resolved, this function is invoked with the
-   *        resolution value of the promise as its only argument, and the
-   *        outcome of the function determines the state of the new promise
-   *        returned by the "then" method.  In case this parameter is not a
-   *        function (usually "null"), the new promise returned by the "then"
-   *        method is resolved with the same value as the original promise.
-   *
-   * @param aOnReject
-   *        If the promise is rejected, this function is invoked with the
-   *        rejection reason of the promise as its only argument, and the
-   *        outcome of the function determines the state of the new promise
-   *        returned by the "then" method.  In case this parameter is not a
-   *        function (usually left "undefined"), the new promise returned by the
-   *        "then" method is rejected with the same reason as the original
-   *        promise.
-   *
-   * @return A new promise that is initially pending, then assumes a state that
-   *         depends on the outcome of the invoked callback function:
-   *          - If the callback returns a value that is not a promise, including
-   *            "undefined", the new promise is resolved with this resolution
-   *            value, even if the original promise was rejected.
-   *          - If the callback throws an exception, the new promise is rejected
-   *            with the exception as the rejection reason, even if the original
-   *            promise was resolved.
-   *          - If the callback returns a promise, the new promise will
-   *            eventually assume the same state as the returned promise.
-   *
-   * @note If the aOnResolve callback throws an exception, the aOnReject
-   *       callback is not invoked.  You can register a rejection callback on
-   *       the returned promise instead, to process any exception occurred in
-   *       either of the callbacks registered on this promise.
-   */
-  then: function (aOnResolve, aOnReject)
-  {
-    let handler = new Handler(this, aOnResolve, aOnReject);
-    this[N_HANDLERS].push(handler);
-
-    // Ensure the handler is scheduled for processing if this promise is already
-    // resolved or rejected.
-    if (this[N_STATUS] != STATUS_PENDING) {
-
-      // This promise is not the last in the chain anymore. Remove any watchdog.
-      if (this[N_WITNESS] != null) {
-        let [id, witness] = this[N_WITNESS];
-        this[N_WITNESS] = null;
-        witness.forget();
-        PendingErrors.unregister(id);
-      }
-
-      PromiseWalker.schedulePromise(this);
-    }
-
-    return handler.nextPromise;
-  },
+  reject: null,
 };
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Handler
 
 /**
  * Handler registered on a promise by the "then" function.
  */
 function Handler(aThisPromise, aOnResolve, aOnReject)
 {
   this.thisPromise = aThisPromise;
   this.onResolve = aOnResolve;
   this.onReject = aOnReject;
-  this.nextPromise = new PromiseImpl();
+  this.nextPromise = new Promise(() => {});
 }
 
 Handler.prototype = {
   /**
    * Promise on which the "then" method was called.
    */
   thisPromise: null,
 
--- a/toolkit/modules/tests/xpcshell/test_Promise.js
+++ b/toolkit/modules/tests/xpcshell/test_Promise.js
@@ -657,16 +657,87 @@ tests.push(
         do_check_eq(val2, 2);
         do_check_eq(val3, 3);
       }
     );
 
     return Promise.all([p1, p2]);
   }));
 
+// Test behavior of the Promise constructor.
+tests.push(
+  make_promise_test(function test_constructor(test) {
+    try {
+      new Promise(null);
+      do_check_true(false, "Constructor should fail when not passed a function");
+    } catch (e) {
+      do_check_true(true, "Constructor fails when not passed a function");
+    }
+
+    let executorRan = false;
+    let receiver;
+    let promise = new Promise(
+      function executor(resolve, reject) {
+        executorRan = true;
+        receiver = this;
+        do_check_eq(typeof resolve, "function",
+                    "resolve function should be passed to the executor");
+        do_check_eq(typeof reject, "function",
+                    "reject function should be passed to the executor");
+      }
+    );
+    do_check_instanceof(promise, Promise);
+    do_check_true(executorRan, "Executor should execute synchronously");
+    do_check_eq(receiver, promise, "The promise is the |this| in the executor");
+
+    // resolve a promise from the executor
+    let resolvePromise = new Promise(
+      function executor(resolve) {
+        resolve(1);
+      }
+    ).then(
+      function onResolve(value) {
+        do_check_eq(value, 1, "Executor resolved with correct value");
+      },
+      function onReject() {
+        do_throw("Executor unexpectedly rejected");
+      }
+    );
+
+    // reject a promise from the executor
+    let rejectPromise = new Promise(
+      function executor(_, reject) {
+        reject(1);
+      }
+    ).then(
+      function onResolve() {
+        do_throw("Executor unexpectedly resolved");
+      },
+      function onReject(reason) {
+        do_check_eq(reason, 1, "Executor rejected with correct value");
+      }
+    );
+
+    // throw from the executor, causing a rejection
+    let throwPromise = new Promise(
+      function executor() {
+        throw 1;
+      }
+    ).then(
+      function onResolve() {
+        do_throw("Throwing inside an executor should not resolve the promise");
+      },
+      function onReject(reason) {
+        do_check_eq(reason, 1, "Executor rejected with correct value");
+      }
+    );
+
+    return Promise.all([resolvePromise, rejectPromise, throwPromise]);
+  }));
+
 // Test deadlock in Promise.jsm with nested event loops
 // The scenario being tested is:
 // promise_1.then({
 //   do some work that will asynchronously signal done
 //   start an event loop waiting for the done signal
 // }
 // where the async work uses resolution of a second promise to 
 // trigger the "done" signal. While this would likely work in a