Bug 1540054 - Create fetch request from selection. r=Honza
authorMrigank Krishan <mrigankkrishan@gmail.com>
Fri, 19 Apr 2019 09:31:50 +0000
changeset 470186 29e9e55681ab0acb9c25d39efc863e08f9936435
parent 470185 cf4f78f532eefc838bc622e3b39b0dfaf758971f
child 470187 2cc5bbbfe0820cbf580b01d84c12948f3cf527fc
push id35889
push usercsabou@mozilla.com
push dateFri, 19 Apr 2019 16:34:07 +0000
treeherdermozilla-central@0b1de782bd32 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersHonza
bugs1540054
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 1540054 - Create fetch request from selection. r=Honza add "Copy as fetch" context menu item Differential Revision: https://phabricator.services.mozilla.com/D25450
devtools/client/locales/en-US/netmonitor.properties
devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
devtools/client/netmonitor/test/browser.ini
devtools/client/netmonitor/test/browser_net_copy_as_fetch.js
devtools/client/netmonitor/test/browser_net_use_as_fetch.js
--- a/devtools/client/locales/en-US/netmonitor.properties
+++ b/devtools/client/locales/en-US/netmonitor.properties
@@ -949,16 +949,24 @@ netmonitor.context.copyRequestData.acces
 # The capitalization is part of the official name and should be used throughout all languages.
 # http://en.wikipedia.org/wiki/CURL
 netmonitor.context.copyAsCurl=Copy as cURL
 
 # LOCALIZATION NOTE (netmonitor.context.copyAsCurl.accesskey): This is the access key
 # for the Copy as cURL menu item displayed in the context menu for a request
 netmonitor.context.copyAsCurl.accesskey=C
 
+# LOCALIZATION NOTE (netmonitor.context.copyAsFetch): This is the label displayed
+# on the context menu that copies the selected request as a fetch request.
+netmonitor.context.copyAsFetch=Copy as Fetch
+
+# LOCALIZATION NOTE (netmonitor.context.copyAsFetch.accesskey): This is the access key
+# for the Copy as fetch menu item displayed in the context menu for a request
+netmonitor.context.copyAsFetch.accesskey=F
+
 # LOCALIZATION NOTE (netmonitor.context.copyRequestHeaders): This is the label displayed
 # on the context menu that copies the selected item's request headers
 netmonitor.context.copyRequestHeaders=Copy Request Headers
 
 # LOCALIZATION NOTE (netmonitor.context.copyRequestHeaders.accesskey): This is the access key
 # for the Copy Request Headers menu item displayed in the context menu for a request
 netmonitor.context.copyRequestHeaders.accesskey=q
 
@@ -981,16 +989,24 @@ netmonitor.context.copyResponse.accesske
 # LOCALIZATION NOTE (netmonitor.context.copyImageAsDataUri): This is the label displayed
 # on the context menu that copies the selected image as data uri
 netmonitor.context.copyImageAsDataUri=Copy Image as Data URI
 
 # LOCALIZATION NOTE (netmonitor.context.copyImageAsDataUri.accesskey): This is the access key
 # for the Copy Image As Data URI menu item displayed in the context menu for a request
 netmonitor.context.copyImageAsDataUri.accesskey=I
 
+# LOCALIZATION NOTE (netmonitor.context.useAsFetch): This is the label displayed
+# on the context menu that copies the selected request as a fetch command.
+netmonitor.context.useAsFetch=Use as Fetch in Console
+
+# LOCALIZATION NOTE (netmonitor.context.useAsFetch.accesskey): This is the access key
+# for the Copy as fetch menu item displayed in the context menu for a request
+netmonitor.context.useAsFetch.accesskey=F
+
 # LOCALIZATION NOTE (netmonitor.context.saveImageAs): This is the label displayed
 # on the context menu that save the Image
 netmonitor.context.saveImageAs=Save Image As
 
 # LOCALIZATION NOTE (netmonitor.context.saveImageAs.accesskey): This is the access key
 # for the Copy Image As Data URI menu item displayed in the context menu for a request
 netmonitor.context.saveImageAs.accesskey=v
 
--- a/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
+++ b/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
@@ -86,16 +86,25 @@ class RequestListContextMenu {
       // 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,
       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,
+      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"),
@@ -226,16 +235,29 @@ class RequestListContextMenu {
     menu.push({
       id: "request-list-context-perf",
       label: L10N.getStr("netmonitor.context.perfTools"),
       accesskey: L10N.getStr("netmonitor.context.perfTools.accesskey"),
       visible: requests.size > 0,
       click: () => openStatistics(true),
     });
 
+    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,
+      click: () =>
+        this.useAsFetch(id, url, method, requestHeaders, requestPostData),
+    });
+
     showMenu(menu, {
       screenX: event.screenX,
       screenY: event.screenY,
     });
   }
 
   /**
    * Opens selected item in the debugger
@@ -320,16 +342,117 @@ class RequestListContextMenu {
       headers: requestHeaders.headers,
       httpVersion,
       postDataText: requestPostData ? requestPostData.postData.text : "",
     };
     copyString(Curl.generateCommand(data));
   }
 
   /**
+   * Generate fetch string
+   */
+  async generateFetchString(id, url, method, requestHeaders, requestPostData) {
+    requestHeaders = requestHeaders ||
+      await this.props.connector.requestData(id, "requestHeaders");
+
+    requestPostData = requestPostData ||
+      await this.props.connector.requestData(id, "requestPostData");
+
+    // https://fetch.spec.whatwg.org/#forbidden-header-name
+    const forbiddenHeaders = {
+      "accept-charset": 1,
+      "accept-encoding": 1,
+      "access-control-request-headers": 1,
+      "access-control-request-method": 1,
+      "connection": 1,
+      "content-length": 1,
+      "cookie": 1,
+      "cookie2": 1,
+      "date": 1,
+      "dnt": 1,
+      "expect": 1,
+      "host": 1,
+      "keep-alive": 1,
+      "origin": 1,
+      "referer": 1,
+      "te": 1,
+      "trailer": 1,
+      "transfer-encoding": 1,
+      "upgrade": 1,
+      "via": 1,
+    };
+    const credentialHeaders = {"cookie": 1, "authorization": 1};
+
+    const headers = {};
+    for (const {name, value} of requestHeaders.headers) {
+      if (!forbiddenHeaders[name.toLowerCase()]) {
+        headers[name] = value;
+      }
+    }
+
+    const referrerHeader = requestHeaders.headers.find(
+      ({ name }) => name.toLowerCase() === "referer"
+    );
+
+    const referrerPolicy = requestHeaders.headers.find(
+      ({ name }) => name.toLowerCase() === "referrer-policy"
+    );
+
+    const referrer = referrerHeader ? referrerHeader.value : undefined;
+    const credentials = requestHeaders.headers.some(
+      ({name}) => credentialHeaders[name.toLowerCase()]
+    ) ? "include" : "omit";
+
+    const fetchOptions = {
+      credentials,
+      headers,
+      referrer,
+      referrerPolicy,
+      body: requestPostData.postData.text,
+      method: method,
+      mode: "cors",
+    };
+
+    const options = JSON.stringify(fetchOptions, null, 4);
+    const fetchString = `await fetch("${url}", ${options});`;
+    return fetchString;
+  }
+
+  /**
+   * Copy the currently selected item as fetch request.
+   */
+  async copyAsFetch(id, url, method, requestHeaders, requestPostData) {
+    const fetchString = await this.generateFetchString(
+      id,
+      url,
+      method,
+      requestHeaders,
+      requestPostData
+    );
+    copyString(fetchString);
+  }
+
+  /**
+   * Open split console and fill it with fetch command for selected item
+   */
+  async useAsFetch(id, url, method, requestHeaders, requestPostData) {
+    const fetchString = await this.generateFetchString(
+      id,
+      url,
+      method,
+      requestHeaders,
+      requestPostData
+    );
+    const toolbox = gDevTools.getToolbox(this.props.connector.getTabTarget());
+    await toolbox.openSplitConsole();
+    const { hud } = await toolbox.getPanel("webconsole");
+    hud.setInputValue(fetchString);
+  }
+
+  /**
    * Copy the raw request headers from the currently selected item.
    */
   async copyRequestHeaders(id, requestHeaders) {
     requestHeaders = requestHeaders ||
       await this.props.connector.requestData(id, "requestHeaders");
 
     let rawHeaders = requestHeaders.rawHeaders.trim();
 
--- a/devtools/client/netmonitor/test/browser.ini
+++ b/devtools/client/netmonitor/test/browser.ini
@@ -111,16 +111,19 @@ skip-if = (verify && !debug && (os == 'm
 [browser_net_copy_response.js]
 subsuite = clipboard
 [browser_net_copy_headers.js]
 subsuite = clipboard
 [browser_net_cookies_sorted.js]
 skip-if = (verify && debug && os == 'win')
 [browser_net_copy_as_curl.js]
 subsuite = clipboard
+[browser_net_copy_as_fetch.js]
+subsuite = clipboard
+[browser_net_use_as_fetch.js]
 [browser_net_cors_requests.js]
 [browser_net_cyrillic-01.js]
 [browser_net_cyrillic-02.js]
 [browser_net_frame.js]
 skip-if = (os == 'mac') || (os == 'win' && os_version == '10.0') # Bug 1479782
 [browser_net_header-docs.js]
 [browser_net_edit_resend_cancel.js]
 [browser_net_edit_resend_caret.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_copy_as_fetch.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if Copy as Fetch works.
+ */
+
+add_task(async function() {
+  const { tab, monitor } = await initNetMonitor(CURL_URL);
+  info("Starting test... ");
+
+  // GET request, no cookies (first request)
+  await performRequest("GET");
+  await testClipboardContent(`await fetch("http://example.com/browser/devtools/client/netmonitor/test/sjs_simple-test-server.sjs", {
+    "credentials": "omit",
+    "headers": {
+        "User-Agent": "${navigator.userAgent}",
+        "Accept": "*/*",
+        "Accept-Language": "en-US",
+        "X-Custom-Header-1": "Custom value",
+        "X-Custom-Header-2": "8.8.8.8",
+        "X-Custom-Header-3": "Mon, 3 Mar 2014 11:11:11 GMT",
+        "Pragma": "no-cache",
+        "Cache-Control": "no-cache"
+    },
+    "referrer": "http://example.com/browser/devtools/client/netmonitor/test/html_copy-as-curl.html",
+    "method": "GET",
+    "mode": "cors"
+});`);
+
+  await teardown(monitor);
+
+  async function performRequest(method, payload) {
+    const waitRequest = waitForNetworkEvents(monitor, 1);
+    await ContentTask.spawn(tab.linkedBrowser, {
+      url: SIMPLE_SJS,
+      method_: method,
+      payload_: payload,
+    }, async function({url, method_, payload_}) {
+      content.wrappedJSObject.performRequest(url, method_, payload_);
+    });
+    await waitRequest;
+  }
+
+  async function testClipboardContent(expectedResult) {
+    const { document } = monitor.panelWin;
+
+    const items = document.querySelectorAll(".request-list-item");
+    EventUtils.sendMouseEvent({ type: "mousedown" }, items[items.length - 1]);
+    EventUtils.sendMouseEvent({ type: "contextmenu" },
+      document.querySelectorAll(".request-list-item")[0]);
+
+    /* Ensure that the copy as fetch option is always visible */
+    const copyAsFetchNode = monitor.panelWin.parent.document
+      .querySelector("#request-list-context-copy-as-fetch");
+    is(!!copyAsFetchNode, true,
+      "The \"Copy as Fetch\" context menu item should not be hidden.");
+
+    await waitForClipboardPromise(function setup() {
+      copyAsFetchNode.click();
+    }, function validate(result) {
+      if (typeof result !== "string") {
+        return false;
+      }
+
+      return expectedResult === result;
+    });
+
+    info("Clipboard contains a fetch command for item " + (items.length - 1));
+  }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_use_as_fetch.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if Use as Fetch works.
+ */
+
+add_task(async function() {
+  const { tab, monitor, toolbox } = await initNetMonitor(CURL_URL);
+  info("Starting test... ");
+
+  // GET request, no cookies (first request)
+  await performRequest("GET");
+  await testConsoleInput(`await fetch("http://example.com/browser/devtools/client/netmonitor/test/sjs_simple-test-server.sjs", {
+    "credentials": "omit",
+    "headers": {
+        "User-Agent": "${navigator.userAgent}",
+        "Accept": "*/*",
+        "Accept-Language": "en-US",
+        "X-Custom-Header-1": "Custom value",
+        "X-Custom-Header-2": "8.8.8.8",
+        "X-Custom-Header-3": "Mon, 3 Mar 2014 11:11:11 GMT",
+        "Pragma": "no-cache",
+        "Cache-Control": "no-cache"
+    },
+    "referrer": "http://example.com/browser/devtools/client/netmonitor/test/html_copy-as-curl.html",
+    "method": "GET",
+    "mode": "cors"
+});`);
+
+  await teardown(monitor);
+
+  async function performRequest(method, payload) {
+    const waitRequest = waitForNetworkEvents(monitor, 1);
+    await ContentTask.spawn(tab.linkedBrowser, {
+      url: SIMPLE_SJS,
+      method_: method,
+      payload_: payload,
+    }, async function({url, method_, payload_}) {
+      content.wrappedJSObject.performRequest(url, method_, payload_);
+    });
+    await waitRequest;
+  }
+
+  async function testConsoleInput(expectedResult) {
+    const { document } = monitor.panelWin;
+
+    const items = document.querySelectorAll(".request-list-item");
+    EventUtils.sendMouseEvent({ type: "mousedown" }, items[items.length - 1]);
+    EventUtils.sendMouseEvent({ type: "contextmenu" },
+      document.querySelectorAll(".request-list-item")[0]);
+
+    /* Ensure that the use as fetch option is always visible */
+    const useAsFetchNode = monitor.panelWin.parent.document
+      .querySelector("#request-list-context-use-as-fetch");
+    is(!!useAsFetchNode, true,
+      "The \"Use as Fetch\" context menu item should not be hidden.");
+
+    useAsFetchNode.click();
+    await toolbox.once("split-console");
+    const hud = toolbox.getPanel("webconsole").hud;
+    await hud.jsterm.once("set-input-value");
+
+    is(hud.getInputValue(), expectedResult,
+      "Console input contains fetch request for item " + (items.length - 1));
+  }
+});