Bug 1315175 - Move Netmonitor context menu code to a separate module r?Honza draft
authorJarda Snajdr <jsnajdr@gmail.com>
Fri, 04 Nov 2016 16:30:06 +0100
changeset 434764 18132a5ccbcf1f3cf1b3efd85d10a45023765fa0
parent 434636 908557c762f798605a2f96e4c943791cbada1b50
child 536113 a8f6a8f8bdd90a1ada8fa5a829cc610fe0882cdd
push id34822
push userbmo:jsnajdr@gmail.com
push dateMon, 07 Nov 2016 10:03:00 +0000
reviewersHonza
bugs1315175
milestone52.0a1
Bug 1315175 - Move Netmonitor context menu code to a separate module r?Honza MozReview-Commit-ID: LH6WvyAjiow
devtools/client/netmonitor/har/test/browser_net_har_copy_all_as_har.js
devtools/client/netmonitor/har/test/browser_net_har_post_data.js
devtools/client/netmonitor/har/test/browser_net_har_throttle_upload.js
devtools/client/netmonitor/moz.build
devtools/client/netmonitor/request-list-context-menu.js
devtools/client/netmonitor/requests-menu-view.js
devtools/client/netmonitor/test/browser_net_copy_as_curl.js
devtools/client/netmonitor/test/browser_net_copy_headers.js
devtools/client/netmonitor/test/browser_net_copy_image_as_data_uri.js
devtools/client/netmonitor/test/browser_net_copy_params.js
devtools/client/netmonitor/test/browser_net_copy_response.js
devtools/client/netmonitor/test/browser_net_copy_svg_image_as_data_uri.js
devtools/client/netmonitor/test/browser_net_copy_url.js
devtools/client/netmonitor/test/browser_net_open_request_in_tab.js
devtools/client/netmonitor/test/head.js
--- a/devtools/client/netmonitor/har/test/browser_net_har_copy_all_as_har.js
+++ b/devtools/client/netmonitor/har/test/browser_net_har_copy_all_as_har.js
@@ -15,17 +15,17 @@ add_task(function* () {
   let { RequestsMenu } = NetMonitorView;
 
   RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 1);
   tab.linkedBrowser.reload();
   yield wait;
 
-  yield RequestsMenu.copyAllAsHar();
+  yield RequestsMenu.contextMenu.copyAllAsHar();
 
   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/har/test/browser_net_har_post_data.js
+++ b/devtools/client/netmonitor/har/test/browser_net_har_post_data.js
@@ -20,17 +20,17 @@ add_task(function* () {
   // 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 jsonString = yield RequestsMenu.copyAllAsHar();
+  let jsonString = yield RequestsMenu.contextMenu.copyAllAsHar();
   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/har/test/browser_net_har_throttle_upload.js
+++ b/devtools/client/netmonitor/har/test/browser_net_har_throttle_upload.js
@@ -46,17 +46,17 @@ function* throttleUploadTest(actuallyThr
   // 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 jsonString = yield RequestsMenu.copyAllAsHar();
+  let jsonString = yield RequestsMenu.contextMenu.copyAllAsHar();
   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/moz.build
+++ b/devtools/client/netmonitor/moz.build
@@ -15,16 +15,17 @@ DevToolsModules(
     'constants.js',
     'custom-request-view.js',
     'events.js',
     'filter-predicates.js',
     'l10n.js',
     'panel.js',
     'performance-statistics-view.js',
     'prefs.js',
+    'request-list-context-menu.js',
     'request-utils.js',
     'requests-menu-view.js',
     'sort-predicates.js',
     'store.js',
     'toolbar-view.js',
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/request-list-context-menu.js
@@ -0,0 +1,357 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* globals NetMonitorController, NetMonitorView, gNetwork */
+
+"use strict";
+
+const Services = require("Services");
+const { Task } = require("devtools/shared/task");
+const { Curl } = require("devtools/client/shared/curl");
+const { gDevTools } = require("devtools/client/framework/devtools");
+const Menu = require("devtools/client/framework/menu");
+const MenuItem = require("devtools/client/framework/menu-item");
+const { L10N } = require("./l10n");
+const { formDataURI, getFormDataSections } = require("./request-utils");
+
+loader.lazyRequireGetter(this, "HarExporter",
+  "devtools/client/netmonitor/har/har-exporter", true);
+
+loader.lazyServiceGetter(this, "clipboardHelper",
+  "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
+
+loader.lazyRequireGetter(this, "NetworkHelper",
+  "devtools/shared/webconsole/network-helper");
+
+function RequestListContextMenu() {}
+
+RequestListContextMenu.prototype = {
+  get selectedItem() {
+    return NetMonitorView.RequestsMenu.selectedItem;
+  },
+
+  get items() {
+    return NetMonitorView.RequestsMenu.items;
+  },
+
+  /**
+   * 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
+   */
+  open({ screenX = 0, screenY = 0 } = {}) {
+    let selectedItem = this.selectedItem;
+
+    let menu = new Menu();
+    menu.append(new MenuItem({
+      id: "request-menu-context-copy-url",
+      label: L10N.getStr("netmonitor.context.copyUrl"),
+      accesskey: L10N.getStr("netmonitor.context.copyUrl.accesskey"),
+      visible: !!selectedItem,
+      click: () => this.copyUrl(),
+    }));
+
+    menu.append(new MenuItem({
+      id: "request-menu-context-copy-url-params",
+      label: L10N.getStr("netmonitor.context.copyUrlParams"),
+      accesskey: L10N.getStr("netmonitor.context.copyUrlParams.accesskey"),
+      visible: !!(selectedItem &&
+               NetworkHelper.nsIURL(selectedItem.attachment.url).query),
+      click: () => this.copyUrlParams(),
+    }));
+
+    menu.append(new MenuItem({
+      id: "request-menu-context-copy-post-data",
+      label: L10N.getStr("netmonitor.context.copyPostData"),
+      accesskey: L10N.getStr("netmonitor.context.copyPostData.accesskey"),
+      visible: !!(selectedItem && selectedItem.attachment.requestPostData),
+      click: () => this.copyPostData(),
+    }));
+
+    menu.append(new MenuItem({
+      id: "request-menu-context-copy-as-curl",
+      label: L10N.getStr("netmonitor.context.copyAsCurl"),
+      accesskey: L10N.getStr("netmonitor.context.copyAsCurl.accesskey"),
+      visible: !!(selectedItem && selectedItem.attachment),
+      click: () => this.copyAsCurl(),
+    }));
+
+    menu.append(new MenuItem({
+      type: "separator",
+      visible: !!selectedItem,
+    }));
+
+    menu.append(new MenuItem({
+      id: "request-menu-context-copy-request-headers",
+      label: L10N.getStr("netmonitor.context.copyRequestHeaders"),
+      accesskey: L10N.getStr("netmonitor.context.copyRequestHeaders.accesskey"),
+      visible: !!(selectedItem && selectedItem.attachment.requestHeaders),
+      click: () => this.copyRequestHeaders(),
+    }));
+
+    menu.append(new MenuItem({
+      id: "response-menu-context-copy-response-headers",
+      label: L10N.getStr("netmonitor.context.copyResponseHeaders"),
+      accesskey: L10N.getStr("netmonitor.context.copyResponseHeaders.accesskey"),
+      visible: !!(selectedItem && selectedItem.attachment.responseHeaders),
+      click: () => this.copyResponseHeaders(),
+    }));
+
+    menu.append(new MenuItem({
+      id: "request-menu-context-copy-response",
+      label: L10N.getStr("netmonitor.context.copyResponse"),
+      accesskey: L10N.getStr("netmonitor.context.copyResponse.accesskey"),
+      visible: !!(selectedItem &&
+               selectedItem.attachment.responseContent &&
+               selectedItem.attachment.responseContent.content.text &&
+               selectedItem.attachment.responseContent.content.text.length !== 0),
+      click: () => this.copyResponse(),
+    }));
+
+    menu.append(new MenuItem({
+      id: "request-menu-context-copy-image-as-data-uri",
+      label: L10N.getStr("netmonitor.context.copyImageAsDataUri"),
+      accesskey: L10N.getStr("netmonitor.context.copyImageAsDataUri.accesskey"),
+      visible: !!(selectedItem &&
+               selectedItem.attachment.responseContent &&
+               selectedItem.attachment.responseContent.content
+                 .mimeType.includes("image/")),
+      click: () => this.copyImageAsDataUri(),
+    }));
+
+    menu.append(new MenuItem({
+      type: "separator",
+      visible: !!selectedItem,
+    }));
+
+    menu.append(new MenuItem({
+      id: "request-menu-context-copy-all-as-har",
+      label: L10N.getStr("netmonitor.context.copyAllAsHar"),
+      accesskey: L10N.getStr("netmonitor.context.copyAllAsHar.accesskey"),
+      visible: !!this.items.length,
+      click: () => this.copyAllAsHar(),
+    }));
+
+    menu.append(new MenuItem({
+      id: "request-menu-context-save-all-as-har",
+      label: L10N.getStr("netmonitor.context.saveAllAsHar"),
+      accesskey: L10N.getStr("netmonitor.context.saveAllAsHar.accesskey"),
+      visible: !!this.items.length,
+      click: () => this.saveAllAsHar(),
+    }));
+
+    menu.append(new MenuItem({
+      type: "separator",
+      visible: !!selectedItem,
+    }));
+
+    menu.append(new MenuItem({
+      id: "request-menu-context-resend",
+      label: L10N.getStr("netmonitor.context.editAndResend"),
+      accesskey: L10N.getStr("netmonitor.context.editAndResend.accesskey"),
+      visible: !!(NetMonitorController.supportsCustomRequest &&
+               selectedItem &&
+               !selectedItem.attachment.isCustom),
+      click: () => NetMonitorView.RequestsMenu.cloneSelectedRequest(),
+    }));
+
+    menu.append(new MenuItem({
+      type: "separator",
+      visible: !!selectedItem,
+    }));
+
+    menu.append(new MenuItem({
+      id: "request-menu-context-newtab",
+      label: L10N.getStr("netmonitor.context.newTab"),
+      accesskey: L10N.getStr("netmonitor.context.newTab.accesskey"),
+      visible: !!selectedItem,
+      click: () => this.openRequestInTab()
+    }));
+
+    menu.append(new MenuItem({
+      id: "request-menu-context-perf",
+      label: L10N.getStr("netmonitor.context.perfTools"),
+      accesskey: L10N.getStr("netmonitor.context.perfTools.accesskey"),
+      visible: !!NetMonitorController.supportsPerfStats,
+      click: () => NetMonitorView.toggleFrontendMode()
+    }));
+
+    menu.popup(screenX, screenY, NetMonitorController._toolbox);
+    return menu;
+  },
+
+  /**
+   * Opens selected item in a new tab.
+   */
+  openRequestInTab() {
+    let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+    let { url } = this.selectedItem.attachment;
+    win.openUILinkIn(url, "tab", { relatedToCurrent: true });
+  },
+
+  /**
+   * Copy the request url from the currently selected item.
+   */
+  copyUrl() {
+    clipboardHelper.copyString(this.selectedItem.attachment.url);
+  },
+
+  /**
+   * Copy the request url query string parameters from the currently
+   * selected item.
+   */
+  copyUrlParams() {
+    let { url } = this.selectedItem.attachment;
+    let params = NetworkHelper.nsIURL(url).query.split("&");
+    let string = params.join(Services.appinfo.OS === "WINNT" ? "\r\n" : "\n");
+    clipboardHelper.copyString(string);
+  },
+
+  /**
+   * Copy the request form data parameters (or raw payload) from
+   * the currently selected item.
+   */
+  copyPostData: Task.async(function* () {
+    let selected = this.selectedItem.attachment;
+
+    // Try to extract any form data parameters.
+    let formDataSections = yield getFormDataSections(
+      selected.requestHeaders,
+      selected.requestHeadersFromUploadStream,
+      selected.requestPostData,
+      gNetwork.getString.bind(gNetwork));
+
+    let params = [];
+    formDataSections.forEach(section => {
+      let paramsArray = NetworkHelper.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 postData = selected.requestPostData.postData.text;
+      string = yield gNetwork.getString(postData);
+      if (Services.appinfo.OS !== "WINNT") {
+        string = string.replace(/\r/g, "");
+      }
+    }
+
+    clipboardHelper.copyString(string);
+  }),
+
+  /**
+   * Copy a cURL command from the currently selected item.
+   */
+  copyAsCurl: Task.async(function* () {
+    let selected = this.selectedItem.attachment;
+
+    // Create a sanitized object for the Curl command generator.
+    let data = {
+      url: selected.url,
+      method: selected.method,
+      headers: [],
+      httpVersion: selected.httpVersion,
+      postDataText: null
+    };
+
+    // Fetch header values.
+    for (let { name, value } of selected.requestHeaders.headers) {
+      let text = yield gNetwork.getString(value);
+      data.headers.push({ name: name, value: text });
+    }
+
+    // Fetch the request payload.
+    if (selected.requestPostData) {
+      let postData = selected.requestPostData.postData.text;
+      data.postDataText = yield gNetwork.getString(postData);
+    }
+
+    clipboardHelper.copyString(Curl.generateCommand(data));
+  }),
+
+  /**
+   * Copy the raw request headers from the currently selected item.
+   */
+  copyRequestHeaders() {
+    let selected = this.selectedItem.attachment;
+    let rawHeaders = selected.requestHeaders.rawHeaders.trim();
+    if (Services.appinfo.OS !== "WINNT") {
+      rawHeaders = rawHeaders.replace(/\r/g, "");
+    }
+    clipboardHelper.copyString(rawHeaders);
+  },
+
+  /**
+   * Copy the raw response headers from the currently selected item.
+   */
+  copyResponseHeaders() {
+    let selected = this.selectedItem.attachment;
+    let rawHeaders = selected.responseHeaders.rawHeaders.trim();
+    if (Services.appinfo.OS !== "WINNT") {
+      rawHeaders = rawHeaders.replace(/\r/g, "");
+    }
+    clipboardHelper.copyString(rawHeaders);
+  },
+
+  /**
+   * Copy image as data uri.
+   */
+  copyImageAsDataUri() {
+    let selected = this.selectedItem.attachment;
+    let { mimeType, text, encoding } = selected.responseContent.content;
+
+    gNetwork.getString(text).then(string => {
+      let data = formDataURI(mimeType, encoding, string);
+      clipboardHelper.copyString(data);
+    });
+  },
+
+  /**
+   * Copy response data as a string.
+   */
+  copyResponse() {
+    let selected = this.selectedItem.attachment;
+    let text = selected.responseContent.content.text;
+
+    gNetwork.getString(text).then(string => {
+      clipboardHelper.copyString(string);
+    });
+  },
+
+  /**
+   * Copy HAR from the network panel content to the clipboard.
+   */
+  copyAllAsHar() {
+    let options = this.getDefaultHarOptions();
+    return HarExporter.copy(options);
+  },
+
+  /**
+   * Save HAR from the network panel content to a file.
+   */
+  saveAllAsHar() {
+    let options = this.getDefaultHarOptions();
+    return HarExporter.save(options);
+  },
+
+  getDefaultHarOptions() {
+    let form = NetMonitorController._target.form;
+    let title = form.title || form.url;
+
+    return {
+      getString: gNetwork.getString.bind(gNetwork),
+      view: NetMonitorView.RequestsMenu,
+      items: NetMonitorView.RequestsMenu.items,
+      title: title
+    };
+  }
+};
+
+module.exports = RequestListContextMenu;
--- a/devtools/client/netmonitor/requests-menu-view.js
+++ b/devtools/client/netmonitor/requests-menu-view.js
@@ -1,47 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
 /* globals document, window, dumpn, $, gNetwork, EVENTS, Prefs,
            NetMonitorController, NetMonitorView */
+
 "use strict";
+
 /* eslint-disable mozilla/reject-some-requires */
 const { Cu } = require("chrome");
-const Services = require("Services");
 const {Task} = require("devtools/shared/task");
 const {DeferredTask} = Cu.import("resource://gre/modules/DeferredTask.jsm", {});
 /* eslint-disable mozilla/reject-some-requires */
 const {SideMenuWidget} = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
 const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
 const {setImageTooltip, getImageDimensions} =
   require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
 const {Heritage, WidgetMethods, setNamedTimeout} =
   require("devtools/client/shared/widgets/view-helpers");
-const {gDevTools} = require("devtools/client/framework/devtools");
-const Menu = require("devtools/client/framework/menu");
-const MenuItem = require("devtools/client/framework/menu-item");
-const {Curl, CurlUtils} = require("devtools/client/shared/curl");
+const {CurlUtils} = require("devtools/client/shared/curl");
 const {PluralForm} = require("devtools/shared/plural-form");
 const {Filters, isFreetextMatch} = require("./filter-predicates");
 const {Sorters} = require("./sort-predicates");
 const {L10N, WEBCONSOLE_L10N} = require("./l10n");
-const {getFormDataSections,
-       formDataURI,
+const {formDataURI,
        writeHeaderText,
        getKeyWithEvent,
        getAbbreviatedMimeType,
        getUriNameWithQuery,
        getUriHostPort,
        getUriHost,
        loadCauseString} = require("./request-utils");
 const Actions = require("./actions/index");
-
-loader.lazyServiceGetter(this, "clipboardHelper",
-  "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
-
-loader.lazyRequireGetter(this, "HarExporter",
-  "devtools/client/netmonitor/har/har-exporter", true);
+const RequestListContextMenu = require("./request-list-context-menu");
 
 loader.lazyRequireGetter(this, "NetworkHelper",
   "devtools/shared/webconsole/network-helper");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const EPSILON = 0.001;
 // ms
 const RESIZE_REFRESH_RATE = 50;
@@ -123,16 +119,18 @@ RequestsMenuView.prototype = Heritage.ex
   /**
    * Initialization function, called when the network monitor is started.
    */
   initialize: function (store) {
     dumpn("Initializing the RequestsMenuView");
 
     this.store = store;
 
+    this.contextMenu = new RequestListContextMenu();
+
     let widgetParentEl = $("#requests-menu-contents");
     this.widget = new SideMenuWidget(widgetParentEl);
     this._splitter = $("#network-inspector-view-splitter");
     this._summary = $("#requests-menu-network-summary-button");
     this._summary.setAttribute("label", L10N.getStr("networkMenu.empty"));
 
     // Create a tooltip for the newly appended network request item.
     this.tooltip = new HTMLTooltip(NetMonitorController._toolbox.doc, { type: "arrow" });
@@ -149,23 +147,16 @@ RequestsMenuView.prototype = Heritage.ex
     this.widget.addEventListener("select", this._onSelect, false);
     this.widget.addEventListener("swap", this._onSwap, false);
     this._splitter.addEventListener("mousemove", this._onResize, false);
     window.addEventListener("resize", this._onResize, false);
 
     this.requestsMenuSortEvent = getKeyWithEvent(this.sortBy.bind(this));
     this.requestsMenuSortKeyboardEvent = getKeyWithEvent(this.sortBy.bind(this), true);
     this._onContextMenu = this._onContextMenu.bind(this);
-    this._onContextNewTabCommand = this.openRequestInTab.bind(this);
-    this._onContextCopyUrlCommand = this.copyUrl.bind(this);
-    this._onContextCopyImageAsDataUriCommand =
-      this.copyImageAsDataUri.bind(this);
-    this._onContextCopyResponseCommand = this.copyResponse.bind(this);
-    this._onContextResendCommand = this.cloneSelectedRequest.bind(this);
-    this._onContextToggleRawHeadersCommand = this.toggleRawHeaders.bind(this);
     this._onContextPerfCommand = () => NetMonitorView.toggleFrontendMode();
     this._onReloadCommand = () => NetMonitorView.reloadPage();
     this._flushRequestsTask = new DeferredTask(this._flushRequests,
       REQUESTS_REFRESH_RATE);
 
     this.sendCustomRequestEvent = this.sendCustomRequest.bind(this);
     this.closeCustomRequestEvent = this.closeCustomRequest.bind(this);
     this.cloneSelectedRequestEvent = this.cloneSelectedRequest.bind(this);
@@ -344,191 +335,16 @@ RequestsMenuView.prototype = Heritage.ex
       return void this._flushRequests();
     }
 
     this._flushRequestsTask.arm();
     return undefined;
   },
 
   /**
-   * Opens selected item in a new tab.
-   */
-  openRequestInTab: function () {
-    let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
-    let selected = this.selectedItem.attachment;
-    win.openUILinkIn(selected.url, "tab", { relatedToCurrent: true });
-  },
-
-  /**
-   * Copy the request url from the currently selected item.
-   */
-  copyUrl: function () {
-    let selected = this.selectedItem.attachment;
-    clipboardHelper.copyString(selected.url);
-  },
-
-  /**
-   * Copy the request url query string parameters from the currently
-   * selected item.
-   */
-  copyUrlParams: function () {
-    let selected = this.selectedItem.attachment;
-    let params = NetworkHelper.nsIURL(selected.url).query.split("&");
-    let string = params.join(Services.appinfo.OS === "WINNT" ? "\r\n" : "\n");
-    clipboardHelper.copyString(string);
-  },
-
-  /**
-   * Copy the request form data parameters (or raw payload) from
-   * the currently selected item.
-   */
-  copyPostData: Task.async(function* () {
-    let selected = this.selectedItem.attachment;
-
-    // Try to extract any form data parameters.
-    let formDataSections = yield getFormDataSections(
-      selected.requestHeaders,
-      selected.requestHeadersFromUploadStream,
-      selected.requestPostData,
-      gNetwork.getString.bind(gNetwork));
-
-    let params = [];
-    formDataSections.forEach(section => {
-      let paramsArray = NetworkHelper.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 postData = selected.requestPostData.postData.text;
-      string = yield gNetwork.getString(postData);
-      if (Services.appinfo.OS !== "WINNT") {
-        string = string.replace(/\r/g, "");
-      }
-    }
-
-    clipboardHelper.copyString(string);
-  }),
-
-  /**
-   * Copy a cURL command from the currently selected item.
-   */
-  copyAsCurl: function () {
-    let selected = this.selectedItem.attachment;
-
-    Task.spawn(function* () {
-      // Create a sanitized object for the Curl command generator.
-      let data = {
-        url: selected.url,
-        method: selected.method,
-        headers: [],
-        httpVersion: selected.httpVersion,
-        postDataText: null
-      };
-
-      // Fetch header values.
-      for (let { name, value } of selected.requestHeaders.headers) {
-        let text = yield gNetwork.getString(value);
-        data.headers.push({ name: name, value: text });
-      }
-
-      // Fetch the request payload.
-      if (selected.requestPostData) {
-        let postData = selected.requestPostData.postData.text;
-        data.postDataText = yield gNetwork.getString(postData);
-      }
-
-      clipboardHelper.copyString(Curl.generateCommand(data));
-    });
-  },
-
-  /**
-   * Copy HAR from the network panel content to the clipboard.
-   */
-  copyAllAsHar: function () {
-    let options = this.getDefaultHarOptions();
-    return HarExporter.copy(options);
-  },
-
-  /**
-   * Save HAR from the network panel content to a file.
-   */
-  saveAllAsHar: function () {
-    let options = this.getDefaultHarOptions();
-    return HarExporter.save(options);
-  },
-
-  getDefaultHarOptions: function () {
-    let form = NetMonitorController._target.form;
-    let title = form.title || form.url;
-
-    return {
-      getString: gNetwork.getString.bind(gNetwork),
-      view: this,
-      items: NetMonitorView.RequestsMenu.items,
-      title: title
-    };
-  },
-
-  /**
-   * Copy the raw request headers from the currently selected item.
-   */
-  copyRequestHeaders: function () {
-    let selected = this.selectedItem.attachment;
-    let rawHeaders = selected.requestHeaders.rawHeaders.trim();
-    if (Services.appinfo.OS !== "WINNT") {
-      rawHeaders = rawHeaders.replace(/\r/g, "");
-    }
-    clipboardHelper.copyString(rawHeaders);
-  },
-
-  /**
-   * Copy the raw response headers from the currently selected item.
-   */
-  copyResponseHeaders: function () {
-    let selected = this.selectedItem.attachment;
-    let rawHeaders = selected.responseHeaders.rawHeaders.trim();
-    if (Services.appinfo.OS !== "WINNT") {
-      rawHeaders = rawHeaders.replace(/\r/g, "");
-    }
-    clipboardHelper.copyString(rawHeaders);
-  },
-
-  /**
-   * Copy image as data uri.
-   */
-  copyImageAsDataUri: function () {
-    let selected = this.selectedItem.attachment;
-    let { mimeType, text, encoding } = selected.responseContent.content;
-
-    gNetwork.getString(text).then(string => {
-      let data = formDataURI(mimeType, encoding, string);
-      clipboardHelper.copyString(data);
-    });
-  },
-
-  /**
-   * Copy response data as a string.
-   */
-  copyResponse: function () {
-    let selected = this.selectedItem.attachment;
-    let text = selected.responseContent.content.text;
-
-    gNetwork.getString(text).then(string => {
-      clipboardHelper.copyString(string);
-    });
-  },
-
-  /**
    * Create a new custom request form populated with the data from
    * the currently selected request.
    */
   cloneSelectedRequest: function () {
     let selected = this.selectedItem.attachment;
 
     // Create the element node for the network request item.
     let menuView = this._createMenuView(selected.method, selected.url,
@@ -1708,167 +1524,17 @@ RequestsMenuView.prototype = Heritage.ex
     this.tooltip.hide();
   },
 
   /**
    * Open context menu
    */
   _onContextMenu: function (e) {
     e.preventDefault();
-    this._openMenu({
-      screenX: e.screenX,
-      screenY: e.screenY,
-      target: e.target,
-    });
-  },
-
-  /**
-   * 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
-   */
-  _openMenu: function ({ target, screenX = 0, screenY = 0 } = { }) {
-    let selectedItem = this.selectedItem;
-
-    let menu = new Menu();
-    menu.append(new MenuItem({
-      id: "request-menu-context-copy-url",
-      label: L10N.getStr("netmonitor.context.copyUrl"),
-      accesskey: L10N.getStr("netmonitor.context.copyUrl.accesskey"),
-      visible: !!selectedItem,
-      click: () => this._onContextCopyUrlCommand(),
-    }));
-
-    menu.append(new MenuItem({
-      id: "request-menu-context-copy-url-params",
-      label: L10N.getStr("netmonitor.context.copyUrlParams"),
-      accesskey: L10N.getStr("netmonitor.context.copyUrlParams.accesskey"),
-      visible: !!(selectedItem &&
-               NetworkHelper.nsIURL(selectedItem.attachment.url).query),
-      click: () => this.copyUrlParams(),
-    }));
-
-    menu.append(new MenuItem({
-      id: "request-menu-context-copy-post-data",
-      label: L10N.getStr("netmonitor.context.copyPostData"),
-      accesskey: L10N.getStr("netmonitor.context.copyPostData.accesskey"),
-      visible: !!(selectedItem && selectedItem.attachment.requestPostData),
-      click: () => this.copyPostData(),
-    }));
-
-    menu.append(new MenuItem({
-      id: "request-menu-context-copy-as-curl",
-      label: L10N.getStr("netmonitor.context.copyAsCurl"),
-      accesskey: L10N.getStr("netmonitor.context.copyAsCurl.accesskey"),
-      visible: !!(selectedItem && selectedItem.attachment),
-      click: () => this.copyAsCurl(),
-    }));
-
-    menu.append(new MenuItem({
-      type: "separator",
-      visible: !!selectedItem,
-    }));
-
-    menu.append(new MenuItem({
-      id: "request-menu-context-copy-request-headers",
-      label: L10N.getStr("netmonitor.context.copyRequestHeaders"),
-      accesskey: L10N.getStr("netmonitor.context.copyRequestHeaders.accesskey"),
-      visible: !!(selectedItem && selectedItem.attachment.requestHeaders),
-      click: () => this.copyRequestHeaders(),
-    }));
-
-    menu.append(new MenuItem({
-      id: "response-menu-context-copy-response-headers",
-      label: L10N.getStr("netmonitor.context.copyResponseHeaders"),
-      accesskey: L10N.getStr("netmonitor.context.copyResponseHeaders.accesskey"),
-      visible: !!(selectedItem && selectedItem.attachment.responseHeaders),
-      click: () => this.copyResponseHeaders(),
-    }));
-
-    menu.append(new MenuItem({
-      id: "request-menu-context-copy-response",
-      label: L10N.getStr("netmonitor.context.copyResponse"),
-      accesskey: L10N.getStr("netmonitor.context.copyResponse.accesskey"),
-      visible: !!(selectedItem &&
-               selectedItem.attachment.responseContent &&
-               selectedItem.attachment.responseContent.content.text &&
-               selectedItem.attachment.responseContent.content.text.length !== 0),
-      click: () => this._onContextCopyResponseCommand(),
-    }));
-
-    menu.append(new MenuItem({
-      id: "request-menu-context-copy-image-as-data-uri",
-      label: L10N.getStr("netmonitor.context.copyImageAsDataUri"),
-      accesskey: L10N.getStr("netmonitor.context.copyImageAsDataUri.accesskey"),
-      visible: !!(selectedItem &&
-               selectedItem.attachment.responseContent &&
-               selectedItem.attachment.responseContent.content
-                 .mimeType.includes("image/")),
-      click: () => this._onContextCopyImageAsDataUriCommand(),
-    }));
-
-    menu.append(new MenuItem({
-      type: "separator",
-      visible: !!selectedItem,
-    }));
-
-    menu.append(new MenuItem({
-      id: "request-menu-context-copy-all-as-har",
-      label: L10N.getStr("netmonitor.context.copyAllAsHar"),
-      accesskey: L10N.getStr("netmonitor.context.copyAllAsHar.accesskey"),
-      visible: !!this.items.length,
-      click: () => this.copyAllAsHar(),
-    }));
-
-    menu.append(new MenuItem({
-      id: "request-menu-context-save-all-as-har",
-      label: L10N.getStr("netmonitor.context.saveAllAsHar"),
-      accesskey: L10N.getStr("netmonitor.context.saveAllAsHar.accesskey"),
-      visible: !!this.items.length,
-      click: () => this.saveAllAsHar(),
-    }));
-
-    menu.append(new MenuItem({
-      type: "separator",
-      visible: !!selectedItem,
-    }));
-
-    menu.append(new MenuItem({
-      id: "request-menu-context-resend",
-      label: L10N.getStr("netmonitor.context.editAndResend"),
-      accesskey: L10N.getStr("netmonitor.context.editAndResend.accesskey"),
-      visible: !!(NetMonitorController.supportsCustomRequest &&
-               selectedItem &&
-               !selectedItem.attachment.isCustom),
-      click: () => this._onContextResendCommand(),
-    }));
-
-    menu.append(new MenuItem({
-      type: "separator",
-      visible: !!selectedItem,
-    }));
-
-    menu.append(new MenuItem({
-      id: "request-menu-context-newtab",
-      label: L10N.getStr("netmonitor.context.newTab"),
-      accesskey: L10N.getStr("netmonitor.context.newTab.accesskey"),
-      visible: !!selectedItem,
-      click: () => this._onContextNewTabCommand(),
-    }));
-
-    menu.append(new MenuItem({
-      id: "request-menu-context-perf",
-      label: L10N.getStr("netmonitor.context.perfTools"),
-      accesskey: L10N.getStr("netmonitor.context.perfTools.accesskey"),
-      visible: !!NetMonitorController.supportsPerfStats,
-      click: () => this._onContextPerfCommand(),
-    }));
-
-    menu.popup(screenX, screenY, NetMonitorController._toolbox);
-    return menu;
+    this.contextMenu.open(e);
   },
 
   /**
    * Checks if the specified unix time is the first one to be known of,
    * and saves it if so.
    *
    * @param number unixTime
    *        The milliseconds to check and save.
--- a/devtools/client/netmonitor/test/browser_net_copy_as_curl.js
+++ b/devtools/client/netmonitor/test/browser_net_copy_as_curl.js
@@ -50,17 +50,17 @@ add_task(function* () {
     content.wrappedJSObject.performRequest(url);
   });
   yield wait;
 
   let requestItem = RequestsMenu.getItemAtIndex(0);
   RequestsMenu.selectedItem = requestItem;
 
   yield waitForClipboardPromise(function setup() {
-    RequestsMenu.copyAsCurl();
+    RequestsMenu.contextMenu.copyAsCurl();
   }, function validate(result) {
     if (typeof result !== "string") {
       return false;
     }
 
     // Different setups may produce the same command, but with the
     // parameters in a different order in the commandline (which is fine).
     // Here we confirm that the commands are the same even in that case.
--- a/devtools/client/netmonitor/test/browser_net_copy_headers.js
+++ b/devtools/client/netmonitor/test/browser_net_copy_headers.js
@@ -34,17 +34,17 @@ add_task(function* () {
     "Accept-Encoding: gzip, deflate",
     "Connection: keep-alive",
     "Upgrade-Insecure-Requests: 1",
     "Pragma: no-cache",
     "Cache-Control: no-cache"
   ].join("\n");
 
   yield waitForClipboardPromise(function setup() {
-    RequestsMenu.copyRequestHeaders();
+    RequestsMenu.contextMenu.copyRequestHeaders();
   }, function validate(result) {
     // Sometimes, a "Cookie" header is left over from other tests. Remove it:
     result = String(result).replace(/Cookie: [^\n]+\n/, "");
     return result === EXPECTED_REQUEST_HEADERS;
   });
   info("Clipboard contains the currently selected item's request headers.");
 
   const EXPECTED_RESPONSE_HEADERS = [
@@ -53,17 +53,17 @@ add_task(function* () {
     "Content-Type: text/html",
     "Content-Length: 465",
     "Connection: close",
     "Server: httpd.js",
     "Date: Sun, 3 May 2015 11:11:11 GMT"
   ].join("\n");
 
   yield waitForClipboardPromise(function setup() {
-    RequestsMenu.copyResponseHeaders();
+    RequestsMenu.contextMenu.copyResponseHeaders();
   }, function validate(result) {
     // Fake the "Last-Modified" and "Date" headers because they will vary:
     result = String(result)
       .replace(/Last-Modified: [^\n]+ GMT/, "Last-Modified: Sun, 3 May 2015 11:11:11 GMT")
       .replace(/Date: [^\n]+ GMT/, "Date: Sun, 3 May 2015 11:11:11 GMT");
     return result === EXPECTED_RESPONSE_HEADERS;
   });
   info("Clipboard contains the currently selected item's response headers.");
--- a/devtools/client/netmonitor/test/browser_net_copy_image_as_data_uri.js
+++ b/devtools/client/netmonitor/test/browser_net_copy_image_as_data_uri.js
@@ -21,15 +21,15 @@ add_task(function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
   let requestItem = RequestsMenu.getItemAtIndex(5);
   RequestsMenu.selectedItem = requestItem;
 
   yield waitForClipboardPromise(function setup() {
-    RequestsMenu.copyImageAsDataUri();
+    RequestsMenu.contextMenu.copyImageAsDataUri();
   }, TEST_IMAGE_DATA_URI);
 
   ok(true, "Clipboard contains the currently selected image as data uri.");
 
   yield teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_copy_params.js
+++ b/devtools/client/netmonitor/test/browser_net_copy_params.js
@@ -70,29 +70,29 @@ add_task(function* () {
       item.id === "request-menu-context-copy-url-params");
     is(copyUrlParamsNode.visible, !hidden,
       "The \"Copy URL Parameters\" context menu item should" + (hidden ? " " : " not ") +
         "be hidden.");
   }
 
   function* testCopyUrlParams(queryString) {
     yield waitForClipboardPromise(function setup() {
-      RequestsMenu.copyUrlParams();
+      RequestsMenu.contextMenu.copyUrlParams();
     }, queryString);
     ok(true, "The url query string copied from the selected item is correct.");
   }
 
   function testCopyPostDataHidden(hidden) {
     let allMenuItems = openContextMenuAndGetAllItems(NetMonitorView);
     let copyPostDataNode = allMenuItems.find(item =>
       item.id === "request-menu-context-copy-post-data");
     is(copyPostDataNode.visible, !hidden,
       "The \"Copy POST Data\" context menu item should" + (hidden ? " " : " not ") +
         "be hidden.");
   }
 
   function* testCopyPostData(postData) {
     yield waitForClipboardPromise(function setup() {
-      RequestsMenu.copyPostData();
+      RequestsMenu.contextMenu.copyPostData();
     }, postData);
     ok(true, "The post data string copied from the selected item is correct.");
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_copy_response.js
+++ b/devtools/client/netmonitor/test/browser_net_copy_response.js
@@ -23,13 +23,13 @@ add_task(function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
   let requestItem = RequestsMenu.getItemAtIndex(3);
   RequestsMenu.selectedItem = requestItem;
 
   yield waitForClipboardPromise(function setup() {
-    RequestsMenu.copyResponse();
+    RequestsMenu.contextMenu.copyResponse();
   }, EXPECTED_RESULT);
 
   yield teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_copy_svg_image_as_data_uri.js
+++ b/devtools/client/netmonitor/test/browser_net_copy_svg_image_as_data_uri.js
@@ -23,15 +23,15 @@ add_task(function* () {
     content.wrappedJSObject.performRequest(url);
   });
   yield wait;
 
   let requestItem = RequestsMenu.getItemAtIndex(0);
   RequestsMenu.selectedItem = requestItem;
 
   yield waitForClipboardPromise(function setup() {
-    RequestsMenu.copyImageAsDataUri();
+    RequestsMenu.contextMenu.copyImageAsDataUri();
   }, function check(text) {
     return text.startsWith("data:") && !/undefined/.test(text);
   });
 
   yield teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_copy_url.js
+++ b/devtools/client/netmonitor/test/browser_net_copy_url.js
@@ -19,13 +19,13 @@ add_task(function* () {
     content.wrappedJSObject.performRequests(1);
   });
   yield wait;
 
   let requestItem = RequestsMenu.getItemAtIndex(0);
   RequestsMenu.selectedItem = requestItem;
 
   yield waitForClipboardPromise(function setup() {
-    RequestsMenu.copyUrl();
+    RequestsMenu.contextMenu.copyUrl();
   }, requestItem.attachment.url);
 
   yield teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_open_request_in_tab.js
+++ b/devtools/client/netmonitor/test/browser_net_open_request_in_tab.js
@@ -21,17 +21,17 @@ add_task(function* () {
     content.wrappedJSObject.performRequests(1);
   });
   yield wait;
 
   let requestItem = RequestsMenu.getItemAtIndex(0);
   RequestsMenu.selectedItem = requestItem;
 
   let onTabOpen = once(gBrowser.tabContainer, "TabOpen", false);
-  RequestsMenu.openRequestInTab();
+  RequestsMenu.contextMenu.openRequestInTab();
   yield onTabOpen;
 
   ok(true, "A new tab has been opened");
 
   yield teardown(monitor);
 
   gBrowser.removeCurrentTab();
 });
--- a/devtools/client/netmonitor/test/head.js
+++ b/devtools/client/netmonitor/test/head.js
@@ -490,21 +490,21 @@ function waitForContentMessage(name) {
     def.resolve(msg);
   });
   return def.promise;
 }
 
 /**
  * Open the requestMenu menu and return all of it's items in a flat array
  * @param {netmonitorPanel} netmonitor
- * @param {Object} options to pass into openMenu
+ * @param {Event} event mouse event with screenX and screenX coordinates
  * @return An array of MenuItems
  */
-function openContextMenuAndGetAllItems(netmonitor, options) {
-  let menu = netmonitor.RequestsMenu._openMenu(options);
+function openContextMenuAndGetAllItems(netmonitor, event) {
+  let menu = netmonitor.RequestsMenu.contextMenu.open(event);
 
   // Flatten all menu items into a single array to make searching through it easier
   let allItems = [].concat.apply([], menu.items.map(function addItem(item) {
     if (item.submenu) {
       return addItem(item.submenu.items);
     }
     return item;
   }));