Bug 1404928 - Avoid passing dispatch props and keep RequestListContnet interface clearly r?honza,ochameau draft
authorRicky Chien <ricky060709@gmail.com>
Fri, 17 Nov 2017 17:28:52 +0800
changeset 701270 97c364d3ebe9e0081ee5fde7ea02a4bd35d4c779
parent 700409 953b13d5e8ea681c9e28f18dc3c56cc51f73af79
child 741131 3c134614ee44a10074ce00b3179e35afcf0a3ceb
push id90120
push userbmo:rchien@mozilla.com
push dateTue, 21 Nov 2017 13:07:04 +0000
reviewershonza, ochameau
bugs1404928
milestone59.0a1
Bug 1404928 - Avoid passing dispatch props and keep RequestListContnet interface clearly r?honza,ochameau MozReview-Commit-ID: 1vcj6blRFIc
devtools/client/netmonitor/src/components/CustomRequestPanel.js
devtools/client/netmonitor/src/components/HeadersPanel.js
devtools/client/netmonitor/src/components/ParamsPanel.js
devtools/client/netmonitor/src/components/RequestListContent.js
devtools/client/netmonitor/src/components/TabboxPanel.js
devtools/client/netmonitor/src/connector/firefox-data-provider.js
devtools/client/netmonitor/src/constants.js
devtools/client/netmonitor/src/har/har-builder.js
devtools/client/netmonitor/src/har/test/browser_net_har_copy_all_as_har.js
devtools/client/netmonitor/src/har/test/browser_net_har_post_data.js
devtools/client/netmonitor/src/har/test/browser_net_har_post_data_on_get.js
devtools/client/netmonitor/src/har/test/browser_net_har_throttle_upload.js
devtools/client/netmonitor/src/reducers/requests.js
devtools/client/netmonitor/src/request-list-context-menu.js
devtools/client/netmonitor/test/browser_net_curl-utils.js
devtools/client/netmonitor/test/browser_net_open_in_debugger.js
devtools/client/netmonitor/test/browser_net_open_in_style_editor.js
devtools/client/netmonitor/test/browser_net_open_request_in_tab.js
devtools/client/netmonitor/test/browser_net_post-data-03.js
devtools/client/netmonitor/test/browser_net_resend.js
devtools/client/netmonitor/test/browser_net_resend_cors.js
devtools/client/netmonitor/test/head.js
devtools/client/webconsole/webconsole-connection-proxy.js
--- a/devtools/client/netmonitor/src/components/CustomRequestPanel.js
+++ b/devtools/client/netmonitor/src/components/CustomRequestPanel.js
@@ -44,34 +44,26 @@ class CustomRequestPanel extends Compone
       request: PropTypes.object,
       sendCustomRequest: PropTypes.func.isRequired,
       updateRequest: PropTypes.func.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
-    this.maybeFetchPostData = this.maybeFetchPostData.bind(this);
-    this.parseRequestText = this.parseRequestText.bind(this);
-    this.updateCustomRequestFields = this.updateCustomRequestFields.bind(this);
   }
 
   componentDidMount() {
     this.maybeFetchPostData(this.props);
   }
 
   componentWillReceiveProps(nextProps) {
     this.maybeFetchPostData(nextProps);
   }
 
-  /**
-   * When switching to another request, lazily fetch post data
-   * from the backend. The CustomRequestPanel will first be empty and then
-   * display the content.
-   */
   maybeFetchPostData(props) {
     if (!props.request.requestPostData ||
         !props.request.requestPostData.postData.text) {
       let actorId = props.request.id.replace("-clone", "");
       // This method will set `props.request.requestPostData`
       // asynchronously and force another render.
       props.connector.requestData(actorId, "requestPostData");
     }
--- a/devtools/client/netmonitor/src/components/HeadersPanel.js
+++ b/devtools/client/netmonitor/src/components/HeadersPanel.js
@@ -44,16 +44,17 @@ const SUMMARY_VERSION = L10N.getStr("net
 
 /*
  * Headers panel component
  * Lists basic information about the request
  */
 class HeadersPanel extends Component {
   static get propTypes() {
     return {
+      connector: PropTypes.object.isRequired,
       cloneSelectedRequest: PropTypes.func.isRequired,
       request: PropTypes.object.isRequired,
       renderValue: PropTypes.func,
       openLink: PropTypes.func,
     };
   }
 
   constructor(props) {
@@ -62,16 +63,34 @@ class HeadersPanel extends Component {
     this.state = {
       rawHeadersOpened: false,
     };
 
     this.getProperties = this.getProperties.bind(this);
     this.toggleRawHeaders = this.toggleRawHeaders.bind(this);
     this.renderSummary = this.renderSummary.bind(this);
     this.renderValue = this.renderValue.bind(this);
+    this.maybeFetchPostData = this.maybeFetchPostData.bind(this);
+  }
+
+  componentDidMount() {
+    this.maybeFetchPostData(this.props);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    this.maybeFetchPostData(nextProps);
+  }
+
+  maybeFetchPostData(props) {
+    if (!props.request.requestPostData ||
+      !props.request.requestPostData.postData.text) {
+      // This method will set `props.request.requestPostData`
+      // asynchronously and force another render.
+      props.connector.requestData(props.request.id, "requestPostData");
+    }
   }
 
   getProperties(headers, title) {
     if (headers && headers.headers.length) {
       let headerKey = `${title} (${getFormattedSize(headers.headersSize, 3)})`;
       let propertiesResult = {
         [headerKey]:
           headers.headers.reduce((acc, { name, value }) =>
--- a/devtools/client/netmonitor/src/components/ParamsPanel.js
+++ b/devtools/client/netmonitor/src/components/ParamsPanel.js
@@ -43,35 +43,28 @@ class ParamsPanel extends Component {
       openLink: PropTypes.func,
       request: PropTypes.object.isRequired,
       updateRequest: PropTypes.func.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
-    this.maybeFetchPostData = this.maybeFetchPostData.bind(this);
-    this.getProperties = this.getProperties.bind(this);
   }
 
   componentDidMount() {
     this.maybeFetchPostData(this.props);
     updateFormDataSections(this.props);
   }
 
   componentWillReceiveProps(nextProps) {
     this.maybeFetchPostData(nextProps);
     updateFormDataSections(nextProps);
   }
 
-  /**
-   * When switching to another request, lazily fetch post data
-   * from the backend. The CustomRequestPanel will first be empty and then
-   * display the content.
-   */
   maybeFetchPostData(props) {
     if (!props.request.requestPostData ||
         !props.request.requestPostData.postData) {
       // This method will set `props.request.requestPostData`
       // asynchronously and force another render.
       props.connector.requestData(props.request.id, "requestPostData");
     }
   }
--- a/devtools/client/netmonitor/src/components/RequestListContent.js
+++ b/devtools/client/netmonitor/src/components/RequestListContent.js
@@ -8,16 +8,17 @@ const { Component, createFactory } = req
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
 const Actions = require("../actions/index");
 const { setTooltipImageContent } = require("../request-list-tooltip");
 const {
   getDisplayedRequests,
+  getSelectedRequest,
   getWaterfallScale,
 } = require("../selectors/index");
 
 // Components
 const RequestListHeader = createFactory(require("./RequestListHeader"));
 const RequestListItem = createFactory(require("./RequestListItem"));
 const RequestListContextMenu = require("../request-list-context-menu");
 
@@ -31,59 +32,57 @@ const MAX_SCROLL_HEIGHT = 2147483647;
 /**
  * Renders the actual contents of the request list.
  */
 class RequestListContent extends Component {
   static get propTypes() {
     return {
       connector: PropTypes.object.isRequired,
       columns: PropTypes.object.isRequired,
-      dispatch: PropTypes.func.isRequired,
+      cloneSelectedRequest: 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,
       onWaterfallMouseDown: PropTypes.func.isRequired,
+      openStatistics: PropTypes.func.isRequired,
       scale: PropTypes.number,
-      selectedRequestId: PropTypes.string,
+      selectedRequest: PropTypes.object,
     };
   }
 
   constructor(props) {
     super(props);
     this.isScrolledToBottom = this.isScrolledToBottom.bind(this);
     this.onHover = this.onHover.bind(this);
     this.onScroll = this.onScroll.bind(this);
     this.onKeyDown = this.onKeyDown.bind(this);
     this.onContextMenu = this.onContextMenu.bind(this);
     this.onFocusedNodeChange = this.onFocusedNodeChange.bind(this);
   }
 
   componentWillMount() {
-    const { dispatch, connector } = this.props;
+    const { connector, cloneSelectedRequest, openStatistics } = 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,
+      connector,
+      cloneSelectedRequest,
+      openStatistics,
     });
     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,
       interactive: true
     });
-
     // Install event handler to hide the tooltip on scroll
     this.refs.contentEl.addEventListener("scroll", this.onScroll, true);
   }
 
   componentWillUpdate(nextProps) {
     // Check if the list is scrolled to bottom before the UI update.
     // The scroll is ever needed only if new rows are added to the list.
     const delta = nextProps.displayedRequests.size - this.props.displayedRequests.size;
@@ -215,37 +214,37 @@ class RequestListContent extends Compone
       columns,
       displayedRequests,
       firstRequestStartedMillis,
       onCauseBadgeMouseDown,
       onItemMouseDown,
       onSecurityIconMouseDown,
       onWaterfallMouseDown,
       scale,
-      selectedRequestId,
+      selectedRequest,
     } = this.props;
 
     return (
-      div({ className: "requests-list-wrapper"},
-        div({ className: "requests-list-table"},
+      div({ className: "requests-list-wrapper" },
+        div({ className: "requests-list-table" },
           div({
             ref: "contentEl",
             className: "requests-list-contents",
             tabIndex: 0,
             onKeyDown: this.onKeyDown,
-            style: {"--timings-scale": scale, "--timings-rev-scale": 1 / scale}
+            style: { "--timings-scale": scale, "--timings-rev-scale": 1 / scale }
           },
             RequestListHeader(),
             displayedRequests.map((item, index) => RequestListItem({
               firstRequestStartedMillis,
               fromCache: item.status === "304" || item.fromCache,
               columns,
               item,
               index,
-              isSelected: item.id === selectedRequestId,
+              isSelected: item.id === (selectedRequest && selectedRequest.id),
               key: item.id,
               onContextMenu: this.onContextMenu,
               onFocusedNodeChange: this.onFocusedNodeChange,
               onMouseDown: () => onItemMouseDown(item.id),
               onCauseBadgeMouseDown: () => onCauseBadgeMouseDown(item.cause),
               onSecurityIconMouseDown: () => onSecurityIconMouseDown(item.securityState),
               onWaterfallMouseDown: () => onWaterfallMouseDown(),
             }))
@@ -256,21 +255,22 @@ class RequestListContent extends Compone
   }
 }
 
 module.exports = connect(
   (state) => ({
     columns: state.ui.columns,
     displayedRequests: getDisplayedRequests(state),
     firstRequestStartedMillis: state.requests.firstStartedMillis,
-    selectedRequestId: state.requests.selectedId,
+    selectedRequest: getSelectedRequest(state),
     scale: getWaterfallScale(state),
   }),
-  (dispatch) => ({
-    dispatch,
+  (dispatch, props) => ({
+    cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
+    openStatistics: (open) => dispatch(Actions.openStatistics(props.connector, open)),
     /**
      * A handler that opens the stack trace tab when a stack trace is available
      */
     onCauseBadgeMouseDown: (cause) => {
       if (cause.stacktrace && cause.stacktrace.length > 0) {
         dispatch(Actions.selectDetailsPanelTab("stack-trace"));
       }
     },
--- a/devtools/client/netmonitor/src/components/TabboxPanel.js
+++ b/devtools/client/netmonitor/src/components/TabboxPanel.js
@@ -53,17 +53,22 @@ function TabboxPanel({
       onSelect: selectTab,
       renderOnlySelected: true,
       showAllTabsMenu: true,
     },
       TabPanel({
         id: PANELS.HEADERS,
         title: HEADERS_TITLE,
       },
-        HeadersPanel({ request, cloneSelectedRequest, openLink }),
+        HeadersPanel({
+          cloneSelectedRequest,
+          connector,
+          openLink,
+          request,
+        }),
       ),
       TabPanel({
         id: PANELS.COOKIES,
         title: COOKIES_TITLE,
       },
         CookiesPanel({ request, openLink }),
       ),
       TabPanel({
--- a/devtools/client/netmonitor/src/connector/firefox-data-provider.js
+++ b/devtools/client/netmonitor/src/connector/firefox-data-provider.js
@@ -30,16 +30,17 @@ class FirefoxDataProvider {
     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);
+    this.getRequestFromQueue = this.getRequestFromQueue.bind(this);
 
     // Event handlers
     this.onNetworkEvent = this.onNetworkEvent.bind(this);
     this.onNetworkEventUpdate = this.onNetworkEventUpdate.bind(this);
   }
 
   /**
    * Add a new network request to application state.
@@ -252,19 +253,19 @@ class FirefoxDataProvider {
     // 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)
+        (record.responseHeaders && record.responseCookies) ||
+        payload.securityState == "broken" ||
+        (payload.responseContentAvailable && !payload.status)
       );
   }
 
   /**
    * Merge upcoming networkEventUpdate payload into existing one.
    *
    * @param {string} id request id
    * @param {object} payload request data payload
@@ -376,16 +377,23 @@ class FirefoxDataProvider {
 
     switch (updateType) {
       case "requestHeaders":
       case "requestCookies":
       case "responseHeaders":
       case "responseCookies":
         this.requestPayloadData(actor, updateType);
         break;
+      case "requestPostData":
+        this.updateRequest(actor, {
+          // This field helps knowing when/if requestPostData property is available
+          // and can be requested via `requestData`
+          requestPostDataAvailable: true,
+        });
+        break;
       case "securityInfo":
         this.updateRequest(actor, {
           securityState: networkInfo.securityInfo,
         }).then(() => {
           this.requestPayloadData(actor, updateType);
         });
         break;
       case "responseStart":
@@ -503,20 +511,22 @@ class FirefoxDataProvider {
     // 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);
+      let request = this.getRequestFromQueue(actor);
+      if (request) {
+        let { updateRequest } = this.actions;
+        if (updateRequest) {
+          await updateRequest(actor, request.payload, true);
+        }
       }
     });
     return promise;
   }
 
   /**
    * Internal helper used to request HTTP details from the backend.
    *
--- a/devtools/client/netmonitor/src/constants.js
+++ b/devtools/client/netmonitor/src/constants.js
@@ -116,16 +116,17 @@ const UPDATE_PROPS = [
   "totalTime",
   "eventTimings",
   "headersSize",
   "customQueryValue",
   "requestHeaders",
   "requestHeadersFromUploadStream",
   "requestCookies",
   "requestPostData",
+  "requestPostDataAvailable",
   "responseHeaders",
   "responseCookies",
   "responseContent",
   "responseContentAvailable",
   "formDataSections",
 ];
 
 const PANELS = {
--- a/devtools/client/netmonitor/src/har/har-builder.js
+++ b/devtools/client/netmonitor/src/har/har-builder.js
@@ -115,17 +115,17 @@ HarBuilder.prototype = {
   buildEntry: async function (log, file) {
     let page = this.getPage(log, file);
 
     let entry = {};
     entry.pageref = page.id;
     entry.startedDateTime = dateToJSON(new Date(file.startedMillis));
     entry.time = file.endedMillis - file.startedMillis;
 
-    entry.request = this.buildRequest(file);
+    entry.request = await this.buildRequest(file);
     entry.response = await this.buildResponse(file);
     entry.cache = this.buildCache(file);
     entry.timings = file.eventTimings ? file.eventTimings.timings : {};
 
     if (file.remoteAddress) {
       entry.serverIPAddress = file.remoteAddress;
     }
 
@@ -147,40 +147,34 @@ HarBuilder.prototype = {
     let timings = {
       onContentLoad: -1,
       onLoad: -1
     };
 
     return timings;
   },
 
-  buildRequest: function (file) {
+  buildRequest: async function (file) {
     let request = {
       bodySize: 0
     };
 
     request.method = file.method;
     request.url = file.url;
     request.httpVersion = file.httpVersion || "";
-
     request.headers = this.buildHeaders(file.requestHeaders);
     request.headers = this.appendHeadersPostData(request.headers, file);
     request.cookies = this.buildCookies(file.requestCookies);
-
     request.queryString = parseQueryString(getUrlQuery(file.url)) || [];
+    request.headersSize = file.requestHeaders.headersSize;
+    request.postData = await this.buildPostData(file);
 
-    if (file.requestPostData) {
-      request.postData = this.buildPostData(file);
-    }
-
-    request.headersSize = file.requestHeaders.headersSize;
-
-    // Set request body size, but make sure the body is fetched
-    // from the backend.
-    if (file.requestPostData) {
+    if (file.requestPostData && file.requestPostData.postData.text) {
+      // Set request body size, but make sure the body is fetched
+      // from the backend.
       this.fetchData(file.requestPostData.postData.text).then(value => {
         request.bodySize = value.length;
       });
     }
 
     return request;
   },
 
@@ -238,56 +232,66 @@ HarBuilder.prototype = {
           value: value
         });
       });
     });
 
     return result;
   },
 
-  buildPostData: function (file) {
+  buildPostData: async function (file) {
+    // When using HarAutomation, HarCollector will automatically fetch requestPostData,
+    // but when we use it from netmonitor, FirefoxDataProvider should fetch it itself
+    // lazily, via requestData.
+    let requestPostData = file.requestPostData;
+
+    if (!requestPostData && this._options.requestData) {
+      requestPostData = await this._options.requestData(file.id, "requestPostData");
+    }
+
+    if (!requestPostData.postData.text) {
+      return undefined;
+    }
+
     let postData = {
       mimeType: findValue(file.requestHeaders.headers, "content-type"),
       params: [],
       text: ""
     };
 
-    if (!file.requestPostData) {
-      return postData;
-    }
-
-    if (file.requestPostData.postDataDiscarded) {
+    if (requestPostData.postDataDiscarded) {
       postData.comment = L10N.getStr("har.requestBodyNotIncluded");
       return postData;
     }
-
     // Load request body from the backend.
-    this.fetchData(file.requestPostData.postData.text).then(postDataText => {
+    postData = await this.fetchData(requestPostData.postData.text).then(postDataText => {
       postData.text = postDataText;
 
       // If we are dealing with URL encoded body, parse parameters.
       let { headers } = file.requestHeaders;
       if (CurlUtils.isUrlEncodedRequest({ headers, postDataText })) {
         postData.mimeType = "application/x-www-form-urlencoded";
 
         // Extract form parameters and produce nice HAR array.
         getFormDataSections(
           file.requestHeaders,
           file.requestHeadersFromUploadStream,
-          file.requestPostData,
+          requestPostData,
           this._options.getString,
         ).then(formDataSections => {
           formDataSections.forEach(section => {
             let paramsArray = parseQueryString(section);
             if (paramsArray) {
               postData.params = [...postData.params, ...paramsArray];
             }
           });
         });
       }
+
+      return postData;
     });
 
     return postData;
   },
 
   buildResponse: async function (file) {
     let response = {
       status: 0
--- a/devtools/client/netmonitor/src/har/test/browser_net_har_copy_all_as_har.js
+++ b/devtools/client/netmonitor/src/har/test/browser_net_har_copy_all_as_har.js
@@ -14,28 +14,28 @@ add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
 
   info("Starting test... ");
 
   let { connector, store, windowRequire } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   let RequestListContextMenu = windowRequire(
     "devtools/client/netmonitor/src/request-list-context-menu");
-  let { getLongString, getTabTarget, requestData } = connector;
+  let { getSortedRequests } = windowRequire(
+    "devtools/client/netmonitor/src/selectors/index");
 
   store.dispatch(Actions.batchEnable(false));
 
   let wait = waitForNetworkEvents(monitor, 1);
   tab.linkedBrowser.reload();
   yield wait;
 
-  let contextMenu = new RequestListContextMenu({
-    getTabTarget, getLongString, requestData });
+  let contextMenu = new RequestListContextMenu({ connector });
 
-  yield contextMenu.copyAllAsHar();
+  yield contextMenu.copyAllAsHar(getSortedRequests(store.getState()));
 
   let jsonString = SpecialPowers.getClipboardData("text/unicode");
   let har = JSON.parse(jsonString);
 
   // Check out HAR log
   isnot(har.log, null, "The HAR log must exist");
   is(har.log.creator.name, "Firefox", "The creator field must be set");
   is(har.log.browser.name, "Firefox", "The browser field must be set");
--- a/devtools/client/netmonitor/src/har/test/browser_net_har_post_data.js
+++ b/devtools/client/netmonitor/src/har/test/browser_net_har_post_data.js
@@ -11,31 +11,31 @@ add_task(function* () {
     HAR_EXAMPLE_URL + "html_har_post-data-test-page.html");
 
   info("Starting test... ");
 
   let { connector, store, windowRequire } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   let RequestListContextMenu = windowRequire(
     "devtools/client/netmonitor/src/request-list-context-menu");
-  let { getLongString, getTabTarget, requestData } = connector;
+  let { getSortedRequests } = windowRequire(
+    "devtools/client/netmonitor/src/selectors/index");
 
   store.dispatch(Actions.batchEnable(false));
 
   // Execute one POST request on the page and wait till its done.
   let wait = waitForNetworkEvents(monitor, 0, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.executeTest();
   });
   yield wait;
 
   // Copy HAR into the clipboard (asynchronous).
-  let contextMenu = new RequestListContextMenu({
-    getTabTarget, getLongString, requestData });
-  let jsonString = yield contextMenu.copyAllAsHar();
+  let contextMenu = new RequestListContextMenu({ connector });
+  let jsonString = yield contextMenu.copyAllAsHar(getSortedRequests(store.getState()));
   let har = JSON.parse(jsonString);
 
   // Check out the HAR log.
   isnot(har.log, null, "The HAR log must exist");
   is(har.log.pages.length, 1, "There must be one page");
   is(har.log.entries.length, 1, "There must be one request");
 
   let entry = har.log.entries[0];
--- a/devtools/client/netmonitor/src/har/test/browser_net_har_post_data_on_get.js
+++ b/devtools/client/netmonitor/src/har/test/browser_net_har_post_data_on_get.js
@@ -11,37 +11,37 @@ add_task(function* () {
     HAR_EXAMPLE_URL + "html_har_post-data-test-page.html");
 
   info("Starting test... ");
 
   let { connector, store, windowRequire } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   let RequestListContextMenu = windowRequire(
     "devtools/client/netmonitor/src/request-list-context-menu");
-  let { getLongString, getTabTarget, requestData } = connector;
+  let { getSortedRequests } = windowRequire(
+    "devtools/client/netmonitor/src/selectors/index");
 
   store.dispatch(Actions.batchEnable(false));
 
   // Execute one GET request on the page and wait till its done.
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.executeTest3();
   });
   yield wait;
 
   // Copy HAR into the clipboard (asynchronous).
-  let contextMenu = new RequestListContextMenu({
-    getTabTarget, getLongString, requestData });
-  let jsonString = yield contextMenu.copyAllAsHar();
+  let contextMenu = new RequestListContextMenu({ connector });
+  let jsonString = yield contextMenu.copyAllAsHar(getSortedRequests(store.getState()));
   let har = JSON.parse(jsonString);
 
   // Check out the HAR log.
   isnot(har.log, null, "The HAR log must exist");
   is(har.log.pages.length, 1, "There must be one page");
   is(har.log.entries.length, 1, "There must be one request");
 
   let entry = har.log.entries[0];
-  is(entry.request.postData, undefined,
-    "Check post data is not present");
+
+  is(entry.request.postData, undefined, "Check post data is not present");
 
   // Clean up
   return teardown(monitor);
 });
--- a/devtools/client/netmonitor/src/har/test/browser_net_har_throttle_upload.js
+++ b/devtools/client/netmonitor/src/har/test/browser_net_har_throttle_upload.js
@@ -15,17 +15,18 @@ function* throttleUploadTest(actuallyThr
     HAR_EXAMPLE_URL + "html_har_post-data-test-page.html");
 
   info("Starting test... (actuallyThrottle = " + actuallyThrottle + ")");
 
   let { connector, store, windowRequire } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   let RequestListContextMenu = windowRequire(
     "devtools/client/netmonitor/src/request-list-context-menu");
-  let { getLongString, getTabTarget, setPreferences, requestData } = connector;
+  let { getSortedRequests } = windowRequire(
+    "devtools/client/netmonitor/src/selectors/index");
 
   store.dispatch(Actions.batchEnable(false));
 
   const size = 4096;
   const uploadSize = actuallyThrottle ? size / 3 : 0;
 
   const request = {
     "NetworkMonitor.throttleData": {
@@ -35,32 +36,31 @@ function* throttleUploadTest(actuallyThr
       downloadBPSMax: 200000,
       uploadBPSMean: uploadSize,
       uploadBPSMax: uploadSize,
     },
   };
 
   info("sending throttle request");
   yield new Promise((resolve) => {
-    setPreferences(request, (response) => {
+    connector.setPreferences(request, (response) => {
       resolve(response);
     });
   });
 
   // Execute one POST request on the page and wait till its done.
   let wait = waitForNetworkEvents(monitor, 0, 1);
   yield ContentTask.spawn(tab.linkedBrowser, { size }, function* (args) {
     content.wrappedJSObject.executeTest2(args.size);
   });
   yield wait;
 
   // Copy HAR into the clipboard (asynchronous).
-  let contextMenu = new RequestListContextMenu({
-    getTabTarget, getLongString, requestData });
-  let jsonString = yield contextMenu.copyAllAsHar();
+  let contextMenu = new RequestListContextMenu({ connector });
+  let jsonString = yield contextMenu.copyAllAsHar(getSortedRequests(store.getState()));
   let har = JSON.parse(jsonString);
 
   // Check out the HAR log.
   isnot(har.log, null, "The HAR log must exist");
   is(har.log.pages.length, 1, "There must be one page");
   is(har.log.entries.length, 1, "There must be one request");
 
   let entry = har.log.entries[0];
--- a/devtools/client/netmonitor/src/reducers/requests.js
+++ b/devtools/client/netmonitor/src/reducers/requests.js
@@ -50,16 +50,17 @@ const Request = I.Record({
   headersSize: undefined,
   // Text value is used for storing custom request query
   // which only appears when user edit the custom requst form
   customQueryValue: undefined,
   requestHeaders: undefined,
   requestHeadersFromUploadStream: undefined,
   requestCookies: undefined,
   requestPostData: undefined,
+  requestPostDataAvailable: false,
   responseHeaders: undefined,
   responseCookies: undefined,
   responseContent: undefined,
   responseContentAvailable: false,
   formDataSections: undefined,
 });
 
 const Requests = I.Record({
--- a/devtools/client/netmonitor/src/request-list-context-menu.js
+++ b/devtools/client/netmonitor/src/request-list-context-menu.js
@@ -12,398 +12,381 @@ const { copyString } = require("devtools
 const { showMenu } = require("devtools/client/netmonitor/src/utils/menu");
 const { HarExporter } = require("./har/har-exporter");
 const {
   getSelectedRequest,
   getSortedRequests,
 } = require("./selectors/index");
 const { L10N } = require("./utils/l10n");
 const {
+  formDataURI,
   getUrlQuery,
+  getUrlBaseName,
   parseQueryString,
-  getUrlBaseName,
-  formDataURI,
 } = 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;
-}
+class RequestListContextMenu {
+  constructor(props) {
+    this.props = props;
+  }
 
-RequestListContextMenu.prototype = {
-  get selectedRequest() {
+  open(event) {
     // FIXME: Bug 1336382 - Implement RequestListContextMenu React component
-    // Remove window.store
-    return getSelectedRequest(window.store.getState());
-  },
-
-  get sortedRequests() {
-    // FIXME: Bug 1336382 - Implement RequestListContextMenu React component
-    // Remove window.store
-    return getSortedRequests(window.store.getState());
-  },
-
-  /**
-   * Handle the context menu opening. Hide items if no request is selected.
-   * Since visible attribute only accept boolean value but the method call may
-   * return undefined, we use !! to force convert any object to boolean
-   */
-  async open(event = {}) {
-    let selectedRequest = this.selectedRequest;
+    // Remove window.store.getState()
+    let selectedRequest = getSelectedRequest(window.store.getState());
+    let sortedRequests = getSortedRequests(window.store.getState());
 
     let menu = [];
     let copySubmenu = [];
-    let menuItemVisible = !!selectedRequest;
-    let [
-      requestPostData,
-      responseContent,
-    ] = await Promise.all([
-      this.requestData(selectedRequest.id, "requestPostData"),
-      this.requestData(selectedRequest.id, "responseContent"),
-    ]);
+    let {
+      id,
+      isCustom,
+      method,
+      mimeType,
+      httpVersion,
+      requestHeaders,
+      requestPostDataAvailable,
+      responseHeaders,
+      responseContentAvailable,
+      url,
+    } = selectedRequest || {};
+    let {
+      cloneSelectedRequest,
+      openStatistics,
+    } = this.props;
 
     copySubmenu.push({
       id: "request-list-context-copy-url",
       label: L10N.getStr("netmonitor.context.copyUrl"),
       accesskey: L10N.getStr("netmonitor.context.copyUrl.accesskey"),
-      visible: menuItemVisible,
-      click: () => this.copyUrl(),
+      visible: !!selectedRequest,
+      click: () => this.copyUrl(url),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-url-params",
       label: L10N.getStr("netmonitor.context.copyUrlParams"),
       accesskey: L10N.getStr("netmonitor.context.copyUrlParams.accesskey"),
-      visible: !!getUrlQuery(selectedRequest.url),
-      click: () => this.copyUrlParams(),
+      visible: !!(selectedRequest && getUrlQuery(url)),
+      click: () => this.copyUrlParams(url),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-post-data",
       label: L10N.getStr("netmonitor.context.copyPostData"),
       accesskey: L10N.getStr("netmonitor.context.copyPostData.accesskey"),
-      visible: !!requestPostData,
-      click: () => this.copyPostData(requestPostData),
+      visible: !!(selectedRequest && requestPostDataAvailable),
+      click: () => this.copyPostData(id),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-as-curl",
       label: L10N.getStr("netmonitor.context.copyAsCurl"),
       accesskey: L10N.getStr("netmonitor.context.copyAsCurl.accesskey"),
-      visible: menuItemVisible,
-      click: () => this.copyAsCurl(),
+      visible: !!selectedRequest,
+      click: () => this.copyAsCurl(id, url, method, requestHeaders, httpVersion),
     });
 
     copySubmenu.push({
       type: "separator",
-      visible: menuItemVisible,
+      visible: copySubmenu.slice(0, 4).some((subMenu) => subMenu.visible),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-request-headers",
       label: L10N.getStr("netmonitor.context.copyRequestHeaders"),
       accesskey: L10N.getStr("netmonitor.context.copyRequestHeaders.accesskey"),
-      visible: !!selectedRequest.requestHeaders,
-      click: () => this.copyRequestHeaders(),
+      visible: !!(selectedRequest && requestHeaders && requestHeaders.rawHeaders),
+      click: () => this.copyRequestHeaders(requestHeaders.rawHeaders.trim()),
     });
 
     copySubmenu.push({
       id: "response-list-context-copy-response-headers",
       label: L10N.getStr("netmonitor.context.copyResponseHeaders"),
       accesskey: L10N.getStr("netmonitor.context.copyResponseHeaders.accesskey"),
-      visible: !!selectedRequest.responseHeaders,
-      click: () => this.copyResponseHeaders(),
+      visible: !!(selectedRequest && responseHeaders && responseHeaders.rawHeaders),
+      click: () => this.copyResponseHeaders(responseHeaders.rawHeaders.trim()),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-response",
       label: L10N.getStr("netmonitor.context.copyResponse"),
       accesskey: L10N.getStr("netmonitor.context.copyResponse.accesskey"),
-      visible: !!selectedRequest.responseContentAvailable,
-      click: () => this.copyResponse(responseContent),
+      visible: !!(selectedRequest && responseContentAvailable),
+      click: () => this.copyResponse(id),
     });
 
     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.mimeType &&
-        selectedRequest.mimeType.includes("image/")),
-      click: () => this.copyImageAsDataUri(responseContent),
+      visible: !!(selectedRequest && mimeType && mimeType.includes("image/")),
+      click: () => this.copyImageAsDataUri(id, mimeType),
     });
 
     copySubmenu.push({
       type: "separator",
-      visible: menuItemVisible,
+      visible: copySubmenu.slice(5, 9).some((subMenu) => subMenu.visible),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-all-as-har",
       label: L10N.getStr("netmonitor.context.copyAllAsHar"),
       accesskey: L10N.getStr("netmonitor.context.copyAllAsHar.accesskey"),
-      visible: this.sortedRequests.size > 0,
-      click: () => this.copyAllAsHar(),
+      visible: sortedRequests.size > 0,
+      click: () => this.copyAllAsHar(sortedRequests),
     });
 
     menu.push({
       label: L10N.getStr("netmonitor.context.copy"),
       accesskey: L10N.getStr("netmonitor.context.copy.accesskey"),
-      visible: menuItemVisible,
+      visible: !!selectedRequest,
       submenu: copySubmenu,
     });
 
     menu.push({
       id: "request-list-context-save-all-as-har",
       label: L10N.getStr("netmonitor.context.saveAllAsHar"),
       accesskey: L10N.getStr("netmonitor.context.saveAllAsHar.accesskey"),
-      visible: this.sortedRequests.size > 0,
-      click: () => this.saveAllAsHar(),
+      visible: sortedRequests.size > 0,
+      click: () => this.saveAllAsHar(sortedRequests),
     });
 
     menu.push({
       id: "request-list-context-save-image-as",
       label: L10N.getStr("netmonitor.context.saveImageAs"),
       accesskey: L10N.getStr("netmonitor.context.saveImageAs.accesskey"),
-      visible: !!(selectedRequest.mimeType &&
-        selectedRequest.mimeType.includes("image/")),
-      click: () => this.saveImageAs(responseContent),
+      visible: !!(selectedRequest && mimeType && mimeType.includes("image/")),
+      click: () => this.saveImageAs(id, url),
     });
 
     menu.push({
       type: "separator",
-      visible: !selectedRequest.isCustom,
+      visible: copySubmenu.slice(10, 14).some((subMenu) => subMenu.visible),
     });
 
     menu.push({
       id: "request-list-context-resend",
       label: L10N.getStr("netmonitor.context.editAndResend"),
       accesskey: L10N.getStr("netmonitor.context.editAndResend.accesskey"),
-      visible: !selectedRequest.isCustom,
-      click: this.cloneSelectedRequest,
+      visible: !!(selectedRequest && !isCustom),
+      click: cloneSelectedRequest,
     });
 
     menu.push({
       type: "separator",
-      visible: menuItemVisible,
+      visible: copySubmenu.slice(15, 16).some((subMenu) => subMenu.visible),
     });
 
     menu.push({
       id: "request-list-context-newtab",
       label: L10N.getStr("netmonitor.context.newTab"),
       accesskey: L10N.getStr("netmonitor.context.newTab.accesskey"),
-      visible: menuItemVisible,
-      click: () => this.openRequestInTab(),
+      visible: !!selectedRequest,
+      click: () => this.openRequestInTab(url),
     });
 
     menu.push({
       id: "request-list-context-open-in-debugger",
       label: L10N.getStr("netmonitor.context.openInDebugger"),
       accesskey: L10N.getStr("netmonitor.context.openInDebugger.accesskey"),
-      visible: !!(selectedRequest.mimeType &&
-        selectedRequest.mimeType.includes("javascript")),
-      click: () => this.openInDebugger(),
+      visible: !!(selectedRequest && mimeType && mimeType.includes("javascript")),
+      click: () => this.openInDebugger(url),
     });
 
     menu.push({
       id: "request-list-context-open-in-style-editor",
       label: L10N.getStr("netmonitor.context.openInStyleEditor"),
       accesskey: L10N.getStr("netmonitor.context.openInStyleEditor.accesskey"),
-      visible: !!(Services.prefs.getBoolPref("devtools.styleeditor.enabled") &&
-        selectedRequest.mimeType &&
-        selectedRequest.mimeType.includes("css")),
-      click: () => this.openInStyleEditor(),
+      visible: !!(selectedRequest &&
+        Services.prefs.getBoolPref("devtools.styleeditor.enabled") &&
+        mimeType && mimeType.includes("css")),
+      click: () => this.openInStyleEditor(url),
     });
 
     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,
-      click: () => this.openStatistics(true),
+      visible: sortedRequests.size > 0,
+      click: () => openStatistics(true),
     });
 
     showMenu(event, menu);
-  },
+  }
 
   /**
    * Opens selected item in a new tab.
    */
-  openRequestInTab() {
+  openRequestInTab(url) {
     let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
-    win.openUILinkIn(this.selectedRequest.url, "tab", { relatedToCurrent: true });
-  },
+    win.openUILinkIn(url, "tab", { relatedToCurrent: true });
+  }
 
   /**
    * Opens selected item in the debugger
    */
-  openInDebugger() {
-    let toolbox = gDevTools.getToolbox(this.getTabTarget());
-    toolbox.viewSourceInDebugger(this.selectedRequest.url, 0);
-  },
+  openInDebugger(url) {
+    let toolbox = gDevTools.getToolbox(this.props.connector.getTabTarget());
+    toolbox.viewSourceInDebugger(url, 0);
+  }
 
   /**
    * Opens selected item in the style editor
    */
-  openInStyleEditor() {
-    let toolbox = gDevTools.getToolbox(this.getTabTarget());
-    toolbox.viewSourceInStyleEditor(this.selectedRequest.url, 0);
-  },
+  openInStyleEditor(url) {
+    let toolbox = gDevTools.getToolbox(this.props.connector.getTabTarget());
+    toolbox.viewSourceInStyleEditor(url, 0);
+  }
 
   /**
    * Copy the request url from the currently selected item.
    */
-  copyUrl() {
-    copyString(this.selectedRequest.url);
-  },
+  copyUrl(url) {
+    copyString(url);
+  }
 
   /**
    * Copy the request url query string parameters from the currently
    * selected item.
    */
-  copyUrlParams() {
-    let params = getUrlQuery(this.selectedRequest.url).split("&");
+  copyUrlParams(url) {
+    let params = getUrlQuery(url).split("&");
     copyString(params.join(Services.appinfo.OS === "WINNT" ? "\r\n" : "\n"));
-  },
+  }
 
   /**
    * Copy the request form data parameters (or raw payload) from
    * the currently selected item.
    */
-  copyPostData(requestPostData) {
-    let { formDataSections } = this.selectedRequest;
+  async copyPostData(id) {
+    // FIXME: Bug 1336382 - Implement RequestListContextMenu React component
+    // Remove window.store.getState()
+    let { formDataSections } = getSelectedRequest(window.store.getState());
     let params = [];
     // Try to extract any form data parameters.
     formDataSections.forEach(section => {
       let paramsArray = parseQueryString(section);
       if (paramsArray) {
         params = [...params, ...paramsArray];
       }
     });
 
     let string = params
       .map(param => param.name + (param.value ? "=" + param.value : ""))
       .join(Services.appinfo.OS === "WINNT" ? "\r\n" : "\n");
 
     // Fall back to raw payload.
     if (!string) {
+      let requestPostData = await this.props.connector.requestData(id, "requestPostData");
       string = requestPostData.postData.text;
       if (Services.appinfo.OS !== "WINNT") {
         string = string.replace(/\r/g, "");
       }
     }
     copyString(string);
-  },
+  }
 
   /**
    * Copy a cURL command from the currently selected item.
    */
-  copyAsCurl() {
-    let selected = this.selectedRequest;
+  async copyAsCurl(id, url, method, requestHeaders, httpVersion) {
+    let requestPostData = await this.props.connector.requestData(id, "requestPostData");
     // Create a sanitized object for the Curl command generator.
     let data = {
-      url: selected.url,
-      method: selected.method,
-      headers: selected.requestHeaders.headers,
-      httpVersion: selected.httpVersion,
-      postDataText: selected.requestPostData && selected.requestPostData.postData.text,
+      url,
+      method,
+      headers: requestHeaders.headers,
+      httpVersion: httpVersion,
+      postDataText: requestPostData ? requestPostData.postData.text : "",
     };
     copyString(Curl.generateCommand(data));
-  },
+  }
 
   /**
    * Copy the raw request headers from the currently selected item.
    */
-  copyRequestHeaders() {
-    let rawHeaders = this.selectedRequest.requestHeaders.rawHeaders.trim();
+  copyRequestHeaders(rawHeaders) {
     if (Services.appinfo.OS !== "WINNT") {
       rawHeaders = rawHeaders.replace(/\r/g, "");
     }
     copyString(rawHeaders);
-  },
+  }
 
   /**
    * Copy the raw response headers from the currently selected item.
    */
-  copyResponseHeaders() {
-    let rawHeaders = this.selectedRequest.responseHeaders.rawHeaders.trim();
+  copyResponseHeaders(rawHeaders) {
     if (Services.appinfo.OS !== "WINNT") {
       rawHeaders = rawHeaders.replace(/\r/g, "");
     }
     copyString(rawHeaders);
-  },
+  }
 
   /**
    * Copy image as data uri.
    */
-  copyImageAsDataUri(responseContent) {
-    let { mimeType } = this.selectedRequest;
+  async copyImageAsDataUri(id, mimeType) {
+    let responseContent = await this.props.connector.requestData(id, "responseContent");
     let { encoding, text } = responseContent.content;
-    let src = formDataURI(mimeType, encoding, text);
-    copyString(src);
-  },
+    copyString(formDataURI(mimeType, encoding, text));
+  }
 
   /**
    * Save image as.
    */
-  saveImageAs(responseContent) {
+  async saveImageAs(id, url) {
+    let responseContent = await this.props.connector.requestData(id, "responseContent");
     let { encoding, text } = responseContent.content;
-    let fileName = getUrlBaseName(this.selectedRequest.url);
+    let fileName = getUrlBaseName(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);
       }
     } else {
       data = text;
     }
     saveAs(new Blob([data]), fileName, document);
-  },
+  }
 
   /**
    * Copy response data as a string.
    */
-  copyResponse(responseContent) {
+  async copyResponse(id) {
+    let responseContent = await this.props.connector.requestData(id, "responseContent");
     copyString(responseContent.content.text);
-  },
+  }
 
   /**
    * Copy HAR from the network panel content to the clipboard.
    */
-  copyAllAsHar() {
-    return HarExporter.copy(this.getDefaultHarOptions());
-  },
+  copyAllAsHar(sortedRequests) {
+    return HarExporter.copy(this.getDefaultHarOptions(sortedRequests));
+  }
 
   /**
    * Save HAR from the network panel content to a file.
    */
-  saveAllAsHar() {
-    // FIXME: This will not work in launchpad
+  saveAllAsHar(sortedRequests) {
+    // This will not work in launchpad
     // document.execCommand(‘cut’/‘copy’) was denied because it was not called from
     // inside a short running user-generated event handler.
     // https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Interact_with_the_clipboard
-    return HarExporter.save(this.getDefaultHarOptions());
-  },
+    return HarExporter.save(this.getDefaultHarOptions(sortedRequests));
+  }
 
-  getDefaultHarOptions() {
-    let form = this.getTabTarget().form;
-    let title = form.title || form.url;
+  getDefaultHarOptions(sortedRequests) {
+    let { getLongString, getTabTarget, requestData } = this.props.connector;
+    let { form: { title, url } } = getTabTarget();
 
     return {
-      requestData: this.requestData,
-      getString: this.getLongString,
-      items: this.sortedRequests,
-      title: title
+      getString: getLongString,
+      items: sortedRequests,
+      requestData,
+      title: title || url,
     };
   }
-};
+}
 
 module.exports = RequestListContextMenu;
--- a/devtools/client/netmonitor/test/browser_net_curl-utils.js
+++ b/devtools/client/netmonitor/test/browser_net_curl-utils.js
@@ -13,49 +13,52 @@ add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CURL_UTILS_URL);
   info("Starting test... ");
 
   let { store, windowRequire, connector } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   let {
     getSortedRequests,
   } = windowRequire("devtools/client/netmonitor/src/selectors/index");
-  let { getLongString } = connector;
+  let {
+    getLongString,
+    requestData,
+  } = connector;
 
   store.dispatch(Actions.batchEnable(false));
 
   let wait = waitForNetworkEvents(monitor, 1, 3);
   yield ContentTask.spawn(tab.linkedBrowser, SIMPLE_SJS, function* (url) {
     content.wrappedJSObject.performRequests(url);
   });
   yield wait;
 
   let requests = {
     get: getSortedRequests(store.getState()).get(0),
     post: getSortedRequests(store.getState()).get(1),
     multipart: getSortedRequests(store.getState()).get(2),
     multipartForm: getSortedRequests(store.getState()).get(3),
   };
 
-  let data = yield createCurlData(requests.get, getLongString);
+  let data = yield createCurlData(requests.get, getLongString, requestData);
   testFindHeader(data);
 
-  data = yield createCurlData(requests.post, getLongString);
+  data = yield createCurlData(requests.post, getLongString, requestData);
   testIsUrlEncodedRequest(data);
   testWritePostDataTextParams(data);
   testWriteEmptyPostDataTextParams(data);
   testDataArgumentOnGeneratedCommand(data);
 
-  data = yield createCurlData(requests.multipart, getLongString);
+  data = yield createCurlData(requests.multipart, getLongString, requestData);
   testIsMultipartRequest(data);
   testGetMultipartBoundary(data);
   testMultiPartHeaders(data);
   testRemoveBinaryDataFromMultipartText(data);
 
-  data = yield createCurlData(requests.multipartForm, getLongString);
+  data = yield createCurlData(requests.multipartForm, getLongString, requestData);
   testMultiPartHeaders(data);
 
   testGetHeadersFromMultipartText({
     postDataText: "Content-Type: text/plain\r\n\r\n",
   });
 
   if (Services.appinfo.OS != "WINNT") {
     testEscapeStringPosix();
@@ -226,17 +229,17 @@ function testEscapeStringWin() {
     "Backslashes should be escaped.");
 
   let newLines = "line1\r\nline2\r\nline3";
   is(CurlUtils.escapeStringWin(newLines),
     '"line1"^\u000d\u000A"line2"^\u000d\u000A"line3"',
     "Newlines should be escaped.");
 }
 
-function* createCurlData(selected, getLongString) {
+function* createCurlData(selected, getLongString, requestData) {
   let { url, method, httpVersion } = selected;
 
   // Create a sanitized object for the Curl command generator.
   let data = {
     url,
     method,
     headers: [],
     httpVersion,
@@ -244,16 +247,17 @@ function* createCurlData(selected, getLo
   };
 
   // Fetch header values.
   for (let { name, value } of selected.requestHeaders.headers) {
     let text = yield getLongString(value);
     data.headers.push({ name: name, value: text });
   }
 
+  let requestPostData = yield requestData(selected.id, "requestPostData");
   // Fetch the request payload.
-  if (selected.requestPostData) {
-    let postData = selected.requestPostData.postData.text;
+  if (requestPostData) {
+    let postData = requestPostData.postData.text;
     data.postDataText = yield getLongString(postData);
   }
 
   return data;
 }
--- a/devtools/client/netmonitor/test/browser_net_open_in_debugger.js
+++ b/devtools/client/netmonitor/test/browser_net_open_in_debugger.js
@@ -7,33 +7,34 @@
  * Test the 'Open in debugger' feature
  */
 
 add_task(function* () {
   let { tab, monitor, toolbox} = yield initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL);
   info("Starting test... ");
 
   let { document, store, windowRequire } = monitor.panelWin;
-
+  let contextMenuDoc = monitor.panelWin.parent.document;
   // Avoid async processing
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   store.dispatch(Actions.batchEnable(false));
 
   let wait = waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
+  wait = waitForDOM(contextMenuDoc, "#request-list-context-open-in-debugger");
   EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelectorAll(".request-list-item")[2]);
   EventUtils.sendMouseEvent({ type: "contextmenu" },
     document.querySelectorAll(".request-list-item")[2]);
+  yield wait;
 
   let onDebuggerReady = toolbox.once("jsdebugger-ready");
-  monitor.panelWin.parent.document
-    .querySelector("#request-list-context-open-in-debugger").click();
+  contextMenuDoc.querySelector("#request-list-context-open-in-debugger").click();
   yield onDebuggerReady;
 
   ok(true, "Debugger has been open");
 
   yield teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_open_in_style_editor.js
+++ b/devtools/client/netmonitor/test/browser_net_open_in_style_editor.js
@@ -7,31 +7,33 @@
  * Test the 'Open in debugger' feature
  */
 
 add_task(function* () {
   let { tab, monitor, toolbox} = yield initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL);
   info("Starting test... ");
 
   let { document, store, windowRequire } = monitor.panelWin;
-
+  let contextMenuDoc = monitor.panelWin.parent.document;
   // Avoid async processing
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   store.dispatch(Actions.batchEnable(false));
 
   let wait = waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
+  wait = waitForDOM(contextMenuDoc, "#request-list-context-open-in-style-editor");
   EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelectorAll(".request-list-item")[1]);
   EventUtils.sendMouseEvent({ type: "contextmenu" },
     document.querySelectorAll(".request-list-item")[1]);
+  yield wait;
 
   let onStyleEditorReady = toolbox.once("styleeditor-ready");
   monitor.panelWin.parent.document
     .querySelector("#request-list-context-open-in-style-editor").click();
   yield onStyleEditorReady;
 
   ok(true, "Style Editor has been open");
 
--- a/devtools/client/netmonitor/test/browser_net_open_request_in_tab.js
+++ b/devtools/client/netmonitor/test/browser_net_open_request_in_tab.js
@@ -7,30 +7,33 @@
  * Tests if Open in new tab works.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
   info("Starting test...");
 
   let { document, store, windowRequire } = monitor.panelWin;
+  let contextMenuDoc = monitor.panelWin.parent.document;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
 
   store.dispatch(Actions.batchEnable(false));
 
   let wait = waitForNetworkEvents(monitor, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests(1);
   });
   yield wait;
 
+  wait = waitForDOM(contextMenuDoc, "#request-list-context-newtab");
   EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelectorAll(".request-list-item")[0]);
   EventUtils.sendMouseEvent({ type: "contextmenu" },
     document.querySelectorAll(".request-list-item")[0]);
+  yield wait;
 
   let onTabOpen = once(gBrowser.tabContainer, "TabOpen", false);
   monitor.panelWin.parent.document
     .querySelector("#request-list-context-newtab").click();
   yield onTabOpen;
 
   ok(true, "A new tab has been opened");
 
--- a/devtools/client/netmonitor/test/browser_net_post-data-03.js
+++ b/devtools/client/netmonitor/test/browser_net_post-data-03.js
@@ -21,25 +21,24 @@ add_task(function* () {
 
   let wait = waitForNetworkEvents(monitor, 0, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
   // Wait for all tree view updated by react
-  wait = waitForDOM(document, "#headers-panel");
+  wait = waitForDOM(document, "#headers-panel .tree-section .treeLabel", 3);
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelector(".network-details-panel-toggle"));
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelector("#headers-tab"));
   yield wait;
 
   let tabpanel = document.querySelector("#headers-panel");
-
   is(tabpanel.querySelectorAll(".tree-section .treeLabel").length, 3,
     "There should be 3 header sections displayed in this tabpanel.");
 
   is(tabpanel.querySelectorAll(".tree-section .treeLabel")[2].textContent,
     L10N.getStr("requestHeadersFromUpload") + " (" +
     L10N.getFormatStr("networkMenu.sizeB", 74) + ")",
     "The request headers from upload section doesn't have the correct title.");
 
--- a/devtools/client/netmonitor/test/browser_net_resend.js
+++ b/devtools/client/netmonitor/test/browser_net_resend.js
@@ -12,16 +12,17 @@ const ADD_HEADER = "Test-header: true";
 const ADD_UA_HEADER = "User-Agent: Custom-Agent";
 const ADD_POSTDATA = "&t3=t4";
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(POST_DATA_URL);
   info("Starting test... ");
 
   let { document, store, windowRequire, connector } = monitor.panelWin;
+  let { requestData } = connector;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   let {
     getSelectedRequest,
     getSortedRequests,
   } = windowRequire("devtools/client/netmonitor/src/selectors/index");
 
   store.dispatch(Actions.batchEnable(false));
 
@@ -50,17 +51,17 @@ add_task(function* () {
   testCustomItemChanged(customItem, origItem);
 
   // send the new request
   wait = waitForNetworkEvents(monitor, 0, 1);
   store.dispatch(Actions.sendCustomRequest(connector));
   yield wait;
 
   let sentItem = getSelectedRequest(store.getState());
-  testSentRequest(sentItem, origItem);
+  testSentRequest(sentItem, origItem, requestData);
 
   return teardown(monitor);
 
   function testCustomItem(item, orig) {
     is(item.method, orig.method, "item is showing the same method as original request");
     is(item.url, orig.url, "item is showing the same URL as original request");
   }
 
@@ -137,28 +138,30 @@ add_task(function* () {
 
     // add to POST data
     type(ADD_POSTDATA);
   }
 
   /*
    * Make sure newly created event matches expected request
    */
-  function testSentRequest(data, origData) {
+  function* testSentRequest(data, origData) {
     is(data.method, origData.method, "correct method in sent request");
     is(data.url, origData.url + "&" + ADD_QUERY, "correct url in sent request");
 
     let { headers } = data.requestHeaders;
     let hasHeader = headers.some(h => `${h.name}: ${h.value}` == ADD_HEADER);
     ok(hasHeader, "new header added to sent request");
 
     let hasUAHeader = headers.some(h => `${h.name}: ${h.value}` == ADD_UA_HEADER);
     ok(hasUAHeader, "User-Agent header added to sent request");
 
-    is(data.requestPostData.postData.text,
+    let requestPostData = yield requestData(data.id, "requestPostData");
+
+    is(requestPostData.postData.text,
        origData.requestPostData.postData.text + ADD_POSTDATA,
        "post data added to sent request");
   }
 
   function type(string) {
     for (let ch of string) {
       EventUtils.synthesizeKey(ch, {}, monitor.panelWin);
     }
--- a/devtools/client/netmonitor/test/browser_net_resend_cors.js
+++ b/devtools/client/netmonitor/test/browser_net_resend_cors.js
@@ -58,20 +58,21 @@ add_task(function* () {
   // Check the resent requests
   for (let i = 0; i < ITEMS.length; i++) {
     let item = ITEMS[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
+      // Force fetching lazy load data
       let responseContent = yield connector.requestData(item.id, "responseContent");
+      let requestPostData = yield connector.requestData(item.id, "requestPostData");
 
-      is(item.requestPostData.postData.text, "post-data",
+      is(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: *",
         "The POST response has the right content");
     }
   }
 
   info("Finishing the test");
--- a/devtools/client/netmonitor/test/head.js
+++ b/devtools/client/netmonitor/test/head.js
@@ -305,18 +305,16 @@ function waitForNetworkEvents(monitor, g
     let genericEvents = 0;
     let postEvents = 0;
     let payloadReady = 0;
     let awaitedEventsToListeners = [
       ["UPDATING_REQUEST_HEADERS", onGenericEvent],
       ["RECEIVED_REQUEST_HEADERS", onGenericEvent],
       ["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],
       ["UPDATING_EVENT_TIMINGS", onGenericEvent],
       ["RECEIVED_EVENT_TIMINGS", onGenericEvent],
       ["PAYLOAD_READY", onPayloadReady]
     ];
--- a/devtools/client/webconsole/webconsole-connection-proxy.js
+++ b/devtools/client/webconsole/webconsole-connection-proxy.js
@@ -239,17 +239,19 @@ WebConsoleConnectionProxy.prototype = {
   /**
    * Dispatch a message event on the new frontend and emit an event for tests.
    */
   dispatchMessageUpdate: function (networkInfo, response) {
     this.webConsoleFrame.newConsoleOutput.dispatchMessageUpdate(networkInfo, response);
   },
 
   dispatchRequestUpdate: function (id, data) {
-    this.webConsoleFrame.newConsoleOutput.dispatchRequestUpdate(id, data);
+    if (this.webConsoleFrame) {
+      this.webConsoleFrame.newConsoleOutput.dispatchRequestUpdate(id, data);
+    }
   },
 
   /**
    * The "cachedMessages" response handler.
    *
    * @private
    * @param object response
    *        The JSON response object received from the server.