Bug 1466598 - [Network-Monitor] Right click on method enables request details tab. r=Honza
authortanhengyeow <E0032242@u.nus.edu>
Fri, 17 May 2019 07:01:05 +0000
changeset 536184 d5a1f414d631aa02482f1c650387bc14c5e31b62
parent 536183 7fdd2abe918b6ba220dd097d27d1a2cda6cfa8e6
child 536185 366103f41d7ddaaaaba111db727f4db3b51ef6bd
push id2082
push userffxbld-merge
push dateMon, 01 Jul 2019 08:34:18 +0000
treeherdermozilla-release@2fb19d0466d2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersHonza
bugs1466598
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1466598 - [Network-Monitor] Right click on method enables request details tab. r=Honza Show context menu properly Differential Revision: https://phabricator.services.mozilla.com/D29126
devtools/client/netmonitor/src/actions/requests.js
devtools/client/netmonitor/src/components/RequestListContent.js
devtools/client/netmonitor/src/constants.js
devtools/client/netmonitor/src/reducers/requests.js
devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
devtools/client/netmonitor/test/browser_net_edit_resend_cancel.js
devtools/client/netmonitor/test/browser_net_edit_resend_with_filtering.js
devtools/client/netmonitor/test/browser_net_edit_resend_xhr.js
--- a/devtools/client/netmonitor/src/actions/requests.js
+++ b/devtools/client/netmonitor/src/actions/requests.js
@@ -2,18 +2,20 @@
  * 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 {
   ADD_REQUEST,
   CLEAR_REQUESTS,
+  CLONE_REQUEST,
   CLONE_SELECTED_REQUEST,
   REMOVE_SELECTED_CUSTOM_REQUEST,
+  RIGHT_CLICK_REQUEST,
   SEND_CUSTOM_REQUEST,
   TOGGLE_RECORDING,
   UPDATE_REQUEST,
 } = require("../constants");
 const { getSelectedRequest, getRequestById } = require("../selectors/index");
 
 function addRequest(id, data, batch) {
   return {
@@ -29,16 +31,37 @@ function updateRequest(id, data, batch) 
     type: UPDATE_REQUEST,
     id,
     data,
     meta: { batch },
   };
 }
 
 /**
+ * Clone request by id. Used when cloning a request
+ * through the "Edit and Resend" option present in the context menu.
+ */
+function cloneRequest(id) {
+  return {
+    id,
+    type: CLONE_REQUEST,
+  };
+}
+
+/**
+ * Right click a request without selecting it.
+ */
+function rightClickRequest(id) {
+  return {
+    id,
+    type: RIGHT_CLICK_REQUEST,
+  };
+}
+
+/**
  * Clone the currently selected request, set the "isCustom" attribute.
  * Used by the "Edit and Resend" feature.
  */
 function cloneSelectedRequest() {
   return {
     type: CLONE_SELECTED_REQUEST,
   };
 }
@@ -138,15 +161,17 @@ function toggleRecording() {
     type: TOGGLE_RECORDING,
   };
 }
 
 module.exports = {
   addRequest,
   blockSelectedRequestURL,
   clearRequests,
+  cloneRequest,
   cloneSelectedRequest,
+  rightClickRequest,
   removeSelectedCustomRequest,
   sendCustomRequest,
   toggleRecording,
   unblockSelectedRequestURL,
   updateRequest,
 };
--- a/devtools/client/netmonitor/src/components/RequestListContent.js
+++ b/devtools/client/netmonitor/src/components/RequestListContent.js
@@ -39,40 +39,47 @@ const { div } = dom;
 
 // Tooltip show / hide delay in ms
 const REQUESTS_TOOLTIP_TOGGLE_DELAY = 500;
 // Tooltip image maximum dimension in px
 const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400;
 // Gecko's scrollTop is int32_t, so the maximum value is 2^31 - 1 = 2147483647
 const MAX_SCROLL_HEIGHT = 2147483647;
 
+const LEFT_MOUSE_BUTTON = 0;
+const RIGHT_MOUSE_BUTTON = 2;
+
 /**
  * Renders the actual contents of the request list.
  */
 class RequestListContent extends Component {
   static get propTypes() {
     return {
       blockSelectedRequestURL: PropTypes.func.isRequired,
       connector: PropTypes.object.isRequired,
       columns: PropTypes.object.isRequired,
       networkDetailsOpen: PropTypes.bool.isRequired,
       networkDetailsWidth: PropTypes.number,
       networkDetailsHeight: PropTypes.number,
-      cloneSelectedRequest: PropTypes.func.isRequired,
+      cloneRequest: PropTypes.func.isRequired,
+      clickedRequest: PropTypes.object,
+      openDetailsPanelTab: PropTypes.func.isRequired,
       sendCustomRequest: PropTypes.func.isRequired,
       displayedRequests: PropTypes.array.isRequired,
       firstRequestStartedMillis: PropTypes.number.isRequired,
       fromCache: PropTypes.bool,
       onCauseBadgeMouseDown: PropTypes.func.isRequired,
+      onItemRightMouseButtonDown: 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,
+      selectRequest: PropTypes.func.isRequired,
       selectedRequest: PropTypes.object,
       unblockSelectedRequestURL: PropTypes.func.isRequired,
       requestFilterTypes: PropTypes.object.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
@@ -80,16 +87,17 @@ class RequestListContent extends Compone
     this.onHover = this.onHover.bind(this);
     this.onScroll = this.onScroll.bind(this);
     this.onResize = this.onResize.bind(this);
     this.onKeyDown = this.onKeyDown.bind(this);
     this.openRequestInTab = this.openRequestInTab.bind(this);
     this.onDoubleClick = this.onDoubleClick.bind(this);
     this.onContextMenu = this.onContextMenu.bind(this);
     this.onFocusedNodeChange = this.onFocusedNodeChange.bind(this);
+    this.onMouseDown = this.onMouseDown.bind(this);
   }
 
   componentWillMount() {
     this.tooltip = new HTMLTooltip(window.parent.document, { type: "arrow" });
     window.addEventListener("resize", this.onResize);
   }
 
   componentDidMount() {
@@ -211,16 +219,24 @@ class RequestListContent extends Compone
 
   /**
    * Scroll listener for the requests menu view.
    */
   onScroll() {
     this.tooltip.hide();
   }
 
+  onMouseDown(evt, id) {
+    if (evt.button === LEFT_MOUSE_BUTTON) {
+      this.props.selectRequest(id);
+    } else if (evt.button === RIGHT_MOUSE_BUTTON) {
+      this.props.onItemRightMouseButtonDown(id);
+    }
+  }
+
   /**
    * Handler for keyboard events. For arrow up/down, page up/down, home/end,
    * move the selection up or down.
    */
   onKeyDown(evt) {
     let delta;
 
     switch (evt.key) {
@@ -268,39 +284,41 @@ class RequestListContent extends Compone
   }
 
   onDoubleClick({ id, url, requestHeaders, requestPostData }) {
     this.openRequestInTab(id, url, requestHeaders, requestPostData);
   }
 
   onContextMenu(evt) {
     evt.preventDefault();
-    const { selectedRequest, displayedRequests } = this.props;
+    const { clickedRequest, displayedRequests } = this.props;
 
     if (!this.contextMenu) {
       const {
         blockSelectedRequestURL,
         connector,
-        cloneSelectedRequest,
+        cloneRequest,
+        openDetailsPanelTab,
         sendCustomRequest,
         openStatistics,
         unblockSelectedRequestURL,
       } = this.props;
       this.contextMenu = new RequestListContextMenu({
         blockSelectedRequestURL,
         connector,
-        cloneSelectedRequest,
+        cloneRequest,
+        openDetailsPanelTab,
         sendCustomRequest,
         openStatistics,
         openRequestInTab: this.openRequestInTab,
         unblockSelectedRequestURL,
       });
     }
 
-    this.contextMenu.open(evt, selectedRequest, displayedRequests);
+    this.contextMenu.open(evt, clickedRequest, displayedRequests);
   }
 
   /**
    * If selection has just changed (by keyboard navigation), don't keep the list
    * scrolled to bottom, but allow scrolling up with the selection.
    */
   onFocusedNodeChange() {
     this.shouldScrollBottom = false;
@@ -308,17 +326,16 @@ class RequestListContent extends Compone
 
   render() {
     const {
       connector,
       columns,
       displayedRequests,
       firstRequestStartedMillis,
       onCauseBadgeMouseDown,
-      onItemMouseDown,
       onSecurityIconMouseDown,
       onWaterfallMouseDown,
       requestFilterTypes,
       scale,
       selectedRequest,
     } = this.props;
 
     return (
@@ -345,17 +362,17 @@ class RequestListContent extends Compone
               columns,
               item,
               index,
               isSelected: item.id === (selectedRequest && selectedRequest.id),
               key: item.id,
               onContextMenu: this.onContextMenu,
               onFocusedNodeChange: this.onFocusedNodeChange,
               onDoubleClick: () => this.onDoubleClick(item),
-              onMouseDown: () => onItemMouseDown(item.id),
+              onMouseDown: (evt) => this.onMouseDown(evt, item.id),
               onCauseBadgeMouseDown: () => onCauseBadgeMouseDown(item.cause),
               onSecurityIconMouseDown: () => onSecurityIconMouseDown(item.securityState),
               onWaterfallMouseDown: () => onWaterfallMouseDown(),
               requestFilterTypes,
             }))
           ) // end of requests-list-row-group">
         )
       )
@@ -364,40 +381,44 @@ class RequestListContent extends Compone
 }
 
 module.exports = connect(
   (state) => ({
     columns: state.ui.columns,
     networkDetailsOpen: state.ui.networkDetailsOpen,
     networkDetailsWidth: state.ui.networkDetailsWidth,
     networkDetailsHeight: state.ui.networkDetailsHeight,
+    clickedRequest: state.requests.clickedRequest,
     displayedRequests: getDisplayedRequests(state),
     firstRequestStartedMillis: state.requests.firstStartedMillis,
     selectedRequest: getSelectedRequest(state),
     scale: getWaterfallScale(state),
     requestFilterTypes: state.filters.requestFilterTypes,
   }),
   (dispatch, props) => ({
     blockSelectedRequestURL: () => {
       dispatch(Actions.blockSelectedRequestURL(props.connector));
     },
-    cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
+    cloneRequest: (id) => dispatch(Actions.cloneRequest(id)),
+    openDetailsPanelTab: () => dispatch(Actions.openNetworkDetails(true)),
     sendCustomRequest: () => dispatch(Actions.sendCustomRequest(props.connector)),
     openStatistics: (open) => dispatch(Actions.openStatistics(props.connector, open)),
     unblockSelectedRequestURL: () => {
       dispatch(Actions.unblockSelectedRequestURL(props.connector));
     },
     /**
      * 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"));
       }
     },
+    selectRequest: (id) => dispatch(Actions.selectRequest(id)),
+    onItemRightMouseButtonDown: (id) => dispatch(Actions.rightClickRequest(id)),
     onItemMouseDown: (id) => dispatch(Actions.selectRequest(id)),
     /**
      * A handler that opens the security tab in the details view if secure or
      * broken security indicator is clicked.
      */
     onSecurityIconMouseDown: (securityState) => {
       if (securityState && securityState !== "insecure") {
         dispatch(Actions.selectDetailsPanelTab("security"));
--- a/devtools/client/netmonitor/src/constants.js
+++ b/devtools/client/netmonitor/src/constants.js
@@ -7,20 +7,22 @@
 const actionTypes = {
   ADD_REQUEST: "ADD_REQUEST",
   ADD_TIMING_MARKER: "ADD_TIMING_MARKER",
   BATCH_ACTIONS: "BATCH_ACTIONS",
   BATCH_ENABLE: "BATCH_ENABLE",
   BATCH_FLUSH: "BATCH_FLUSH",
   CLEAR_REQUESTS: "CLEAR_REQUESTS",
   CLEAR_TIMING_MARKERS: "CLEAR_TIMING_MARKERS",
+  CLONE_REQUEST: "CLONE_REQUEST",
   CLONE_SELECTED_REQUEST: "CLONE_SELECTED_REQUEST",
   ENABLE_REQUEST_FILTER_TYPE_ONLY: "ENABLE_REQUEST_FILTER_TYPE_ONLY",
   OPEN_NETWORK_DETAILS: "OPEN_NETWORK_DETAILS",
   RESIZE_NETWORK_DETAILS: "RESIZE_NETWORK_DETAILS",
+  RIGHT_CLICK_REQUEST: "RIGHT_CLICK_REQUEST",
   ENABLE_PERSISTENT_LOGS: "ENABLE_PERSISTENT_LOGS",
   DISABLE_BROWSER_CACHE: "DISABLE_BROWSER_CACHE",
   OPEN_STATISTICS: "OPEN_STATISTICS",
   REMOVE_SELECTED_CUSTOM_REQUEST: "REMOVE_SELECTED_CUSTOM_REQUEST",
   RESET_COLUMNS: "RESET_COLUMNS",
   SELECT_REQUEST: "SELECT_REQUEST",
   SELECT_DETAILS_PANEL_TAB: "SELECT_DETAILS_PANEL_TAB",
   SEND_CUSTOM_REQUEST: "SEND_CUSTOM_REQUEST",
--- a/devtools/client/netmonitor/src/reducers/requests.js
+++ b/devtools/client/netmonitor/src/reducers/requests.js
@@ -6,19 +6,21 @@
 
 const {
   getUrlDetails,
   processNetworkUpdates,
 } = require("../utils/request-utils");
 const {
   ADD_REQUEST,
   CLEAR_REQUESTS,
+  CLONE_REQUEST,
   CLONE_SELECTED_REQUEST,
   OPEN_NETWORK_DETAILS,
   REMOVE_SELECTED_CUSTOM_REQUEST,
+  RIGHT_CLICK_REQUEST,
   SELECT_REQUEST,
   SEND_CUSTOM_REQUEST,
   TOGGLE_RECORDING,
   UPDATE_REQUEST,
 } = require("../constants");
 
 /**
  * This structure stores list of all HTTP requests received
@@ -106,52 +108,41 @@ function requestsReducer(state = Request
       return {
         ...Requests(),
         recording: state.recording,
       };
     }
 
     // Select specific request.
     case SELECT_REQUEST: {
+      // Selected request represents the last request that was clicked
+      // before the context menu is shown
+      const clickedRequest = state.requests.get(action.id);
       return {
         ...state,
+        clickedRequest,
         selectedId: action.id,
       };
     }
 
     // Clone selected request for re-send.
-    case CLONE_SELECTED_REQUEST: {
-      const { requests, selectedId } = state;
-
-      if (!selectedId) {
-        return state;
-      }
-
-      const clonedRequest = requests.get(selectedId);
-      if (!clonedRequest) {
-        return state;
-      }
+    case CLONE_REQUEST: {
+      return cloneRequest(state, action.id);
+    }
 
-      const newRequest = {
-        id: clonedRequest.id + "-clone",
-        cause: clonedRequest.cause,
-        method: clonedRequest.method,
-        url: clonedRequest.url,
-        urlDetails: clonedRequest.urlDetails,
-        requestHeaders: clonedRequest.requestHeaders,
-        requestPostData: clonedRequest.requestPostData,
-        requestPostDataAvailable: clonedRequest.requestPostDataAvailable,
-        isCustom: true,
-      };
+    case CLONE_SELECTED_REQUEST: {
+      return cloneRequest(state, state.selectedId);
+    }
 
+    case RIGHT_CLICK_REQUEST: {
+      const { requests } = state;
+      const clickedRequest = requests.get(action.id);
       return {
         ...state,
-        requests: mapSet(requests, newRequest.id, newRequest),
-        selectedId: newRequest.id,
-        preselectedId: selectedId,
+        clickedRequest,
       };
     }
 
     // Removing temporary cloned request (created for re-send, but canceled).
     case REMOVE_SELECTED_CUSTOM_REQUEST: {
       return closeCustomRequest(state);
     }
 
@@ -189,16 +180,48 @@ function requestsReducer(state = Request
 
     default:
       return state;
   }
 }
 
 // Helpers
 
+function cloneRequest(state, id) {
+  const { requests } = state;
+
+  if (!id) {
+    return state;
+  }
+
+  const clonedRequest = requests.get(id);
+  if (!clonedRequest) {
+    return state;
+  }
+
+  const newRequest = {
+    id: clonedRequest.id + "-clone",
+    method: clonedRequest.method,
+    cause: clonedRequest.cause,
+    url: clonedRequest.url,
+    urlDetails: clonedRequest.urlDetails,
+    requestHeaders: clonedRequest.requestHeaders,
+    requestPostData: clonedRequest.requestPostData,
+    requestPostDataAvailable: clonedRequest.requestPostDataAvailable,
+    isCustom: true,
+  };
+
+  return {
+    ...state,
+    requests: mapSet(requests, newRequest.id, newRequest),
+    selectedId: newRequest.id,
+    preselectedId: id,
+  };
+}
+
 /**
  * Remove the currently selected custom request.
  */
 function closeCustomRequest(state) {
   const { requests, selectedId, preselectedId } = state;
 
   if (!selectedId) {
     return state;
--- a/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
+++ b/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
@@ -20,17 +20,17 @@ loader.lazyRequireGetter(this, "copyStri
 loader.lazyRequireGetter(this, "showMenu", "devtools/client/shared/components/menu/utils", true);
 loader.lazyRequireGetter(this, "HarMenuUtils", "devtools/client/netmonitor/src/har/har-menu-utils", true);
 
 class RequestListContextMenu {
   constructor(props) {
     this.props = props;
   }
 
-  open(event, selectedRequest, requests) {
+  open(event, clickedRequest, requests) {
     const {
       id,
       blockedReason,
       isCustom,
       formDataSections,
       method,
       mimeType,
       httpVersion,
@@ -38,115 +38,116 @@ class RequestListContextMenu {
       requestHeadersAvailable,
       requestPostData,
       requestPostDataAvailable,
       responseHeaders,
       responseHeadersAvailable,
       responseContent,
       responseContentAvailable,
       url,
-    } = selectedRequest;
+    } = clickedRequest;
     const {
       blockSelectedRequestURL,
       connector,
-      cloneSelectedRequest,
+      cloneRequest,
+      openDetailsPanelTab,
       sendCustomRequest,
       openStatistics,
       openRequestInTab,
       unblockSelectedRequestURL,
     } = this.props;
     const menu = [];
     const copySubmenu = [];
 
     copySubmenu.push({
       id: "request-list-context-copy-url",
       label: L10N.getStr("netmonitor.context.copyUrl"),
       accesskey: L10N.getStr("netmonitor.context.copyUrl.accesskey"),
-      visible: !!selectedRequest,
+      visible: !!clickedRequest,
       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: !!(selectedRequest && getUrlQuery(url)),
+      visible: !!(clickedRequest && getUrlQuery(url)),
       click: () => this.copyUrlParams(url),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-post-data",
       label: L10N.getFormatStr("netmonitor.context.copyRequestData", method),
       accesskey: L10N.getStr("netmonitor.context.copyRequestData.accesskey"),
       // Menu item will be visible even if data hasn't arrived, so we need to check
       // *Available property and then fetch data lazily once user triggers the action.
-      visible: !!(selectedRequest && (requestPostDataAvailable || requestPostData)),
+      visible: !!(clickedRequest && (requestPostDataAvailable || requestPostData)),
       click: () => this.copyPostData(id, formDataSections, requestPostData),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-as-curl",
       label: L10N.getStr("netmonitor.context.copyAsCurl"),
       accesskey: L10N.getStr("netmonitor.context.copyAsCurl.accesskey"),
       // Menu item will be visible even if data hasn't arrived, so we need to check
       // *Available property and then fetch data lazily once user triggers the action.
-      visible: !!selectedRequest,
+      visible: !!clickedRequest,
       click: () =>
         this.copyAsCurl(id, url, method, httpVersion, requestHeaders, requestPostData),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-as-fetch",
       label: L10N.getStr("netmonitor.context.copyAsFetch"),
       accesskey: L10N.getStr("netmonitor.context.copyAsFetch.accesskey"),
-      visible: !!selectedRequest,
+      visible: !!clickedRequest,
       click: () =>
         this.copyAsFetch(id, url, method, requestHeaders, requestPostData),
     });
 
     copySubmenu.push({
       type: "separator",
       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"),
       // Menu item will be visible even if data hasn't arrived, so we need to check
       // *Available property and then fetch data lazily once user triggers the action.
-      visible: !!(selectedRequest && (requestHeadersAvailable || requestHeaders)),
+      visible: !!(clickedRequest && (requestHeadersAvailable || requestHeaders)),
       click: () => this.copyRequestHeaders(id, requestHeaders),
     });
 
     copySubmenu.push({
       id: "response-list-context-copy-response-headers",
       label: L10N.getStr("netmonitor.context.copyResponseHeaders"),
       accesskey: L10N.getStr("netmonitor.context.copyResponseHeaders.accesskey"),
       // Menu item will be visible even if data hasn't arrived, so we need to check
       // *Available property and then fetch data lazily once user triggers the action.
-      visible: !!(selectedRequest && (responseHeadersAvailable || responseHeaders)),
+      visible: !!(clickedRequest && (responseHeadersAvailable || responseHeaders)),
       click: () => this.copyResponseHeaders(id, responseHeaders),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-response",
       label: L10N.getStr("netmonitor.context.copyResponse"),
       accesskey: L10N.getStr("netmonitor.context.copyResponse.accesskey"),
       // Menu item will be visible even if data hasn't arrived, so we need to check
       // *Available property and then fetch data lazily once user triggers the action.
-      visible: !!(selectedRequest && (responseContentAvailable || responseContent)),
+      visible: !!(clickedRequest && (responseContentAvailable || responseContent)),
       click: () => this.copyResponse(id, responseContent),
     });
 
     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 && (responseContentAvailable || responseContent) &&
+      visible: !!(clickedRequest && (responseContentAvailable || responseContent) &&
         mimeType && mimeType.includes("image/")),
       click: () => this.copyImageAsDataUri(id, mimeType, responseContent),
     });
 
     copySubmenu.push({
       type: "separator",
       visible: copySubmenu.slice(5, 9).some((subMenu) => subMenu.visible),
     });
@@ -157,98 +158,106 @@ class RequestListContextMenu {
       accesskey: L10N.getStr("netmonitor.context.copyAllAsHar.accesskey"),
       visible: requests.length > 0,
       click: () => HarMenuUtils.copyAllAsHar(requests, connector),
     });
 
     menu.push({
       label: L10N.getStr("netmonitor.context.copy"),
       accesskey: L10N.getStr("netmonitor.context.copy.accesskey"),
-      visible: !!selectedRequest,
+      visible: !!clickedRequest,
       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: requests.length > 0,
       click: () => HarMenuUtils.saveAllAsHar(requests, connector),
     });
 
     menu.push({
       id: "request-list-context-save-image-as",
       label: L10N.getStr("netmonitor.context.saveImageAs"),
       accesskey: L10N.getStr("netmonitor.context.saveImageAs.accesskey"),
-      visible: !!(selectedRequest && (responseContentAvailable || responseContent) &&
+      visible: !!(clickedRequest && (responseContentAvailable || responseContent) &&
         mimeType && mimeType.includes("image/")),
       click: () => this.saveImageAs(id, url, responseContent),
     });
 
     menu.push({
       type: "separator",
       visible: copySubmenu.slice(10, 14).some((subMenu) => subMenu.visible),
     });
 
     menu.push({
       id: "request-list-context-resend-only",
       label: L10N.getStr("netmonitor.context.resend.label"),
       accesskey: L10N.getStr("netmonitor.context.resend.accesskey"),
-      visible: !!(selectedRequest && !isCustom),
-      click: sendCustomRequest,
+      visible: !!(clickedRequest && !isCustom),
+      click: () => {
+        cloneRequest(id);
+        sendCustomRequest();
+      },
     });
 
     menu.push({
       id: "request-list-context-resend",
       label: L10N.getStr("netmonitor.context.editAndResend"),
       accesskey: L10N.getStr("netmonitor.context.editAndResend.accesskey"),
-      visible: !!(selectedRequest && !isCustom),
-      click: cloneSelectedRequest,
+      visible: !!(clickedRequest && !isCustom),
+      click: () => {
+        this.fetchRequestHeaders(id).then(() => {
+          cloneRequest(id);
+          openDetailsPanelTab();
+        });
+      },
     });
 
     menu.push({
       id: "request-list-context-block-url",
       label: L10N.getStr("netmonitor.context.blockURL"),
-      visible: !!(selectedRequest && !blockedReason),
+      visible: !!(clickedRequest && !blockedReason),
       click: blockSelectedRequestURL,
     });
 
     menu.push({
       id: "request-list-context-unblock-url",
       label: L10N.getStr("netmonitor.context.unblockURL"),
-      visible: !!(selectedRequest && blockedReason),
+      visible: !!(clickedRequest && blockedReason),
       click: unblockSelectedRequestURL,
     });
 
     menu.push({
       type: "separator",
       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: !!selectedRequest,
+      visible: !!clickedRequest,
       click: () => openRequestInTab(id, url, requestHeaders, requestPostData),
     });
 
     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 && mimeType.includes("javascript")),
+      visible: !!(clickedRequest && 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: !!(selectedRequest &&
+      visible: !!(clickedRequest &&
         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"),
@@ -260,17 +269,17 @@ class RequestListContextMenu {
     menu.push({
       type: "separator",
     });
 
     menu.push({
       id: "request-list-context-use-as-fetch",
       label: L10N.getStr("netmonitor.context.useAsFetch"),
       accesskey: L10N.getStr("netmonitor.context.useAsFetch.accesskey"),
-      visible: !!selectedRequest,
+      visible: !!clickedRequest,
       click: () =>
         this.useAsFetch(id, url, method, requestHeaders, requestPostData),
     });
 
     showMenu(menu, {
       screenX: event.screenX,
       screenY: event.screenY,
     });
@@ -531,11 +540,15 @@ class RequestListContextMenu {
    * Copy response data as a string.
    */
   async copyResponse(id, responseContent) {
     responseContent = responseContent ||
       await this.props.connector.requestData(id, "responseContent");
 
     copyString(responseContent.content.text);
   }
+
+  async fetchRequestHeaders(id) {
+    await this.props.connector.requestData(id, "requestHeaders");
+  }
 }
 
 module.exports = RequestListContextMenu;
--- a/devtools/client/netmonitor/test/browser_net_edit_resend_cancel.js
+++ b/devtools/client/netmonitor/test/browser_net_edit_resend_cancel.js
@@ -29,20 +29,21 @@ add_task(async function() {
   EventUtils.sendMouseEvent({ type: "mousedown" }, firstRequest);
   await waitForHeaders;
   EventUtils.sendMouseEvent({ type: "contextmenu" }, firstRequest);
   const firstRequestState = getSelectedRequest(store.getState());
   const contextResend = getContextMenuItem(monitor, "request-list-context-resend");
   contextResend.click();
 
   // Waits for "Edit & Resend" panel to appear > New request "Cancel"
+  await waitUntil(() => document.querySelector(".custom-request-panel"));
   document.querySelector("#custom-request-close-button").click();
   const finalRequestState = getSelectedRequest(store.getState());
 
-  ok(firstRequestState === finalRequestState,
+  ok(firstRequestState.id === finalRequestState.id,
     "Original request is selected after cancel button is clicked"
   );
 
   ok(document.querySelector(".headers-overview") !== null,
     "Request is selected and headers panel is visible"
   );
 
   return teardown(monitor);
--- a/devtools/client/netmonitor/test/browser_net_edit_resend_with_filtering.js
+++ b/devtools/client/netmonitor/test/browser_net_edit_resend_with_filtering.js
@@ -6,49 +6,58 @@
 "use strict";
 
 /**
  * Tests if resending a XHR request while filtering XHR displays
  * the correct requests
  */
 add_task(async function() {
   const { tab, monitor } = await initNetMonitor(POST_RAW_URL);
-
   const { document, store, windowRequire } = monitor.panelWin;
+  const {
+    getSelectedRequest,
+  } = windowRequire("devtools/client/netmonitor/src/selectors/index");
   const Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   store.dispatch(Actions.batchEnable(false));
 
   // Execute XHR request and filter by XHR
   await performRequests(monitor, tab, 1);
   document.querySelector(".requests-list-filter-xhr-button").click();
 
   // Confirm XHR request and click it
   const xhrRequestItem = document.querySelectorAll(".request-list-item")[0];
   EventUtils.sendMouseEvent({ type: "mousedown" }, xhrRequestItem);
-
-  const {
-    getSelectedRequest,
-  } = windowRequire("devtools/client/netmonitor/src/selectors/index");
+  const waitForHeaders = waitUntil(() => document.querySelector(".headers-overview"));
+  await waitForHeaders;
   const firstRequest = getSelectedRequest(store.getState());
 
   // Open context menu and execute "Edit & Resend".
   EventUtils.sendMouseEvent({ type: "contextmenu" }, xhrRequestItem);
   getContextMenuItem(monitor, "request-list-context-resend").click();
 
-  // Click Resend
+  // Wait for "Edit & Resend" panel to appear
   await waitUntil(() => document.querySelector("#custom-request-send-button"));
+
+  // Select the temporary clone-request and check its ID
+  // it should be calculated from the original request
+  // by appending '-clone' suffix.
+  document.querySelectorAll(".request-list-item")[1].click();
+  const cloneRequest = getSelectedRequest(store.getState());
+
+  ok(cloneRequest.id.replace(/-clone$/, "") == firstRequest.id,
+    "The second XHR request is a clone of the first");
+
+  // Click the "Send" button and wait till the new request appears in the list
   document.querySelector("#custom-request-send-button").click();
+  await waitForNetworkEvents(monitor, 1);
 
   // Filtering by "other" so the resent request is visible after completion
   document.querySelector(".requests-list-filter-other-button").click();
 
-  // Select the cloned request
+  // Select the new (cloned) request
   document.querySelectorAll(".request-list-item")[0].click();
   const resendRequest = getSelectedRequest(store.getState());
 
   ok(resendRequest.id !== firstRequest.id,
     "The second XHR request was made and is unique");
 
-  ok(resendRequest.id.replace(/-clone$/, "") == firstRequest.id,
-    "The second XHR request is a clone of the first");
-
   return teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_edit_resend_xhr.js
+++ b/devtools/client/netmonitor/test/browser_net_edit_resend_xhr.js
@@ -26,18 +26,22 @@ add_task(async function() {
   const { getSelectedRequest }
   = windowRequire("devtools/client/netmonitor/src/selectors/index");
   const original = getSelectedRequest(store.getState());
 
   // Context Menu > "Edit & Resend"
   EventUtils.sendMouseEvent({ type: "contextmenu" }, xhrRequest);
   getContextMenuItem(monitor, "request-list-context-resend").click();
 
-  // Waits for "Edit & Resend" panel to appear > New request "Send"
+  // 1) Wait for "Edit & Resend" panel to appear
+  // 2) Click the "Send" button
+  // 3) Wait till the new request appears in the list
+  await waitUntil(() => document.querySelector(".custom-request-panel"));
   document.querySelector("#custom-request-send-button").click();
+  await waitForNetworkEvents(monitor, 1);
 
   // Selects cloned request
   const clonedRequest = document.querySelectorAll(".request-list-item")[1];
   EventUtils.sendMouseEvent({ type: "mousedown" }, clonedRequest);
   const cloned = getSelectedRequest(store.getState());
 
   // Compares if the requests have the same cause type (XHR)
   ok(original.cause.type === cloned.cause.type,