Backed out 3 changesets (bug 1404917)for failing clipboard in devtools/client/netmonitor/src/har/test/browser_net_har_copy_all_as_har.js r=backout on a CLOSED TREE
authorNoemi Erli <nerli@mozilla.com>
Fri, 10 Nov 2017 16:56:35 +0200
changeset 444540 004b34f82c4445d37834306e81f90d82acb82f14
parent 444539 396d357f337f134df9df5c635a064c420d1f035b
child 444541 3c135c119ef48c70730959624519487974199987
push id1618
push userCallek@gmail.com
push dateThu, 11 Jan 2018 17:45:48 +0000
treeherdermozilla-release@882ca853e05a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbackout
bugs1404917
milestone58.0a1
backs out3e7a6e920c6badbaa1b2e08c748d771583e2a47e
7dcfe8d12d6f4914d2eba8e5dcaab5660d3c6e61
3d8a6d24cec9ae98ce8b5b50218c909243d6804b
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Backed out 3 changesets (bug 1404917)for failing clipboard in devtools/client/netmonitor/src/har/test/browser_net_har_copy_all_as_har.js r=backout on a CLOSED TREE Backed out changeset 3e7a6e920c6b (bug 1404917) Backed out changeset 7dcfe8d12d6f (bug 1404917) Backed out changeset 3d8a6d24cec9 (bug 1404917)
devtools/client/netmonitor/src/assets/styles/RequestList.css
devtools/client/netmonitor/src/components/RequestListColumnFile.js
devtools/client/netmonitor/src/components/RequestListContent.js
devtools/client/netmonitor/src/components/RequestListItem.js
devtools/client/netmonitor/src/components/ResponsePanel.js
devtools/client/netmonitor/src/components/TabboxPanel.js
devtools/client/netmonitor/src/connector/chrome/response.js
devtools/client/netmonitor/src/connector/firefox-connector.js
devtools/client/netmonitor/src/connector/firefox-data-provider.js
devtools/client/netmonitor/src/connector/index.js
devtools/client/netmonitor/src/constants.js
devtools/client/netmonitor/src/reducers/requests.js
devtools/client/netmonitor/src/request-list-context-menu.js
devtools/client/netmonitor/src/request-list-tooltip.js
devtools/client/netmonitor/test/browser.ini
devtools/client/netmonitor/test/browser_net_autoscroll.js
devtools/client/netmonitor/test/browser_net_brotli.js
devtools/client/netmonitor/test/browser_net_clear.js
devtools/client/netmonitor/test/browser_net_content-type.js
devtools/client/netmonitor/test/browser_net_headers-alignment.js
devtools/client/netmonitor/test/browser_net_icon-preview.js
devtools/client/netmonitor/test/browser_net_image-tooltip.js
devtools/client/netmonitor/test/browser_net_resend_cors.js
devtools/client/netmonitor/test/browser_net_security-error.js
devtools/client/netmonitor/test/browser_net_security-state.js
devtools/client/netmonitor/test/browser_net_security-tab-visibility.js
devtools/client/netmonitor/test/browser_net_simple-request-data.js
devtools/client/netmonitor/test/browser_net_streaming-response.js
devtools/client/netmonitor/test/browser_net_thumbnail-click.js
devtools/client/netmonitor/test/head.js
devtools/client/netmonitor/test/html_infinite-get-page.html
devtools/client/netmonitor/test/shared-head.js
devtools/client/styleeditor/test/browser.ini
devtools/client/styleeditor/test/browser_styleeditor_fetch-from-cache.js
devtools/client/webconsole/new-console-output/components/message-types/NetworkEventMessage.js
devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
devtools/client/webconsole/new-console-output/store.js
devtools/client/webconsole/new-console-output/test/mochitest/browser_netmonitor_shows_reqs_in_webconsole.js
devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_network_attach.js
devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_network_messages_openinnet.js
devtools/client/webconsole/new-console-output/test/mochitest/head.js
devtools/client/webconsole/test/browser_netmonitor_shows_reqs_in_webconsole.js
devtools/client/webconsole/test/browser_webconsole_netlogging_panel.js
devtools/client/webconsole/test/browser_webconsole_netlogging_reset_filter.js
--- a/devtools/client/netmonitor/src/assets/styles/RequestList.css
+++ b/devtools/client/netmonitor/src/assets/styles/RequestList.css
@@ -296,16 +296,25 @@
 .requests-list-file {
   width: 22%;
 }
 
 .requests-list-file.requests-list-column {
   text-align: start;
 }
 
+.requests-list-icon {
+  background: transparent;
+  width: 15px;
+  height: 15px;
+  margin: 0 4px;
+  outline: 1px solid var(--table-splitter-color);
+  vertical-align: top;
+}
+
 /* Protocol column */
 
 .requests-list-protocol {
   width: 8%;
 }
 
 /* Cookies column */
 
--- a/devtools/client/netmonitor/src/components/RequestListColumnFile.js
+++ b/devtools/client/netmonitor/src/components/RequestListColumnFile.js
@@ -6,42 +6,50 @@
 
 const {
   Component,
   DOM,
   PropTypes,
 } = require("devtools/client/shared/vendor/react");
 const { propertiesEqual } = require("../utils/request-utils");
 
-const { div } = DOM;
+const { div, img } = DOM;
 
 const UPDATED_FILE_PROPS = [
+  "responseContentDataUri",
   "urlDetails",
 ];
 
 class RequestListColumnFile extends Component {
   static get propTypes() {
     return {
       item: PropTypes.object.isRequired,
+      onThumbnailMouseDown: PropTypes.func.isRequired,
     };
   }
 
   shouldComponentUpdate(nextProps) {
     return !propertiesEqual(UPDATED_FILE_PROPS, this.props.item, nextProps.item);
   }
 
   render() {
     let {
-      item: { urlDetails },
+      item: { responseContentDataUri, urlDetails },
+      onThumbnailMouseDown
     } = this.props;
 
     return (
       div({
         className: "requests-list-column requests-list-file",
         title: urlDetails.unicodeUrl,
       },
+        img({
+          className: "requests-list-icon",
+          src: responseContentDataUri,
+          onMouseDown: onThumbnailMouseDown,
+        }),
         urlDetails.baseNameWithQuery
       )
     );
   }
 }
 
 module.exports = RequestListColumnFile;
--- a/devtools/client/netmonitor/src/components/RequestListContent.js
+++ b/devtools/client/netmonitor/src/components/RequestListContent.js
@@ -42,16 +42,17 @@ class RequestListContent extends Compone
       dispatch: PropTypes.func.isRequired,
       displayedRequests: PropTypes.object.isRequired,
       firstRequestStartedMillis: PropTypes.number.isRequired,
       fromCache: PropTypes.bool,
       onCauseBadgeMouseDown: PropTypes.func.isRequired,
       onItemMouseDown: PropTypes.func.isRequired,
       onSecurityIconMouseDown: PropTypes.func.isRequired,
       onSelectDelta: PropTypes.func.isRequired,
+      onThumbnailMouseDown: PropTypes.func.isRequired,
       onWaterfallMouseDown: PropTypes.func.isRequired,
       scale: PropTypes.number,
       selectedRequestId: PropTypes.string,
     };
   }
 
   constructor(props) {
     super(props);
@@ -65,17 +66,16 @@ class RequestListContent extends Compone
 
   componentWillMount() {
     const { dispatch, connector } = this.props;
     this.contextMenu = new RequestListContextMenu({
       cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
       getTabTarget: connector.getTabTarget,
       getLongString: connector.getLongString,
       openStatistics: (open) => dispatch(Actions.openStatistics(connector, open)),
-      requestData: connector.requestData,
     });
     this.tooltip = new HTMLTooltip(window.parent.document, { type: "arrow" });
   }
 
   componentDidMount() {
     // Install event handler for displaying a tooltip
     this.tooltip.startTogglingOnHover(this.refs.contentEl, this.onHover, {
       toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY,
@@ -143,17 +143,17 @@ class RequestListContent extends Compone
       return false;
     }
     let requestItem = this.props.displayedRequests.find(r => r.id == itemId);
     if (!requestItem) {
       return false;
     }
 
     let { connector } = this.props;
-    if (target.closest(".requests-list-file")) {
+    if (requestItem.responseContent && target.closest(".requests-list-icon")) {
       return setTooltipImageContent(connector, tooltip, itemEl, requestItem);
     }
 
     return false;
   }
 
   /**
    * Scroll listener for the requests menu view.
@@ -216,16 +216,17 @@ class RequestListContent extends Compone
   render() {
     const {
       columns,
       displayedRequests,
       firstRequestStartedMillis,
       onCauseBadgeMouseDown,
       onItemMouseDown,
       onSecurityIconMouseDown,
+      onThumbnailMouseDown,
       onWaterfallMouseDown,
       scale,
       selectedRequestId,
     } = this.props;
 
     return (
       div({ className: "requests-list-wrapper"},
         div({ className: "requests-list-table"},
@@ -245,16 +246,17 @@ class RequestListContent extends Compone
               index,
               isSelected: item.id === selectedRequestId,
               key: item.id,
               onContextMenu: this.onContextMenu,
               onFocusedNodeChange: this.onFocusedNodeChange,
               onMouseDown: () => onItemMouseDown(item.id),
               onCauseBadgeMouseDown: () => onCauseBadgeMouseDown(item.cause),
               onSecurityIconMouseDown: () => onSecurityIconMouseDown(item.securityState),
+              onThumbnailMouseDown: () => onThumbnailMouseDown(),
               onWaterfallMouseDown: () => onWaterfallMouseDown(),
             }))
           )
         )
       )
     );
   }
 }
@@ -284,15 +286,22 @@ module.exports = connect(
      */
     onSecurityIconMouseDown: (securityState) => {
       if (securityState && securityState !== "insecure") {
         dispatch(Actions.selectDetailsPanelTab("security"));
       }
     },
     onSelectDelta: (delta) => dispatch(Actions.selectDelta(delta)),
     /**
+     * A handler that opens the response tab in the details view if
+     * the thumbnail is clicked.
+     */
+    onThumbnailMouseDown: () => {
+      dispatch(Actions.selectDetailsPanelTab("response"));
+    },
+    /**
      * A handler that opens the timing sidebar panel if the waterfall is clicked.
      */
     onWaterfallMouseDown: () => {
       dispatch(Actions.selectDetailsPanelTab("timings"));
     },
   }),
 )(RequestListContent);
--- a/devtools/client/netmonitor/src/components/RequestListItem.js
+++ b/devtools/client/netmonitor/src/components/RequestListItem.js
@@ -82,16 +82,17 @@ class RequestListItem extends Component 
       isSelected: PropTypes.bool.isRequired,
       firstRequestStartedMillis: PropTypes.number.isRequired,
       fromCache: PropTypes.bool,
       onCauseBadgeMouseDown: PropTypes.func.isRequired,
       onContextMenu: PropTypes.func.isRequired,
       onFocusedNodeChange: PropTypes.func,
       onMouseDown: PropTypes.func.isRequired,
       onSecurityIconMouseDown: PropTypes.func.isRequired,
+      onThumbnailMouseDown: PropTypes.func.isRequired,
       onWaterfallMouseDown: PropTypes.func.isRequired,
       waterfallWidth: PropTypes.number,
     };
   }
 
   componentDidMount() {
     if (this.props.isSelected) {
       this.refs.listItem.focus();
@@ -120,16 +121,17 @@ class RequestListItem extends Component 
       index,
       isSelected,
       firstRequestStartedMillis,
       fromCache,
       onContextMenu,
       onMouseDown,
       onCauseBadgeMouseDown,
       onSecurityIconMouseDown,
+      onThumbnailMouseDown,
       onWaterfallMouseDown,
     } = this.props;
 
     let classList = ["request-list-item", index % 2 ? "odd" : "even"];
     isSelected && classList.push("selected");
     fromCache && classList.push("fromCache");
 
     return (
@@ -138,17 +140,17 @@ class RequestListItem extends Component 
         className: classList.join(" "),
         "data-id": item.id,
         tabIndex: 0,
         onContextMenu,
         onMouseDown,
       },
         columns.get("status") && RequestListColumnStatus({ item }),
         columns.get("method") && RequestListColumnMethod({ item }),
-        columns.get("file") && RequestListColumnFile({ item }),
+        columns.get("file") && RequestListColumnFile({ item, onThumbnailMouseDown }),
         columns.get("protocol") && RequestListColumnProtocol({ item }),
         columns.get("scheme") && RequestListColumnScheme({ item }),
         columns.get("domain") && RequestListColumnDomain({ item,
                                                            onSecurityIconMouseDown }),
         columns.get("remoteip") && RequestListColumnRemoteIP({ item }),
         columns.get("cause") && RequestListColumnCause({ item, onCauseBadgeMouseDown }),
         columns.get("type") && RequestListColumnType({ item }),
         columns.get("cookies") && RequestListColumnCookies({ item }),
--- a/devtools/client/netmonitor/src/components/ResponsePanel.js
+++ b/devtools/client/netmonitor/src/components/ResponsePanel.js
@@ -34,17 +34,16 @@ const JSON_VIEW_MIME_TYPE = "application
  * Response panel component
  * Displays the GET parameters and POST data of a request
  */
 class ResponsePanel extends Component {
   static get propTypes() {
     return {
       request: PropTypes.object.isRequired,
       openLink: PropTypes.func,
-      connector: PropTypes.object.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.state = {
       imageDimensions: {
@@ -52,46 +51,16 @@ class ResponsePanel extends Component {
         height: 0,
       },
     };
 
     this.updateImageDimemsions = this.updateImageDimemsions.bind(this);
     this.isJSON = this.isJSON.bind(this);
   }
 
-  /**
-   * `componentDidMount` is called when opening the ResponsePanel for the first time
-   */
-  componentDidMount() {
-    this.maybeFetchResponseContent(this.props);
-  }
-
-  /**
-   * `componentWillReceiveProps` is the only method called when switching between two
-   * requests while the response panel is displayed.
-   */
-  componentWillReceiveProps(nextProps) {
-    this.maybeFetchResponseContent(nextProps);
-  }
-
-  /**
-   * When switching to another request, lazily fetch response content
-   * from the backend. The Response Panel will first be empty and then
-   * display the content.
-   */
-  maybeFetchResponseContent(props) {
-    if (props.request.responseContentAvailable &&
-        (!props.request.responseContent ||
-         !props.request.responseContent.content)) {
-      // This method will set `props.request.responseContent.content`
-      // asynchronously and force another render.
-      props.connector.requestData(props.request.id, "responseContent");
-    }
-  }
-
   updateImageDimemsions({ target }) {
     this.setState({
       imageDimensions: {
         width: target.naturalWidth,
         height: target.naturalHeight,
       },
     });
   }
--- a/devtools/client/netmonitor/src/components/TabboxPanel.js
+++ b/devtools/client/netmonitor/src/components/TabboxPanel.js
@@ -73,17 +73,17 @@ function TabboxPanel({
         title: PARAMS_TITLE,
       },
         ParamsPanel({ connector, openLink, request }),
       ),
       TabPanel({
         id: PANELS.RESPONSE,
         title: RESPONSE_TITLE,
       },
-        ResponsePanel({ request, openLink, connector }),
+        ResponsePanel({ request, openLink }),
       ),
       TabPanel({
         id: PANELS.TIMINGS,
         title: TIMINGS_TITLE,
       },
         TimingsPanel({ request }),
       ),
       request.cause && request.cause.stacktrace && request.cause.stacktrace.length > 0 &&
--- a/devtools/client/netmonitor/src/connector/chrome/response.js
+++ b/devtools/client/netmonitor/src/connector/chrome/response.js
@@ -1,14 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+const { formDataURI } = require("../../utils/request-utils");
+
 function ResponseInfo(id, response, content) {
   let {
     mimeType
   } = response;
   const {body, base64Encoded} = content;
   return {
     from: id,
     content: {
@@ -16,26 +18,29 @@ function ResponseInfo(id, response, cont
       text: !body ? "" : body,
       size: !body ? 0 : body.length,
       encoding: base64Encoded ? "base64" : undefined
     }
   };
 }
 
 function ResponseContent(id, response, content) {
-  const {body} = content;
+  const {body, base64Encoded} = content;
   let {mimeType, encodedDataLength} = response;
   let responseContent = ResponseInfo(id, response, content);
   let payload = Object.assign(
     {
       responseContent,
       contentSize: !body ? 0 : body.length,
       transferredSize: encodedDataLength, // TODO: verify
       mimeType: mimeType
     }, body);
+  if (mimeType.includes("image/")) {
+    payload.responseContentDataUri = formDataURI(mimeType, base64Encoded, response);
+  }
   return payload;
 }
 
 /**
  * Not support on current version.
  * unstable method: Security
  * cause: https://chromedevtools.github.io/devtools-protocol/tot/Security/
  */
--- a/devtools/client/netmonitor/src/connector/firefox-connector.js
+++ b/devtools/client/netmonitor/src/connector/firefox-connector.js
@@ -17,17 +17,16 @@ class FirefoxConnector {
     this.willNavigate = this.willNavigate.bind(this);
     this.displayCachedEvents = this.displayCachedEvents.bind(this);
     this.onDocLoadingMarker = this.onDocLoadingMarker.bind(this);
     this.sendHTTPRequest = this.sendHTTPRequest.bind(this);
     this.setPreferences = this.setPreferences.bind(this);
     this.triggerActivity = this.triggerActivity.bind(this);
     this.getTabTarget = this.getTabTarget.bind(this);
     this.viewSourceInDebugger = this.viewSourceInDebugger.bind(this);
-    this.requestData = this.requestData.bind(this);
 
     // Internals
     this.getLongString = this.getLongString.bind(this);
     this.getNetworkRequest = this.getNetworkRequest.bind(this);
   }
 
   async connect(connection, actions, getState) {
     this.actions = actions;
@@ -281,15 +280,11 @@ class FirefoxConnector {
    * @param {string} sourceURL source url
    * @param {number} sourceLine source line number
    */
   viewSourceInDebugger(sourceURL, sourceLine) {
     if (this.toolbox) {
       this.toolbox.viewSourceInDebugger(sourceURL, sourceLine);
     }
   }
-
-  requestData(request, type) {
-    return this.dataProvider.requestData(request, type);
-  }
 }
 
 module.exports = new FirefoxConnector();
--- a/devtools/client/netmonitor/src/connector/firefox-data-provider.js
+++ b/devtools/client/netmonitor/src/connector/firefox-data-provider.js
@@ -4,16 +4,17 @@
 /* eslint-disable block-scoped-var */
 
 "use strict";
 
 const { EVENTS } = require("../constants");
 const { CurlUtils } = require("devtools/client/shared/curl");
 const {
   fetchHeaders,
+  formDataURI,
 } = require("../utils/request-utils");
 
 /**
  * This object is responsible for fetching additional HTTP
  * data from the backend over RDP protocol.
  *
  * The object also keeps track of RDP requests in-progress,
  * so it's possible to determine whether all has been fetched
@@ -24,20 +25,16 @@ class FirefoxDataProvider {
     // Options
     this.webConsoleClient = webConsoleClient;
     this.actions = actions;
 
     // Internal properties
     this.payloadQueue = [];
     this.rdpRequestMap = new Map();
 
-    // Map[key string => Promise] used by `requestData` to prevent requesting the same
-    // request data twice.
-    this.lazyRequestData = new Map();
-
     // Fetching data from the backend
     this.getLongString = this.getLongString.bind(this);
 
     // Event handlers
     this.onNetworkEvent = this.onNetworkEvent.bind(this);
     this.onNetworkEventUpdate = this.onNetworkEventUpdate.bind(this);
   }
 
@@ -115,25 +112,28 @@ class FirefoxDataProvider {
       requestHeadersObj,
       responseHeadersObj,
       postDataObj,
       requestCookiesObj,
       responseCookiesObj
     );
 
     this.pushRequestToQueue(id, payload);
-
-    return payload;
   }
 
   async fetchResponseContent(mimeType, responseContent) {
     let payload = {};
     if (mimeType && responseContent && responseContent.content) {
-      let { text } = responseContent.content;
+      let { encoding, text } = responseContent.content;
       let response = await this.getLongString(text);
+
+      if (mimeType.includes("image/")) {
+        payload.responseContentDataUri = formDataURI(mimeType, encoding, response);
+      }
+
       responseContent.content.text = response;
       payload.responseContent = responseContent;
     }
     return payload;
   }
 
   async fetchRequestHeaders(requestHeaders) {
     let payload = {};
@@ -238,34 +238,20 @@ class FirefoxDataProvider {
    * @return {boolean} return whether a specific networkEvent has been updated completely.
    */
   isRequestPayloadReady(id) {
     let record = this.rdpRequestMap.get(id);
     if (!record) {
       return false;
     }
 
-    let { payload } = this.getRequestFromQueue(id);
-
-    // The payload is ready when all values in the record are true. (i.e. all data
-    // received, but the lazy one. responseContent is the only one for now).
-    // Note that we never fetch response header/cookies for request with security issues.
-    // (Be careful, securityState can be undefined, for example for WebSocket requests)
-    // Also note that service worker don't have security info set.
-    // Bug 1404917 should simplify this heuristic by making all these field be lazily
-    // fetched, only on-demand.
-    return record.requestHeaders &&
-      record.requestCookies &&
-      record.eventTimings &&
-      (record.securityInfo || record.fromServiceWorker) &&
-      (
-       (record.responseHeaders && record.responseCookies) ||
-       payload.securityState == "broken" ||
-       (payload.responseContentAvailable && !payload.status)
-      );
+    // The payload is ready when all values in the record are true.
+    // (i.e. all data received).
+    let props = Object.getOwnPropertyNames(record);
+    return props.every(prop => record[prop] === true);
   }
 
   /**
    * Merge upcoming networkEventUpdate payload into existing one.
    *
    * @param {string} id request id
    * @param {object} payload request data payload
    */
@@ -328,24 +314,18 @@ class FirefoxDataProvider {
       },
       startedDateTime,
     } = networkInfo;
 
     // Create tracking record for this request.
     this.rdpRequestMap.set(actor, {
       requestHeaders: false,
       requestCookies: false,
-      responseHeaders: false,
-      responseCookies: false,
-      securityInfo: false,
       eventTimings: false,
-
-      // This isn't a request data, but we need to know about request being served from
-      // service worker later, from isRequestPayloadReady.
-      fromServiceWorker,
+      responseContent: false,
     });
 
     this.addRequest(actor, {
       cause,
       fromCache,
       fromServiceWorker,
       isXHR,
       method,
@@ -363,204 +343,177 @@ class FirefoxDataProvider {
    * @param {object} packet the message received from the server.
    * @param {object} networkInfo the network request information.
    */
   onNetworkEventUpdate(type, data) {
     let { packet, networkInfo } = data;
     let { actor } = networkInfo;
     let { updateType } = packet;
 
-    // When we pause and resume, we may receive `networkEventUpdate` for a request
-    // that started during the pause and we missed its `networkEvent`.
-    if (!this.rdpRequestMap.has(actor)) {
-      return;
-    }
-
     switch (updateType) {
       case "requestHeaders":
+        this.requestData(actor, updateType).then(response => {
+          this.onRequestHeaders(response)
+            .then(() => this.onDataReceived(actor, updateType));
+          emit(EVENTS.UPDATING_REQUEST_HEADERS, actor);
+        });
+        break;
       case "requestCookies":
+        this.requestData(actor, updateType).then(response => {
+          this.onRequestCookies(response)
+            .then(() => this.onDataReceived(actor, updateType));
+          emit(EVENTS.UPDATING_REQUEST_COOKIES, actor);
+        });
+        break;
       case "requestPostData":
-      case "responseHeaders":
-      case "responseCookies":
-        this.requestPayloadData(actor, updateType);
+        this.requestData(actor, updateType).then(response => {
+          this.onRequestPostData(response)
+            .then(() => this.onDataReceived(actor, updateType));
+          emit(EVENTS.UPDATING_REQUEST_POST_DATA, actor);
+        });
         break;
       case "securityInfo":
         this.updateRequest(actor, {
           securityState: networkInfo.securityInfo,
         }).then(() => {
-          this.requestPayloadData(actor, updateType);
+          this.requestData(actor, updateType).then(response => {
+            this.onSecurityInfo(response)
+              .then(() => this.onDataReceived(actor, updateType));
+            emit(EVENTS.UPDATING_SECURITY_INFO, actor);
+          });
+        });
+        break;
+      case "responseHeaders":
+        this.requestData(actor, updateType).then(response => {
+          this.onResponseHeaders(response)
+            .then(() => this.onDataReceived(actor, updateType));
+          emit(EVENTS.UPDATING_RESPONSE_HEADERS, actor);
+        });
+        break;
+      case "responseCookies":
+        this.requestData(actor, updateType).then(response => {
+          this.onResponseCookies(response)
+            .then(() => this.onDataReceived(actor, updateType));
+          emit(EVENTS.UPDATING_RESPONSE_COOKIES, actor);
         });
         break;
       case "responseStart":
         this.updateRequest(actor, {
           httpVersion: networkInfo.response.httpVersion,
           remoteAddress: networkInfo.response.remoteAddress,
           remotePort: networkInfo.response.remotePort,
           status: networkInfo.response.status,
           statusText: networkInfo.response.statusText,
           headersSize: networkInfo.response.headersSize
         }).then(() => {
           emit(EVENTS.STARTED_RECEIVING_RESPONSE, actor);
         });
         break;
       case "responseContent":
-        this.updateRequest(actor, {
-          contentSize: networkInfo.response.bodySize,
-          transferredSize: networkInfo.response.transferredSize,
-          mimeType: networkInfo.response.content.mimeType,
-
-          // This field helps knowing when/if responseContent property is available
-          // and can be requested via `requestData`
-          responseContentAvailable: true,
+        this.requestData(actor, updateType).then(response => {
+          this.onResponseContent({
+            contentSize: networkInfo.response.bodySize,
+            transferredSize: networkInfo.response.transferredSize,
+            mimeType: networkInfo.response.content.mimeType
+          }, response).then(() => this.onDataReceived(actor, updateType));
+          emit(EVENTS.UPDATING_RESPONSE_CONTENT, actor);
         });
         break;
       case "eventTimings":
         this.updateRequest(actor, { totalTime: networkInfo.totalTime })
           .then(() => {
-            this.requestPayloadData(actor, updateType);
+            this.requestData(actor, updateType).then(response => {
+              this.onEventTimings(response)
+                .then(() => this.onDataReceived(actor, updateType));
+              emit(EVENTS.UPDATING_EVENT_TIMINGS, actor);
+            });
           });
         break;
     }
 
     emit(EVENTS.NETWORK_EVENT_UPDATED, actor);
   }
 
   /**
-   * Wrapper method for requesting HTTP details data for the payload.
-   *
-   * It is specific to all requests done from `onNetworkEventUpdate`, for data that are
-   * immediately fetched whenever the data is available.
+   * Wrapper method for requesting HTTP details data from the backend.
    *
-   * All these requests are cached into `rdpRequestMap`. All requests related to a given
-   * actor will be collected in the same record.
+   * It collects all RDP requests and monitors responses, so it's
+   * possible to determine whether (and when) all requested data
+   * has been fetched from the backend.
    *
-   * Once bug 1404917 is completed, we should no longer use this method.
-   * All request fields should be loaded only on-demand, via `requestData` method.
+   * It also nicely returns a promise.
    *
    * @param {string} actor actor id (used as request id)
    * @param {string} method identifier of the data we want to fetch
+   *
+   * @return {Promise} return a promise resolved when data are received.
    */
-  requestPayloadData(actor, method) {
+  requestData(actor, method) {
     let record = this.rdpRequestMap.get(actor);
 
-    // If data has been already requested, do nothing.
-    if (record[method]) {
-      return;
+    // All RDP requests related to the given actor will be collected
+    // in the same record.
+    if (!record) {
+      record = {};
+    }
+
+    // If data has been already requested return the same promise.
+    if (record.method) {
+      return record.method;
     }
 
-    let promise = this._requestData(actor, method);
-    promise.then(() => {
-      // Once we got the data toggle the Map item to `true` in order to
-      // make isRequestPayloadReady return `true` once all the data is fetched.
-      record[method] = true;
-      this.onPayloadDataReceived(actor, method, !record);
+    // Calculate real name of the client getter.
+    let realMethodName = "get" + method.charAt(0).toUpperCase() +
+      method.slice(1);
+
+    // Request data from the backend.
+    let promise = new Promise((resolve, reject) => {
+      if (typeof this.webConsoleClient[realMethodName] == "function") {
+        this.webConsoleClient[realMethodName](actor, response => {
+          // Resolve incoming HTTP details data-promise.
+          resolve(response);
+        });
+      } else {
+        reject(new Error("Error: No such client method!"));
+      }
     });
+
+    // Store the promise in order to know about RDP requests
+    // in progress.
+    record[method] = promise;
+
+    return promise;
   }
 
   /**
    * Executed when new data are received from the backend.
    */
-  async onPayloadDataReceived(actor, type) {
-    // Notify actions when all the sync request from onNetworkEventUpdate are done,
-    // or, everytime requestData is called for fetching data lazily.
+  async onDataReceived(actor, type) {
+    let record = this.rdpRequestMap.get(actor);
+    if (record) {
+      record[type] = true;
+    }
+
     if (this.isRequestPayloadReady(actor)) {
       let payloadFromQueue = this.getRequestFromQueue(actor).payload;
 
       // Clean up
       this.cleanUpQueue(actor);
       this.rdpRequestMap.delete(actor);
 
       let { updateRequest } = this.actions;
       if (updateRequest) {
         await updateRequest(actor, payloadFromQueue, true);
       }
 
-      // This event is fired only once per request, once all the properties are fetched
-      // from `onNetworkEventUpdate`. There should be no more RDP requests after this.
       emit(EVENTS.PAYLOAD_READY, actor);
     }
   }
 
   /**
-   * Public connector API to lazily request HTTP details from the backend.
-   *
-   * This is internal method that focus on:
-   * - calling the right actor method,
-   * - emitting an event to tell we start fetching some request data,
-   * - call data processing method.
-   *
-   * @param {string} actor actor id (used as request id)
-   * @param {string} method identifier of the data we want to fetch
-   *
-   * @return {Promise} return a promise resolved when data is received.
-   */
-  requestData(actor, method) {
-    // Key string used in `lazyRequestData`. We use this Map to prevent requesting
-    // the same data twice at the same time.
-    let key = actor + "-" + method;
-    let promise = this.lazyRequestData.get(key);
-    // If a request is pending, reuse it.
-    if (promise) {
-      return promise;
-    }
-    // Fetch the data
-    promise = this._requestData(actor, method);
-    this.lazyRequestData.set(key, promise);
-    promise.then(async () => {
-      // Remove the request from the cache, any new call to requestData will fetch the
-      // data again.
-      this.lazyRequestData.delete(key, promise);
-
-      let payloadFromQueue = this.getRequestFromQueue(actor).payload;
-      let { updateRequest } = this.actions;
-      if (updateRequest) {
-        await updateRequest(actor, payloadFromQueue, true);
-      }
-    });
-    return promise;
-  }
-
-  /**
-   * Internal helper used to request HTTP details from the backend.
-   *
-   * This is internal method that focus on:
-   * - calling the right actor method,
-   * - emitting an event to tell we start fetching some request data,
-   * - call data processing method.
-   *
-   * @param {string} actor actor id (used as request id)
-   * @param {string} method identifier of the data we want to fetch
-   *
-   * @return {Promise} return a promise resolved when data is received.
-   */
-  async _requestData(actor, method) {
-    // Calculate real name of the client getter.
-    let clientMethodName = "get" + method.charAt(0).toUpperCase() +
-      method.slice(1);
-    // The name of the callback that processes request response
-    let callbackMethodName = "on" + method.charAt(0).toUpperCase() +
-      method.slice(1);
-    // And the event to fire before updating this data
-    let updatingEventName = "UPDATING_" + method.replace(/([A-Z])/g, "_$1").toUpperCase();
-
-    if (typeof this.webConsoleClient[clientMethodName] == "function") {
-      // Emit event that tell we just start fetching some data
-      emit(EVENTS[updatingEventName], actor);
-
-      // Do a RDP request to fetch data from the actor.
-      let response = await this.webConsoleClient[clientMethodName](actor);
-
-      // Call data processing method.
-      response = await this[callbackMethodName](response);
-      return response;
-    }
-    throw new Error("Error: No such client method '" + clientMethodName + "'!");
-  }
-
-  /**
    * Handles additional information received for a "requestHeaders" packet.
    *
    * @param {object} response the message received from the server.
    */
   onRequestHeaders(response) {
     return this.updateRequest(response.from, {
       requestHeaders: response
     }).then(() => {
@@ -629,30 +582,26 @@ class FirefoxDataProvider {
     return this.updateRequest(response.from, {
       responseCookies: response
     }).then(() => {
       emit(EVENTS.RECEIVED_RESPONSE_COOKIES, response.from);
     });
   }
 
   /**
-   * Handles additional information received via "getResponseContent" request.
+   * Handles additional information received for a "responseContent" packet.
    *
+   * @param {object} data the message received from the server event.
    * @param {object} response the message received from the server.
    */
-  async onResponseContent(response) {
-    let payload = await this.updateRequest(response.from, {
-      // We have to ensure passing mimeType as fetchResponseContent needs it from
-      // updateRequest. It will convert the LongString in `response.content.text` to a
-      // string.
-      mimeType: response.content.mimeType,
-      responseContent: response,
+  onResponseContent(data, response) {
+    let payload = Object.assign({ responseContent: response }, data);
+    return this.updateRequest(response.from, payload).then(() => {
+      emit(EVENTS.RECEIVED_RESPONSE_CONTENT, response.from);
     });
-    emit(EVENTS.RECEIVED_RESPONSE_CONTENT, response.from);
-    return payload.responseContent;
   }
 
   /**
    * Handles additional information received for a "eventTimings" packet.
    *
    * @param {object} response the message received from the server.
    */
   onEventTimings(response) {
--- a/devtools/client/netmonitor/src/connector/index.js
+++ b/devtools/client/netmonitor/src/connector/index.js
@@ -20,17 +20,16 @@ class Connector {
     this.connectFirefox = this.connectFirefox.bind(this);
     this.getLongString = this.getLongString.bind(this);
     this.getNetworkRequest = this.getNetworkRequest.bind(this);
     this.getTabTarget = this.getTabTarget.bind(this);
     this.sendHTTPRequest = this.sendHTTPRequest.bind(this);
     this.setPreferences = this.setPreferences.bind(this);
     this.triggerActivity = this.triggerActivity.bind(this);
     this.viewSourceInDebugger = this.viewSourceInDebugger.bind(this);
-    this.requestData = this.requestData.bind(this);
   }
 
   // Connect/Disconnect API
 
   connect(connection, actions, getState) {
     if (!connection || !connection.tab) {
       return;
     }
@@ -94,15 +93,11 @@ class Connector {
 
   triggerActivity() {
     return this.connector.triggerActivity(...arguments);
   }
 
   viewSourceInDebugger() {
     return this.connector.viewSourceInDebugger(...arguments);
   }
-
-  requestData() {
-    return this.connector.requestData(...arguments);
-  }
 }
 
 module.exports.Connector = Connector;
--- a/devtools/client/netmonitor/src/constants.js
+++ b/devtools/client/netmonitor/src/constants.js
@@ -119,17 +119,16 @@ const UPDATE_PROPS = [
   "customQueryValue",
   "requestHeaders",
   "requestHeadersFromUploadStream",
   "requestCookies",
   "requestPostData",
   "responseHeaders",
   "responseCookies",
   "responseContent",
-  "responseContentAvailable",
   "responseContentDataUri",
   "formDataSections",
 ];
 
 const PANELS = {
   COOKIES: "cookies",
   HEADERS: "headers",
   PARAMS: "params",
--- a/devtools/client/netmonitor/src/reducers/requests.js
+++ b/devtools/client/netmonitor/src/reducers/requests.js
@@ -53,17 +53,17 @@ const Request = I.Record({
   customQueryValue: undefined,
   requestHeaders: undefined,
   requestHeadersFromUploadStream: undefined,
   requestCookies: undefined,
   requestPostData: undefined,
   responseHeaders: undefined,
   responseCookies: undefined,
   responseContent: undefined,
-  responseContentAvailable: false,
+  responseContentDataUri: undefined,
   formDataSections: undefined,
 });
 
 const Requests = I.Record({
   // The collection of requests (keyed by id)
   requests: I.Map(),
   // Selection state
   selectedId: null,
--- a/devtools/client/netmonitor/src/request-list-context-menu.js
+++ b/devtools/client/netmonitor/src/request-list-context-menu.js
@@ -22,23 +22,21 @@ const {
   getUrlBaseName,
 } = require("./utils/request-utils");
 
 function RequestListContextMenu({
   cloneSelectedRequest,
   getLongString,
   getTabTarget,
   openStatistics,
-  requestData,
 }) {
   this.cloneSelectedRequest = cloneSelectedRequest;
   this.getLongString = getLongString;
   this.getTabTarget = getTabTarget;
   this.openStatistics = openStatistics;
-  this.requestData = requestData;
 }
 
 RequestListContextMenu.prototype = {
   get selectedRequest() {
     // FIXME: Bug 1336382 - Implement RequestListContextMenu React component
     // Remove window.store
     return getSelectedRequest(window.store.getState());
   },
@@ -111,27 +109,30 @@ RequestListContextMenu.prototype = {
       visible: !!(selectedRequest && selectedRequest.responseHeaders),
       click: () => this.copyResponseHeaders(),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-response",
       label: L10N.getStr("netmonitor.context.copyResponse"),
       accesskey: L10N.getStr("netmonitor.context.copyResponse.accesskey"),
-      visible: !!(selectedRequest && selectedRequest.responseContentAvailable),
+      visible: !!(selectedRequest &&
+               selectedRequest.responseContent &&
+               selectedRequest.responseContent.content.text &&
+               selectedRequest.responseContent.content.text.length !== 0),
       click: () => this.copyResponse(),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-image-as-data-uri",
       label: L10N.getStr("netmonitor.context.copyImageAsDataUri"),
       accesskey: L10N.getStr("netmonitor.context.copyImageAsDataUri.accesskey"),
       visible: !!(selectedRequest &&
-               selectedRequest.mimeType &&
-               selectedRequest.mimeType.includes("image/")),
+               selectedRequest.responseContent &&
+               selectedRequest.responseContent.content.mimeType.includes("image/")),
       click: () => this.copyImageAsDataUri(),
     });
 
     copySubmenu.push({
       type: "separator",
       visible: !!selectedRequest,
     });
 
@@ -158,18 +159,18 @@ RequestListContextMenu.prototype = {
       click: () => this.saveAllAsHar(),
     });
 
     menu.push({
       id: "request-list-context-save-image-as",
       label: L10N.getStr("netmonitor.context.saveImageAs"),
       accesskey: L10N.getStr("netmonitor.context.saveImageAs.accesskey"),
       visible: !!(selectedRequest &&
-               selectedRequest.mimeType &&
-               selectedRequest.mimeType.includes("image/")),
+               selectedRequest.responseContent &&
+               selectedRequest.responseContent.content.mimeType.includes("image/")),
       click: () => this.saveImageAs(),
     });
 
     menu.push({
       type: "separator",
       visible: !!(selectedRequest && !selectedRequest.isCustom),
     });
 
@@ -194,29 +195,29 @@ RequestListContextMenu.prototype = {
       click: () => this.openRequestInTab()
     });
 
     menu.push({
       id: "request-list-context-open-in-debugger",
       label: L10N.getStr("netmonitor.context.openInDebugger"),
       accesskey: L10N.getStr("netmonitor.context.openInDebugger.accesskey"),
       visible: !!(selectedRequest &&
-               selectedRequest.mimeType &&
-               selectedRequest.mimeType.includes("javascript")),
+               selectedRequest.responseContent &&
+               selectedRequest.responseContent.content.mimeType.includes("javascript")),
       click: () => this.openInDebugger()
     });
 
     menu.push({
       id: "request-list-context-open-in-style-editor",
       label: L10N.getStr("netmonitor.context.openInStyleEditor"),
       accesskey: L10N.getStr("netmonitor.context.openInStyleEditor.accesskey"),
       visible: !!(selectedRequest &&
+               selectedRequest.responseContent &&
                Services.prefs.getBoolPref("devtools.styleeditor.enabled") &&
-               selectedRequest.mimeType &&
-               selectedRequest.mimeType.includes("css")),
+               selectedRequest.responseContent.content.mimeType.includes("css")),
       click: () => this.openInStyleEditor()
     });
 
     menu.push({
       id: "request-list-context-perf",
       label: L10N.getStr("netmonitor.context.perfTools"),
       accesskey: L10N.getStr("netmonitor.context.perfTools.accesskey"),
       visible: this.sortedRequests.size > 0,
@@ -332,28 +333,25 @@ RequestListContextMenu.prototype = {
       rawHeaders = rawHeaders.replace(/\r/g, "");
     }
     copyString(rawHeaders);
   },
 
   /**
    * Copy image as data uri.
    */
-  async copyImageAsDataUri() {
-    await this.requestData(this.selectedRequest.id, "responseContent");
+  copyImageAsDataUri() {
     copyString(this.selectedRequest.responseContentDataUri);
   },
 
   /**
    * Save image as.
    */
-  async saveImageAs() {
-    let responseContent = await this.requestData(this.selectedRequest.id,
-      "responseContent");
-    let { encoding, text } = responseContent.content;
+  saveImageAs() {
+    let { encoding, text } = this.selectedRequest.responseContent.content;
     let fileName = getUrlBaseName(this.selectedRequest.url);
     let data;
     if (encoding === "base64") {
       let decoded = atob(text);
       data = new Uint8Array(decoded.length);
       for (let i = 0; i < decoded.length; ++i) {
         data[i] = decoded.charCodeAt(i);
       }
@@ -361,20 +359,18 @@ RequestListContextMenu.prototype = {
       data = text;
     }
     saveAs(new Blob([data]), fileName, document);
   },
 
   /**
    * Copy response data as a string.
    */
-  async copyResponse() {
-    let responseContent = await this.requestData(this.selectedRequest.id,
-      "responseContent");
-    copyString(responseContent.content.text);
+  copyResponse() {
+    copyString(this.selectedRequest.responseContent.content.text);
   },
 
   /**
    * Copy HAR from the network panel content to the clipboard.
    */
   copyAllAsHar() {
     return HarExporter.copy(this.getDefaultHarOptions());
   },
--- a/devtools/client/netmonitor/src/request-list-tooltip.js
+++ b/devtools/client/netmonitor/src/request-list-tooltip.js
@@ -8,28 +8,27 @@ const {
   setImageTooltip,
   getImageDimensions,
 } = require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
 const { formDataURI } = require("./utils/request-utils");
 
 const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400; // px
 
 async function setTooltipImageContent(connector, tooltip, itemEl, requestItem) {
-  let { mimeType } = requestItem;
+  let { mimeType, text, encoding } = requestItem.responseContent.content;
 
   if (!mimeType || !mimeType.includes("image/")) {
     return false;
   }
 
-  let responseContent = await connector.requestData(requestItem.id, "responseContent");
-  let { encoding, text } = responseContent.content;
-  let src = formDataURI(mimeType, encoding, text);
+  let string = await connector.getLongString(text);
+  let src = formDataURI(mimeType, encoding, string);
   let maxDim = REQUESTS_TOOLTIP_IMAGE_MAX_DIM;
   let { naturalWidth, naturalHeight } = await getImageDimensions(tooltip.doc, src);
   let options = { maxDim, naturalWidth, naturalHeight };
   setImageTooltip(tooltip, tooltip.doc, src, options);
 
-  return itemEl.querySelector(".requests-list-file");
+  return itemEl.querySelector(".requests-list-icon");
 }
 
 module.exports = {
   setTooltipImageContent,
 };
--- a/devtools/client/netmonitor/test/browser.ini
+++ b/devtools/client/netmonitor/test/browser.ini
@@ -1,15 +1,14 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
   dropmarker.svg
   head.js
-  shared-head.js
   html_cause-test-page.html
   html_content-type-without-cache-test-page.html
   html_brotli-test-page.html
   html_image-tooltip-test-page.html
   html_cors-test-page.html
   html_custom-get-page.html
   html_cyrillic-test-page.html
   html_frame-test-page.html
@@ -117,16 +116,17 @@ skip-if = (os == 'linux' && debug && bit
 [browser_net_filter-02.js]
 [browser_net_filter-03.js]
 [browser_net_filter-04.js]
 [browser_net_filter-autocomplete.js]
 [browser_net_filter-flags.js]
 [browser_net_footer-summary.js]
 [browser_net_headers-alignment.js]
 [browser_net_headers_sorted.js]
+[browser_net_icon-preview.js]
 [browser_net_image-tooltip.js]
 [browser_net_json-b64.js]
 [browser_net_json-null.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]
@@ -170,13 +170,14 @@ skip-if = true # Bug 1258809
 [browser_net_simple-request.js]
 [browser_net_sort-01.js]
 [browser_net_sort-02.js]
 [browser_net_statistics-01.js]
 [browser_net_statistics-02.js]
 [browser_net_status-codes.js]
 [browser_net_streaming-response.js]
 [browser_net_throttle.js]
+[browser_net_thumbnail-click.js]
 [browser_net_timeline_ticks.js]
 skip-if = true # TODO: fix the test
 [browser_net_timing-division.js]
 [browser_net_truncate.js]
 [browser_net_waterfall-click.js]
--- a/devtools/client/netmonitor/test/browser_net_autoscroll.js
+++ b/devtools/client/netmonitor/test/browser_net_autoscroll.js
@@ -5,17 +5,17 @@
 
 /**
  * Bug 863102 - Automatically scroll down upon new network requests.
  * edited to account for changes made to fix Bug 1360457
  */
 add_task(function* () {
   requestLongerTimeout(4);
 
-  let { tab, monitor } = yield initNetMonitor(INFINITE_GET_URL, true);
+  let { monitor } = yield initNetMonitor(INFINITE_GET_URL, true);
   let { document, windowRequire, store } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
 
   store.dispatch(Actions.batchEnable(false));
 
   // Wait until the first request makes the empty notice disappear
   yield waitForRequestListToAppear();
 
@@ -52,21 +52,16 @@ add_task(function* () {
   // from just below the headers.
   store.dispatch(Actions.selectRequestByIndex(0));
   yield waitForNetworkEvents(monitor, 8);
   yield waitSomeTime();
   let requestsContainerHeaders = requestsContainer.firstChild;
   let headersHeight = requestsContainerHeaders.offsetHeight;
   is(requestsContainer.scrollTop, headersHeight, "Did not scroll.");
 
-  // Stop doing requests.
-  yield ContentTask.spawn(tab.linkedBrowser, {}, function () {
-    content.wrappedJSObject.stopRequests();
-  });
-
   // Done: clean up.
   return teardown(monitor);
 
   function waitForRequestListToAppear() {
     info("Waiting until the empty notice disappears and is replaced with the list");
     return waitUntil(() => !!document.querySelector(".requests-list-contents"));
   }
 
--- a/devtools/client/netmonitor/test/browser_net_brotli.js
+++ b/devtools/client/netmonitor/test/browser_net_brotli.js
@@ -46,23 +46,21 @@ add_task(function* () {
       type: "plain",
       fullMimeType: "text/plain",
       transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 60),
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 64),
       time: true
     });
 
   wait = waitForDOM(document, ".CodeMirror-code");
-  let onResponseContent = monitor.panelWin.once(EVENTS.RECEIVED_RESPONSE_CONTENT);
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelector(".network-details-panel-toggle"));
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelector("#response-tab"));
   yield wait;
-  yield onResponseContent;
   yield testResponse("br");
   yield teardown(monitor);
 
   function* testResponse(type) {
     switch (type) {
       case "br": {
         is(document.querySelector(".CodeMirror-line").textContent, "X".repeat(64),
           "The text shown in the source editor is incorrect for the brotli request.");
--- a/devtools/client/netmonitor/test/browser_net_clear.js
+++ b/devtools/client/netmonitor/test/browser_net_clear.js
@@ -8,39 +8,40 @@
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
   let { document, store, windowRequire } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
+  let { EVENTS } = windowRequire("devtools/client/netmonitor/src/constants");
   let detailsPanelToggleButton = document.querySelector(".network-details-panel-toggle");
   let clearButton = document.querySelector(".requests-list-clear-button");
 
   store.dispatch(Actions.batchEnable(false));
 
   // Make sure we start in a sane state
   assertNoRequestState();
 
   // Load one request and assert it shows up in the list
-  let onMonitorUpdated = waitForAllRequestsFinished(monitor);
+  let networkEvent = monitor.panelWin.once(EVENTS.NETWORK_EVENT);
   tab.linkedBrowser.reload();
-  yield onMonitorUpdated;
+  yield networkEvent;
 
   assertSingleRequestState();
 
   // Click clear and make sure the requests are gone
   EventUtils.sendMouseEvent({ type: "click" }, clearButton);
   assertNoRequestState();
 
   // Load a second request and make sure they still show up
-  onMonitorUpdated = waitForAllRequestsFinished(monitor);
+  networkEvent = monitor.panelWin.once(EVENTS.NETWORK_EVENT);
   tab.linkedBrowser.reload();
-  yield onMonitorUpdated;
+  yield networkEvent;
 
   assertSingleRequestState();
 
   // Make sure we can now open the network details panel
   EventUtils.sendMouseEvent({ type: "click" }, detailsPanelToggleButton);
 
   ok(document.querySelector(".network-details-panel") &&
     !detailsPanelToggleButton.classList.contains("pane-collapsed"),
--- a/devtools/client/netmonitor/test/browser_net_content-type.js
+++ b/devtools/client/netmonitor/test/browser_net_content-type.js
@@ -17,17 +17,17 @@ add_task(function* () {
   let {
     getDisplayedRequests,
     getSortedRequests,
   } = windowRequire("devtools/client/netmonitor/src/selectors/index");
 
   store.dispatch(Actions.batchEnable(false));
 
   let wait = waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS);
-  yield ContentTask.spawn(tab.linkedBrowser, {}, function () {
+  yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
   for (let requestItem of document.querySelectorAll(".request-list-item")) {
     let requestsListStatus = requestItem.querySelector(".requests-list-status");
     requestItem.scrollIntoView();
     EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
@@ -137,35 +137,35 @@ add_task(function* () {
       type: "plain",
       fullMimeType: "text/plain",
       transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 324),
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 10.73),
       time: true
     }
   );
 
-  yield selectIndexAndWaitForSourceEditor(monitor, 0);
+  yield selectIndexAndWaitForSourceEditor(0);
   yield testResponseTab("xml");
 
-  yield selectIndexAndWaitForSourceEditor(monitor, 1);
+  yield selectIndexAndWaitForSourceEditor(1);
   yield testResponseTab("css");
 
-  yield selectIndexAndWaitForSourceEditor(monitor, 2);
+  yield selectIndexAndWaitForSourceEditor(2);
   yield testResponseTab("js");
 
   yield selectIndexAndWaitForJSONView(3);
   yield testResponseTab("json");
 
-  yield selectIndexAndWaitForSourceEditor(monitor, 4);
+  yield selectIndexAndWaitForSourceEditor(4);
   yield testResponseTab("html");
 
   yield selectIndexAndWaitForImageView(5);
   yield testResponseTab("png");
 
-  yield selectIndexAndWaitForSourceEditor(monitor, 6);
+  yield selectIndexAndWaitForSourceEditor(6);
   yield testResponseTab("gzip");
 
   yield teardown(monitor);
 
   function* testResponseTab(type) {
     let tabpanel = document.querySelector("#response-panel");
 
     function checkVisibility(box) {
@@ -265,31 +265,37 @@ add_task(function* () {
 
         is(text, new Array(1000).join("Hello gzip!"),
           "The text shown in the source editor is incorrect for the gzip request.");
         break;
       }
     }
   }
 
+  function* selectIndexAndWaitForSourceEditor(index) {
+    let editor = document.querySelector("#response-panel .CodeMirror-code");
+    if (!editor) {
+      let waitDOM = waitForDOM(document, "#response-panel .CodeMirror-code");
+      EventUtils.sendMouseEvent({ type: "mousedown" },
+        document.querySelectorAll(".request-list-item")[index]);
+      document.querySelector("#response-tab").click();
+      yield waitDOM;
+    } else {
+      EventUtils.sendMouseEvent({ type: "mousedown" },
+        document.querySelectorAll(".request-list-item")[index]);
+    }
+  }
+
   function* selectIndexAndWaitForJSONView(index) {
-    let onResponseContent = monitor.panelWin.once(EVENTS.RECEIVED_RESPONSE_CONTENT);
     let tabpanel = document.querySelector("#response-panel");
     let waitDOM = waitForDOM(tabpanel, ".treeTable");
     store.dispatch(Actions.selectRequestByIndex(index));
     yield waitDOM;
-    yield onResponseContent;
-
-    // Waiting for RECEIVED_RESPONSE_CONTENT isn't enough.
-    // DOM may not be fully updated yet and checkVisibility(json) may still fail.
-    yield waitForTick();
   }
 
   function* selectIndexAndWaitForImageView(index) {
-    let onResponseContent = monitor.panelWin.once(EVENTS.RECEIVED_RESPONSE_CONTENT);
     let tabpanel = document.querySelector("#response-panel");
     let waitDOM = waitForDOM(tabpanel, ".response-image");
     store.dispatch(Actions.selectRequestByIndex(index));
     let [imageNode] = yield waitDOM;
     yield once(imageNode, "load");
-    yield onResponseContent;
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_headers-alignment.js
+++ b/devtools/client/netmonitor/test/browser_net_headers-alignment.js
@@ -5,17 +5,17 @@
 
 /**
  * Bug 1360457 - Mis-alignment between headers and columns on overflow
  */
 
 add_task(function* () {
   requestLongerTimeout(4);
 
-  let { tab, monitor } = yield initNetMonitor(INFINITE_GET_URL, true);
+  let { monitor } = yield initNetMonitor(INFINITE_GET_URL, true);
   let { document, windowRequire, store } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
 
   store.dispatch(Actions.batchEnable(false));
 
   // Wait until the first request makes the empty notice disappear
   yield waitForRequestListToAppear();
 
@@ -35,21 +35,16 @@ add_task(function* () {
     let aHeaderColumn = headers.childNodes[columnNumber];
     let aRequestColumn = firstRequestLine.childNodes[columnNumber];
     is(aHeaderColumn.getBoundingClientRect().left,
        aRequestColumn.getBoundingClientRect().left,
        "Headers for columns number " + columnNumber + " are aligned."
     );
   }
 
-  // Stop doing requests.
-  yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
-    content.wrappedJSObject.stopRequests();
-  });
-
   // Done: clean up.
   return teardown(monitor);
 
   function waitForRequestListToAppear() {
     info("Waiting until the empty notice disappears and is replaced with the list");
     return waitUntil(() => !!document.querySelector(".requests-list-contents"));
   }
 
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_icon-preview.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if image responses show a thumbnail in the requests menu.
+ */
+
+add_task(function* () {
+  let { tab, monitor } = yield initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL);
+  const SELECTOR = ".requests-list-icon[src]";
+  info("Starting test... ");
+
+  let { document, store, windowRequire, connector } = monitor.panelWin;
+  let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
+  let { triggerActivity } = connector;
+  let { ACTIVITY_TYPE } = windowRequire("devtools/client/netmonitor/src/constants");
+
+  store.dispatch(Actions.batchEnable(false));
+
+  let wait = waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS);
+  yield performRequests();
+  yield wait;
+  yield waitUntil(() => !!document.querySelector(SELECTOR));
+
+  info("Checking the image thumbnail when all items are shown.");
+  checkImageThumbnail();
+
+  store.dispatch(Actions.sortBy("contentSize"));
+  info("Checking the image thumbnail when all items are sorted.");
+  checkImageThumbnail();
+
+  store.dispatch(Actions.toggleRequestFilterType("images"));
+  info("Checking the image thumbnail when only images are shown.");
+  checkImageThumbnail();
+
+  info("Reloading the debuggee and performing all requests again...");
+  wait = waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS);
+  yield reloadAndPerformRequests();
+  yield wait;
+  yield waitUntil(() => !!document.querySelector(SELECTOR));
+
+  info("Checking the image thumbnail after a reload.");
+  checkImageThumbnail();
+
+  yield teardown(monitor);
+
+  function performRequests() {
+    return ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+      content.wrappedJSObject.performRequests();
+    });
+  }
+
+  function* reloadAndPerformRequests() {
+    yield triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED);
+    yield performRequests();
+  }
+
+  function checkImageThumbnail() {
+    is(document.querySelectorAll(SELECTOR).length, 1,
+      "There should be only one image request with a thumbnail displayed.");
+    is(document.querySelector(SELECTOR).src, TEST_IMAGE_DATA_URI,
+      "The image requests-list-icon thumbnail is displayed correctly.");
+    is(document.querySelector(SELECTOR).hidden, false,
+      "The image requests-list-icon thumbnail should not be hidden.");
+  }
+});
--- a/devtools/client/netmonitor/test/browser_net_image-tooltip.js
+++ b/devtools/client/netmonitor/test/browser_net_image-tooltip.js
@@ -6,45 +6,48 @@
 const IMAGE_TOOLTIP_URL = EXAMPLE_URL + "html_image-tooltip-test-page.html";
 const IMAGE_TOOLTIP_REQUESTS = 1;
 
 /**
  * Tests if image responses show a popup in the requests menu when hovered.
  */
 add_task(function* test() {
   let { tab, monitor } = yield initNetMonitor(IMAGE_TOOLTIP_URL);
+  const SELECTOR = ".requests-list-icon[src]";
   info("Starting test... ");
 
   let { document, store, windowRequire, connector } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   let { triggerActivity } = connector;
   let { ACTIVITY_TYPE } = windowRequire("devtools/client/netmonitor/src/constants");
   let toolboxDoc = monitor.panelWin.parent.document;
 
   store.dispatch(Actions.batchEnable(false));
 
   let onEvents = waitForNetworkEvents(monitor, IMAGE_TOOLTIP_REQUESTS);
   yield performRequests();
   yield onEvents;
+  yield waitUntil(() => !!document.querySelector(SELECTOR));
 
   info("Checking the image thumbnail after a few requests were made...");
   yield showTooltipAndVerify(document.querySelectorAll(".request-list-item")[0]);
 
   // Hide tooltip before next test, to avoid the situation that tooltip covers
   // the icon for the request of the next test.
   info("Checking the image thumbnail gets hidden...");
   yield hideTooltipAndVerify(document.querySelectorAll(".request-list-item")[0]);
 
   // +1 extra document reload
   onEvents = waitForNetworkEvents(monitor, IMAGE_TOOLTIP_REQUESTS + 1);
 
   info("Reloading the debuggee and performing all requests again...");
   yield triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED);
   yield performRequests();
   yield onEvents;
+  yield waitUntil(() => !!document.querySelector(SELECTOR));
 
   info("Checking the image thumbnail after a reload.");
   yield showTooltipAndVerify(document.querySelectorAll(".request-list-item")[1]);
 
   info("Checking if the image thumbnail is hidden when mouse leaves the menu widget");
   let requestsListContents = document.querySelector(".requests-list-contents");
   EventUtils.synthesizeMouse(requestsListContents, 0, 0, { type: "mousemove" },
                              monitor.panelWin);
@@ -58,17 +61,17 @@ add_task(function* test() {
     });
   }
 
   /**
    * Show a tooltip on the {target} and verify that it was displayed
    * with the expected content.
    */
   function* showTooltipAndVerify(target) {
-    let anchor = target.querySelector(".requests-list-file");
+    let anchor = target.querySelector(".requests-list-icon");
     yield showTooltipOn(anchor);
 
     info("Tooltip was successfully opened for the image request.");
     is(toolboxDoc.querySelector(".tooltip-panel img").src, TEST_IMAGE_DATA_URI,
       "The tooltip's image content is displayed correctly.");
   }
 
   /**
--- a/devtools/client/netmonitor/test/browser_net_resend_cors.js
+++ b/devtools/client/netmonitor/test/browser_net_resend_cors.js
@@ -51,29 +51,25 @@ add_task(function* () {
     info("Sending the cloned request (without change)");
     store.dispatch(Actions.sendCustomRequest(connector));
   });
 
   info("Waiting for both resent requests");
   yield onRequests;
 
   // Check the resent requests
-  for (let i = 0; i < ITEMS.length; i++) {
-    let item = ITEMS[i];
+  ITEMS.forEach((item, i) => {
     is(item.method, METHODS[i], `The ${item.method} request has the right method`);
     is(item.url, requestUrl, `The ${item.method} request has the right URL`);
     is(item.status, 200, `The ${item.method} response has the right status`);
 
     if (item.method === "POST") {
-      // Force fetching response content
-      let responseContent = yield connector.requestData(item.id, "responseContent");
-
       is(item.requestPostData.postData.text, "post-data",
         "The POST request has the right POST data");
       // eslint-disable-next-line mozilla/no-cpows-in-tests
-      is(responseContent.content.text, "Access-Control-Allow-Origin: *",
+      is(item.responseContent.content.text, "Access-Control-Allow-Origin: *",
         "The POST response has the right content");
     }
-  }
+  });
 
   info("Finishing the test");
   return teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_security-error.js
+++ b/devtools/client/netmonitor/test/browser_net_security-error.js
@@ -42,16 +42,19 @@ add_task(function* () {
    * completed.
    */
   function waitForSecurityBrokenNetworkEvent() {
     let awaitedEvents = [
       "UPDATING_REQUEST_HEADERS",
       "RECEIVED_REQUEST_HEADERS",
       "UPDATING_REQUEST_COOKIES",
       "RECEIVED_REQUEST_COOKIES",
+      "STARTED_RECEIVING_RESPONSE",
+      "UPDATING_RESPONSE_CONTENT",
+      "RECEIVED_RESPONSE_CONTENT",
       "UPDATING_EVENT_TIMINGS",
       "RECEIVED_EVENT_TIMINGS",
       "UPDATING_SECURITY_INFO",
       "RECEIVED_SECURITY_INFO",
     ];
 
     let promises = awaitedEvents.map((event) => {
       return monitor.panelWin.once(EVENTS[event]);
--- a/devtools/client/netmonitor/test/browser_net_security-state.js
+++ b/devtools/client/netmonitor/test/browser_net_security-state.js
@@ -75,40 +75,49 @@ add_task(function* () {
     yield executeRequests(1, "http://test1.example.com" + CORS_SJS_PATH);
     yield done;
 
     done = waitForNetworkEvents(monitor, 1);
     info("Requesting a resource over HTTPS.");
     yield executeRequests(1, "https://example.com" + CORS_SJS_PATH);
     yield done;
 
-    done = waitForSecurityBrokenNetworkEvent();
+    done = waitForSecurityBrokenNetworkEvent(true);
     info("Requesting a resource over HTTP to localhost.");
     yield executeRequests(1, "http://localhost" + CORS_SJS_PATH);
     yield done;
 
     const expectedCount = Object.keys(EXPECTED_SECURITY_STATES).length;
     is(store.getState().requests.requests.size,
       expectedCount,
       expectedCount + " events logged.");
   }
 
   /**
    * Returns a promise that's resolved once a request with security issues is
    * completed.
    */
-  function waitForSecurityBrokenNetworkEvent() {
+  function waitForSecurityBrokenNetworkEvent(networkError) {
     let awaitedEvents = [
       "UPDATING_REQUEST_HEADERS",
       "RECEIVED_REQUEST_HEADERS",
       "UPDATING_REQUEST_COOKIES",
       "RECEIVED_REQUEST_COOKIES",
+      "STARTED_RECEIVING_RESPONSE",
+      "UPDATING_RESPONSE_CONTENT",
+      "RECEIVED_RESPONSE_CONTENT",
       "UPDATING_EVENT_TIMINGS",
       "RECEIVED_EVENT_TIMINGS",
     ];
 
+    // If the reason for breakage is a network error, then the
+    // STARTED_RECEIVING_RESPONSE event does not fire.
+    if (networkError) {
+      awaitedEvents = awaitedEvents.filter(e => e !== "STARTED_RECEIVING_RESPONSE");
+    }
+
     let promises = awaitedEvents.map((event) => {
       return monitor.panelWin.once(EVENTS[event]);
     });
 
     return Promise.all(promises);
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_security-tab-visibility.js
+++ b/devtools/client/netmonitor/test/browser_net_security-tab-visibility.js
@@ -93,16 +93,19 @@ add_task(function* () {
    * completed.
    */
   function waitForSecurityBrokenNetworkEvent() {
     let awaitedEvents = [
       "UPDATING_REQUEST_HEADERS",
       "RECEIVED_REQUEST_HEADERS",
       "UPDATING_REQUEST_COOKIES",
       "RECEIVED_REQUEST_COOKIES",
+      "STARTED_RECEIVING_RESPONSE",
+      "UPDATING_RESPONSE_CONTENT",
+      "RECEIVED_RESPONSE_CONTENT",
       "UPDATING_EVENT_TIMINGS",
       "RECEIVED_EVENT_TIMINGS",
     ];
 
     let promises = awaitedEvents.map((event) => {
       return monitor.panelWin.once(EVENTS[event]);
     });
 
--- a/devtools/client/netmonitor/test/browser_net_simple-request-data.js
+++ b/devtools/client/netmonitor/test/browser_net_simple-request-data.js
@@ -245,34 +245,50 @@ function test() {
         SIMPLE_SJS,
         {
           status: "200",
           statusText: "Och Aye"
         }
       );
     });
 
-    expectEvent(EVENTS.PAYLOAD_READY, async () => {
+    expectEvent(EVENTS.RECEIVED_RESPONSE_CONTENT, async () => {
       await waitUntil(() => {
         let requestItem = getSortedRequests(store.getState()).get(0);
         return requestItem &&
                requestItem.transferredSize &&
                requestItem.contentSize &&
-               requestItem.mimeType;
+               requestItem.mimeType &&
+               requestItem.responseContent;
       });
 
       let requestItem = getSortedRequests(store.getState()).get(0);
 
       is(requestItem.transferredSize, "342",
         "The transferredSize data has an incorrect value.");
       is(requestItem.contentSize, "12",
         "The contentSize data has an incorrect value.");
       is(requestItem.mimeType, "text/plain; charset=utf-8",
         "The mimeType data has an incorrect value.");
 
+      ok(requestItem.responseContent,
+        "There should be a responseContent data available.");
+      // eslint-disable-next-line mozilla/no-cpows-in-tests
+      is(requestItem.responseContent.content.mimeType,
+        "text/plain; charset=utf-8",
+        "The responseContent data has an incorrect |content.mimeType| property.");
+      // eslint-disable-next-line mozilla/no-cpows-in-tests
+      is(requestItem.responseContent.content.text,
+        "Hello world!",
+        "The responseContent data has an incorrect |content.text| property.");
+      // eslint-disable-next-line mozilla/no-cpows-in-tests
+      is(requestItem.responseContent.content.size,
+        12,
+        "The responseContent data has an incorrect |content.size| property.");
+
       verifyRequestItemTarget(
         document,
         getDisplayedRequests(store.getState()),
         requestItem,
         "GET",
         SIMPLE_SJS,
         {
           type: "plain",
--- a/devtools/client/netmonitor/test/browser_net_streaming-response.js
+++ b/devtools/client/netmonitor/test/browser_net_streaming-response.js
@@ -60,23 +60,37 @@ add_task(function* () {
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelector(".network-details-panel-toggle"));
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelector("#response-tab"));
   yield wait;
 
   store.dispatch(Actions.selectRequest(null));
 
-  yield selectIndexAndWaitForSourceEditor(monitor, 0);
+  yield selectIndexAndWaitForSourceEditor(0);
   // the hls-m3u8 part
   testEditorContent(REQUESTS[0]);
 
-  yield selectIndexAndWaitForSourceEditor(monitor, 1);
+  yield selectIndexAndWaitForSourceEditor(1);
   // the mpeg-dash part
   testEditorContent(REQUESTS[1]);
 
   return teardown(monitor);
 
+  function* selectIndexAndWaitForSourceEditor(index) {
+    let editor = document.querySelector("#response-panel .CodeMirror-code");
+    if (!editor) {
+      let waitDOM = waitForDOM(document, "#response-panel .CodeMirror-code");
+      EventUtils.sendMouseEvent({ type: "mousedown" },
+        document.querySelectorAll(".request-list-item")[index]);
+      document.querySelector("#response-tab").click();
+      yield waitDOM;
+    } else {
+      EventUtils.sendMouseEvent({ type: "mousedown" },
+        document.querySelectorAll(".request-list-item")[index]);
+    }
+  }
+
   function testEditorContent([ fmt, textRe ]) {
     ok(document.querySelector(".CodeMirror-line").textContent.match(textRe),
       "The text shown in the source editor for " + fmt + " is correct.");
   }
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_thumbnail-click.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that clicking on the file thumbnail opens the response details tab.
+ */
+
+add_task(function* () {
+  let { tab, monitor } = yield initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL);
+  let { document } = monitor.panelWin;
+
+  yield performRequestsAndWait();
+
+  let wait = waitForDOM(document, "#response-panel");
+
+  let request = document.querySelectorAll(".request-list-item")[5];
+  let icon = request.querySelector(".requests-list-icon");
+
+  info("Clicking thumbnail of the sixth request.");
+  EventUtils.synthesizeMouseAtCenter(icon, {}, monitor.panelWin);
+
+  yield wait;
+
+  ok(document.querySelector("#response-tab[aria-selected=true]"),
+     "Response tab is selected.");
+  ok(document.querySelector(".response-image-box"),
+     "Response image preview is shown.");
+
+  yield teardown(monitor);
+
+  function* performRequestsAndWait() {
+    let onAllEvents = waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS);
+    yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+      content.wrappedJSObject.performRequests();
+    });
+    yield onAllEvents;
+  }
+});
--- a/devtools/client/netmonitor/test/head.js
+++ b/devtools/client/netmonitor/test/head.js
@@ -1,28 +1,24 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /* import-globals-from ../../framework/test/shared-head.js */
-/* import-globals-from shared-head.js */
 /* exported Toolbox, restartNetMonitor, teardown, waitForExplicitFinish,
    verifyRequestItemTarget, waitFor, testFilterButtons, loadCommonFrameScript,
-   performRequestsInContent, waitForNetworkEvents, selectIndexAndWaitForSourceEditor */
+   performRequestsInContent, waitForNetworkEvents */
 
 "use strict";
 
 // shared-head.js handles imports, constants, and utility functions
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
   this);
 
-Services.scriptloader.loadSubScript(
-  "chrome://mochitests/content/browser/devtools/client/netmonitor/test/shared-head.js",
-  this);
-
+const { EVENTS } = require("devtools/client/netmonitor/src/constants");
 const {
   getFormattedIPAndPort,
   getFormattedTime,
 } = require("devtools/client/netmonitor/src/utils/format-utils");
 const {
   decodeUnicodeUrl,
   getFormattedProtocol,
   getUrlBaseName,
@@ -280,22 +276,16 @@ function restartNetMonitor(monitor, newU
 }
 
 function teardown(monitor) {
   info("Destroying the specified network monitor.");
 
   return Task.spawn(function* () {
     let tab = monitor.toolbox.target.tab;
 
-    // Ensure that there is no pending RDP requests related to payload request
-    // done from FirefoxDataProvider.
-    info("Wait for completion of all pending RDP requests...");
-    yield waitForExistingRequests(monitor);
-    info("All pending requests finished.");
-
     let onDestroyed = monitor.once("destroyed");
     yield removeTab(tab);
     yield onDestroyed;
   });
 }
 
 function waitForNetworkEvents(monitor, getRequests, postRequests = 0) {
   return new Promise((resolve) => {
@@ -311,24 +301,23 @@ function waitForNetworkEvents(monitor, g
       ["UPDATING_REQUEST_COOKIES", onGenericEvent],
       ["RECEIVED_REQUEST_COOKIES", onGenericEvent],
       ["UPDATING_REQUEST_POST_DATA", onPostEvent],
       ["RECEIVED_REQUEST_POST_DATA", onPostEvent],
       ["UPDATING_RESPONSE_HEADERS", onGenericEvent],
       ["RECEIVED_RESPONSE_HEADERS", onGenericEvent],
       ["UPDATING_RESPONSE_COOKIES", onGenericEvent],
       ["RECEIVED_RESPONSE_COOKIES", onGenericEvent],
+      ["STARTED_RECEIVING_RESPONSE", onGenericEvent],
+      ["UPDATING_RESPONSE_CONTENT", onGenericEvent],
+      ["RECEIVED_RESPONSE_CONTENT", onGenericEvent],
       ["UPDATING_EVENT_TIMINGS", onGenericEvent],
       ["RECEIVED_EVENT_TIMINGS", onGenericEvent],
       ["PAYLOAD_READY", onPayloadReady]
     ];
-    let expectedGenericEvents = awaitedEventsToListeners
-      .filter(([, listener]) => listener == onGenericEvent).length;
-    let expectedPostEvents = awaitedEventsToListeners
-      .filter(([, listener]) => listener == onPostEvent).length;
 
     function initProgressForURL(url) {
       if (progress[url]) {
         return;
       }
       progress[url] = {};
       awaitedEventsToListeners.forEach(function ([e]) {
         progress[url][e] = 0;
@@ -371,34 +360,32 @@ function waitForNetworkEvents(monitor, g
       }
 
       payloadReady++;
       maybeResolve(event, actor, networkInfo);
     }
 
     function maybeResolve(event, actor, networkInfo) {
       info("> Network events progress: " +
-        "Payload: " + payloadReady + "/" + (getRequests + postRequests) + ", " +
-        "Generic: " + genericEvents + "/" +
-          ((getRequests + postRequests) * expectedGenericEvents) + ", " +
-        "Post: " + postEvents + "/" + (postRequests * expectedPostEvents) + ", " +
+        genericEvents + "/" + ((getRequests + postRequests) * 13) + ", " +
+        postEvents + "/" + (postRequests * 2) + ", " +
         "got " + event + " for " + actor);
 
       let url = networkInfo.request.url;
       updateProgressForURL(url, event);
 
       // Uncomment this to get a detailed progress logging (when debugging a test)
       // info("> Current state: " + JSON.stringify(progress, null, 2));
 
-      // There are `expectedGenericEvents` updates which need to be fired for a request
-      // to be considered finished. The "requestPostData" packet isn't fired for non-POST
-      // requests.
+      // There are 15 updates which need to be fired for a request to be
+      // considered finished. The "requestPostData" packet isn't fired for
+      // non-POST requests.
       if (payloadReady >= (getRequests + postRequests) &&
-        genericEvents >= (getRequests + postRequests) * expectedGenericEvents &&
-        postEvents >= postRequests * expectedPostEvents) {
+        genericEvents >= (getRequests + postRequests) * 13 &&
+        postEvents >= postRequests * 2) {
         awaitedEventsToListeners.forEach(([e, l]) => panel.off(EVENTS[e], l));
         executeSoon(resolve);
       }
     }
 
     awaitedEventsToListeners.forEach(([e, l]) => panel.on(EVENTS[e], l));
   });
 }
@@ -692,30 +679,8 @@ function waitForContentMessage(name) {
 
   return new Promise((resolve) => {
     mm.addMessageListener(name, function onMessage(msg) {
       mm.removeMessageListener(name, onMessage);
       resolve(msg);
     });
   });
 }
-
-/**
- * Select a request and switch to its response panel.
- *
- * @param {Number} index The request index to be selected
- */
-async function selectIndexAndWaitForSourceEditor(monitor, index) {
-  let document = monitor.panelWin.document;
-  let onResponseContent = monitor.panelWin.once(EVENTS.RECEIVED_RESPONSE_CONTENT);
-  // Select the request first, as it may try to fetch whatever is the current request's
-  // responseContent if we select the ResponseTab first.
-  EventUtils.sendMouseEvent({ type: "mousedown" },
-    document.querySelectorAll(".request-list-item")[index]);
-  // We may already be on the ResponseTab, so only select it if needed.
-  let editor = document.querySelector("#response-panel .CodeMirror-code");
-  if (!editor) {
-    let waitDOM = waitForDOM(document, "#response-panel .CodeMirror-code");
-    document.querySelector("#response-tab").click();
-    await waitDOM;
-  }
-  await onResponseContent;
-}
--- a/devtools/client/netmonitor/test/html_infinite-get-page.html
+++ b/devtools/client/netmonitor/test/html_infinite-get-page.html
@@ -26,24 +26,18 @@
             callback();
           }
         };
         xhr.send(null);
       }
 
       // Use a count parameter to defeat caching.
       let count = 0;
-      let doRequests = true;
-      function stopRequests() { // eslint-disable-line no-unused-vars
-        doRequests = false;
-      }
 
       (function performRequests() {
         get("request_" + (count++), function () {
-          if (doRequests) {
-            setTimeout(performRequests, 50);
-          }
+          setTimeout(performRequests, 50);
         });
       })();
     </script>
   </body>
 
 </html>
deleted file mode 100644
--- a/devtools/client/netmonitor/test/shared-head.js
+++ /dev/null
@@ -1,43 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/* exported EVENTS, waitForExistingRequests */
-
-"use strict";
-
-const { EVENTS } = require("devtools/client/netmonitor/src/constants");
-
-async function waitForExistingRequests(monitor) {
-  let { store } = monitor.panelWin;
-  function getRequests() {
-    return store.getState().requests.requests;
-  }
-  function areAllRequestsFullyLoaded() {
-    let requests = getRequests().valueSeq();
-    for (let request of requests) {
-      // Ignore cloned request as we don't lazily fetch data for them
-      // and have arbitrary number of field set.
-      if (request.id.includes("-clone")) {
-        continue;
-      }
-      // Do same check than FirefoxDataProvider.isRequestPayloadReady,
-      // in order to ensure there is no more pending payload requests to be done.
-      if (!request.requestHeaders || !request.requestCookies ||
-          !request.eventTimings ||
-          (!request.securityInfo && !request.fromServiceWorker) ||
-          ((!request.responseHeaders || !request.responseCookies) &&
-            request.securityState != "broken" &&
-            (!request.responseContentAvailable || request.status))) {
-        return false;
-      }
-    }
-    return true;
-  }
-  // If there is no request, we are good to go.
-  if (getRequests().size == 0) {
-    return;
-  }
-  while (!areAllRequestsFullyLoaded()) {
-    await monitor.panelWin.once(EVENTS.PAYLOAD_READY);
-  }
-}
--- a/devtools/client/styleeditor/test/browser.ini
+++ b/devtools/client/styleeditor/test/browser.ini
@@ -53,17 +53,16 @@ support-files =
   doc_xulpage.xul
   sync.html
   utf-16.css
   !/devtools/client/commandline/test/helpers.js
   !/devtools/client/framework/test/shared-head.js
   !/devtools/client/inspector/shared/test/head.js
   !/devtools/client/inspector/test/head.js
   !/devtools/client/inspector/test/shared-head.js
-  !/devtools/client/netmonitor/test/shared-head.js
   !/devtools/client/responsive.html/test/browser/devices.json
   !/devtools/client/shared/test/test-actor-registry.js
   !/devtools/client/shared/test/test-actor.js
 
 [browser_styleeditor_add_stylesheet.js]
 [browser_styleeditor_autocomplete.js]
 [browser_styleeditor_autocomplete-disabled.js]
 [browser_styleeditor_bom.js]
--- a/devtools/client/styleeditor/test/browser_styleeditor_fetch-from-cache.js
+++ b/devtools/client/styleeditor/test/browser_styleeditor_fetch-from-cache.js
@@ -1,21 +1,16 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
-/* import-globals-from ../../netmonitor/test/shared-head.js */
-
 // A test to ensure Style Editor doesn't bybass cache when loading style sheet
 // contents (bug 978688).
 
-Services.scriptloader.loadSubScript(
-  "chrome://mochitests/content/browser/devtools/client/netmonitor/test/shared-head.js", this);
-
 const TEST_URL = TEST_BASE_HTTP + "doc_uncached.html";
 
 add_task(function* () {
   // Disable rcwn to make cache behavior deterministic.
   yield pushPref("network.http.rcwn.enabled", false);
 
   info("Opening netmonitor");
   let tab = yield addTab("about:blank");
@@ -34,18 +29,16 @@ add_task(function* () {
   yield navigateTo(TEST_URL);
 
   info("Opening Style Editor");
   let styleeditor = yield toolbox.selectTool("styleeditor");
 
   info("Waiting for the source to be loaded.");
   yield styleeditor.UI.editors[0].getSourceEditor();
 
-  yield waitForExistingRequests(monitor);
-
   info("Checking Netmonitor contents.");
   let items = [];
   for (let item of getSortedRequests(store.getState())) {
     if (item.url.endsWith("doc_uncached.css")) {
       items.push(item);
     }
   }
 
--- a/devtools/client/webconsole/new-console-output/components/message-types/NetworkEventMessage.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/NetworkEventMessage.js
@@ -111,19 +111,16 @@ function NetworkEventMessage({
     getLongString: (grip) => {
       return serviceContainer.getLongString(grip);
     },
     getTabTarget: () => {},
     getNetworkRequest: () => {},
     sendHTTPRequest: () => {},
     setPreferences: () => {},
     triggerActivity: () => {},
-    requestData: (requestId, dataType) => {
-      return serviceContainer.requestData(requestId, dataType);
-    },
   };
 
   // Only render the attachment if the network-event is
   // actually opened (performance optimization).
   const attachment = open && dom.div({
     className: "network-info network-monitor devtools-monospace"},
     TabboxPanel({
       connector,
--- a/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
+++ b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
@@ -91,19 +91,16 @@ NewConsoleOutputWrapper.prototype = {
           hud.owner.openLink(url);
         },
         createElement: nodename => {
           return this.document.createElement(nodename);
         },
         getLongString: (grip) => {
           return hud.proxy.webConsoleClient.getString(grip);
         },
-        requestData(id, type) {
-          return hud.proxy.networkDataProvider.requestData(id, type);
-        },
       };
 
       // Set `openContextMenu` this way so, `serviceContainer` variable
       // is available in the current scope and we can pass it into
       // `createContextMenu` method.
       serviceContainer.openContextMenu = (e, message) => {
         let { screenX, screenY, target } = e;
 
--- a/devtools/client/webconsole/new-console-output/store.js
+++ b/devtools/client/webconsole/new-console-output/store.js
@@ -172,23 +172,16 @@ function enableNetProvider(hud) {
       // Data provider implements async logic for fetching
       // data from the backend. It's created the first
       // time it's needed.
       if (!dataProvider) {
         dataProvider = new DataProvider({
           actions,
           webConsoleClient: proxy.webConsoleClient
         });
-
-        // /!\ This is terrible, but it allows ResponsePanel to be able to call
-        // `dataProvider.requestData` to fetch response content lazily.
-        // `proxy.networkDataProvider` is put by NewConsoleOutputWrapper on
-        // `serviceContainer` which allow NetworkEventMessage to expose requestData on
-        // the fake `connector` object it hands over to ResponsePanel.
-        proxy.networkDataProvider = dataProvider;
       }
 
       let type = action.type;
       let newState = reducer(state, action);
 
       // If network message has been opened, fetch all HTTP details
       // from the backend. It can happen (especially in test) that
       // the message is opened before all network event updates are
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_netmonitor_shows_reqs_in_webconsole.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_netmonitor_shows_reqs_in_webconsole.js
@@ -59,11 +59,9 @@ async function testNetmonitor(toolbox) {
   await waitUntil(() => store.getState().requests.requests.size > 0);
 
   is(store.getState().requests.requests.size, 1,
     "Network request appears in the network panel");
 
   let item = getSortedRequests(store.getState()).get(0);
   is(item.method, "GET", "The attached method is correct.");
   is(item.url, TEST_PATH, "The attached url is correct.");
-
-  await waitForExistingRequests(monitor);
 }
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_network_attach.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_network_attach.js
@@ -44,18 +44,16 @@ add_task(async function task() {
   // Expand network log
   urlNode.click();
 
   await consoleReady;
 
   info("network-request-payload-ready received");
 
   await testNetworkMessage(messageNode);
-
-  await waitForExistingRequests(monitor);
 });
 
 async function testNetworkMessage(messageNode) {
   let headersTab = messageNode.querySelector("#headers-tab");
 
   ok(headersTab, "Headers tab is available");
 
   // Headers tab should be selected by default, so just check its content.
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_network_messages_openinnet.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_network_messages_openinnet.js
@@ -70,12 +70,9 @@ async function testNetmonitorLink(toolbo
   store.dispatch(actions.batchEnable(false));
 
   await waitUntil(() => {
     const selected = getSelectedRequest(store.getState());
     return selected && selected.url === url;
   });
 
   ok(true, "The attached url is correct.");
-
-  let monitor = toolbox.getCurrentPanel();
-  await waitForExistingRequests(monitor);
 }
--- a/devtools/client/webconsole/new-console-output/test/mochitest/head.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/head.js
@@ -1,30 +1,26 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 /* import-globals-from ../../../../framework/test/shared-head.js */
-/* import-globals-from ../../../../netmonitor/test/shared-head.js */
 /* exported WCUL10n, openNewTabAndConsole, waitForMessages, waitForMessage, waitFor,
    findMessage, openContextMenu, hideContextMenu, loadDocument, hasFocus,
    waitForNodeMutation, testOpenInDebugger, checkClickOnNode, jstermSetValueAndComplete,
    openDebugger, openConsole */
 
 "use strict";
 
 // shared-head.js handles imports, constants, and utility functions
 // Load the shared-head file first.
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
   this);
 
-Services.scriptloader.loadSubScript(
-  "chrome://mochitests/content/browser/devtools/client/netmonitor/test/shared-head.js", this);
-
 var {HUDService} = require("devtools/client/webconsole/hudservice");
 var WCUL10n = require("devtools/client/webconsole/webconsole-l10n");
 
 Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", true);
 registerCleanupFunction(function* () {
   Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled");
   Services.prefs.clearUserPref("devtools.webconsole.ui.filterbar");
 
--- a/devtools/client/webconsole/test/browser_netmonitor_shows_reqs_in_webconsole.js
+++ b/devtools/client/webconsole/test/browser_netmonitor_shows_reqs_in_webconsole.js
@@ -1,18 +1,15 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-Services.scriptloader.loadSubScript(
-  "chrome://mochitests/content/browser/devtools/client/netmonitor/test/shared-head.js", this);
-
 const TEST_URI = "data:text/html;charset=utf8,Test that the netmonitor " +
                  "displays requests that have been recorded in the " +
                  "web console, even if the netmonitor hadn't opened yet.";
 
 const TEST_FILE = "test-network-request.html";
 const TEST_PATH = "http://example.com/browser/devtools/client/webconsole/" +
                   "test/" + TEST_FILE;
 
@@ -74,11 +71,9 @@ function* testNetmonitor(toolbox) {
 
   yield waitUntil(() => store.getState().requests.requests.size > 0);
 
   is(store.getState().requests.requests.size, 1, "Network request appears in the network panel");
 
   let item = getSortedRequests(store.getState()).get(0);
   is(item.method, "GET", "The attached method is correct.");
   is(item.url, TEST_PATH, "The attached url is correct.");
-
-  yield waitForExistingRequests(monitor);
 }
--- a/devtools/client/webconsole/test/browser_webconsole_netlogging_panel.js
+++ b/devtools/client/webconsole/test/browser_webconsole_netlogging_panel.js
@@ -1,41 +1,34 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-/* import-globals-from ../../netmonitor/test/shared-head.js */
-
 // Tests that network log messages bring up the network panel.
 
 "use strict";
 
 const TEST_NETWORK_REQUEST_URI =
   "http://example.com/browser/devtools/client/webconsole/test/" +
   "test-network-request.html";
 
-Services.scriptloader.loadSubScript(
-  "chrome://mochitests/content/browser/devtools/client/netmonitor/test/shared-head.js", this);
-
 add_task(function* () {
   let finishedRequest = waitForFinishedRequest(({ request }) => {
     return request.url.endsWith("test-network-request.html");
   });
 
   const hud = yield loadPageAndGetHud(TEST_NETWORK_REQUEST_URI);
   let request = yield finishedRequest;
 
   yield hud.ui.openNetworkPanel(request.actor);
   let toolbox = gDevTools.getToolbox(hud.target);
   is(toolbox.currentToolId, "netmonitor", "Network panel was opened");
-  let monitor = toolbox.getCurrentPanel();
+  let panel = toolbox.getCurrentPanel();
 
-  let { store, windowRequire } = monitor.panelWin;
+  let { store, windowRequire } = panel.panelWin;
   let { getSelectedRequest } = windowRequire("devtools/client/netmonitor/src/selectors/index");
 
   let selected = getSelectedRequest(store.getState());
   is(selected.method, request.request.method,
      "The correct request is selected");
   is(selected.url, request.request.url,
      "The correct request is definitely selected");
-
-  yield waitForExistingRequests(monitor);
 });
--- a/devtools/client/webconsole/test/browser_webconsole_netlogging_reset_filter.js
+++ b/devtools/client/webconsole/test/browser_webconsole_netlogging_reset_filter.js
@@ -1,28 +1,23 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-/* import-globals-from ../../netmonitor/test/shared-head.js */
-
 // Tests that network log messages bring up the network panel and select the
 // right request even if it was previously filtered off.
 
 "use strict";
 
 const TEST_FILE_URI =
   "http://example.com/browser/devtools/client/webconsole/test/" +
   "test-network.html";
 const TEST_URI = "data:text/html;charset=utf8,<p>test file URI";
 
-Services.scriptloader.loadSubScript(
-  "chrome://mochitests/content/browser/devtools/client/netmonitor/test/shared-head.js", this);
-
 var hud;
 
 add_task(function* () {
   let requests = [];
   let { browser } = yield loadTab(TEST_URI);
 
   yield pushPrefEnv();
   hud = yield openConsole();
@@ -37,18 +32,18 @@ add_task(function* () {
   yield testMessages();
   let htmlRequest = requests.find(e => e.request.url.endsWith("html"));
   ok(htmlRequest, "htmlRequest was a html");
 
   yield hud.ui.openNetworkPanel(htmlRequest.actor);
   let toolbox = gDevTools.getToolbox(hud.target);
   is(toolbox.currentToolId, "netmonitor", "Network panel was opened");
 
-  let monitor = toolbox.getCurrentPanel();
-  let { store, windowRequire } = monitor.panelWin;
+  let panel = toolbox.getCurrentPanel();
+  let { store, windowRequire } = panel.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   let { getSelectedRequest } = windowRequire("devtools/client/netmonitor/src/selectors/index");
 
   let selected = getSelectedRequest(store.getState());
   is(selected.method, htmlRequest.request.method,
      "The correct request is selected");
   is(selected.url, htmlRequest.request.url,
      "The correct request is definitely selected");
@@ -64,18 +59,16 @@ add_task(function* () {
   is(selected.method, htmlRequest.request.method,
      "The correct request is selected");
   is(selected.url, htmlRequest.request.url,
      "The correct request is definitely selected");
 
   // All tests are done. Shutdown.
   HUDService.lastFinishedRequest.callback = null;
   htmlRequest = browser = requests = hud = null;
-
-  yield waitForExistingRequests(monitor);
 });
 
 function testMessages() {
   return waitForMessages({
     webconsole: hud,
     messages: [{
       text: "running network console logging tests",
       category: CATEGORY_WEBDEV,