Bug 1308441 - Support image preview r?honza draft
authorRicky Chien <ricky060709@gmail.com>
Mon, 06 Mar 2017 18:27:11 +0800
changeset 494054 7f66dda3b70415f264ccf1645740eab9374a8c31
parent 494029 22c927990b6d9346ec4401e9ecb6bc15d5b38e8d
child 547992 f25f5f8852e858e9428ce618ae8aeaff25c8b879
push id47915
push userbmo:rchien@mozilla.com
push dateMon, 06 Mar 2017 13:42:37 +0000
reviewershonza
bugs1308441
milestone54.0a1
Bug 1308441 - Support image preview r?honza MozReview-Commit-ID: 6rzKFaZZvnk
devtools/client/netmonitor/components/request-list-column-file.js
devtools/client/netmonitor/components/request-list.js
devtools/client/netmonitor/request-list-tooltip.js
devtools/client/netmonitor/test/browser.ini
devtools/client/netmonitor/test/browser_net_icon-preview.js
devtools/client/netmonitor/test/browser_net_image-tooltip.js
devtools/client/netmonitor/test/html_image-tooltip-test-page.html
devtools/client/themes/netmonitor.css
--- a/devtools/client/netmonitor/components/request-list-column-file.js
+++ b/devtools/client/netmonitor/components/request-list-column-file.js
@@ -11,17 +11,17 @@ const {
   PropTypes,
 } = require("devtools/client/shared/vendor/react");
 const { propertiesEqual } = require("../utils/request-utils");
 
 // Components
 const Column = createFactory(require("devtools/client/shared/vendor/react-virtualized").Column);
 const RequestListColumnHeader = createFactory(require("./request-list-column-header"));
 
-const { div } = DOM;
+const { div, img } = DOM;
 const UPDATED_PROPS = [
   "responseContentDataUri",
   "urlDetails",
 ];
 
 /**
  * Request list file column component
  * Describes the header and cell contents of a table column
@@ -52,22 +52,28 @@ const FileColumnCell = createFactory(cre
   },
 
   shouldComponentUpdate(nextProps) {
     return !propertiesEqual(UPDATED_PROPS, this.props.rowData, nextProps.rowData);
   },
 
   render() {
     let { rowData } = this.props;
-    let { urlDetails } = rowData;
+    let { urlDetails, responseContentDataUri } = rowData;
 
     return (
-      div({
-        className: "requests-list-url subitem-label",
-        title: urlDetails.unicodeUrl,
-      },
-        urlDetails.baseNameWithQuery,
+      div({},
+        img({
+          className: "requests-list-file-icon",
+          src: responseContentDataUri || "chrome://devtools/skin/images/item-toggle.svg",
+        }),
+        div({
+          className: "subitem-label requests-list-url",
+          title: urlDetails.unicodeUrl,
+        },
+          urlDetails.baseNameWithQuery,
+        ),
       )
     );
   }
 }));
 
 module.exports = RequestListColumnFile;
--- a/devtools/client/netmonitor/components/request-list.js
+++ b/devtools/client/netmonitor/components/request-list.js
@@ -11,17 +11,20 @@ const {
   PropTypes,
 } = require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const SortDirection = require("devtools/client/shared/vendor/react-virtualized").SortDirection;
 const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
 const Actions = require("../actions/index");
 const { ACTIVITY_TYPE } = require("../constants");
 const RequestListContextMenu = require("../request-list-context-menu");
-const { setTooltipStackTraceContent } = require("../request-list-tooltip");
+const {
+  setTooltipImageContent,
+  setTooltipStackTraceContent,
+} = require("../request-list-tooltip");
 const {
   getDisplayedRequests,
   getSortedRequests,
   getWaterfallScale,
 } = require("../selectors/index");
 
 // Components
 const AutoSizer = createFactory(require("devtools/client/shared/vendor/react-virtualized").AutoSizer);
@@ -145,17 +148,19 @@ const RequestList = createClass({
       return false;
     }
 
     let requestItem = this.props.displayedRequests.find(r => r.id === itemId);
     if (!requestItem) {
       return false;
     }
 
-    if (requestItem.cause && target.closest(".requests-list-cause-stack")) {
+    if (requestItem.responseContent && target.closest(".requests-list-file-icon")) {
+      return setTooltipImageContent(tooltip, itemEl, requestItem);
+    } else if (requestItem.cause && target.closest(".requests-list-cause-stack")) {
       return setTooltipStackTraceContent(tooltip, requestItem);
     }
 
     return false;
   },
 
   onKeyDown(evt) {
     let { displayedRequests } = this.props;
--- a/devtools/client/netmonitor/request-list-tooltip.js
+++ b/devtools/client/netmonitor/request-list-tooltip.js
@@ -1,21 +1,45 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+const {
+  setImageTooltip,
+  getImageDimensions,
+} = require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
 const { WEBCONSOLE_L10N } = require("./utils/l10n");
+const { formDataURI } = require("./utils/request-utils");
 
 // px
+const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400;
+// px
 const REQUESTS_TOOLTIP_STACK_TRACE_WIDTH = 600;
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
+async function setTooltipImageContent(tooltip, itemEl, requestItem) {
+  let { mimeType, text, encoding } = requestItem.responseContent.content;
+
+  if (!mimeType || !mimeType.includes("image/")) {
+    return false;
+  }
+
+  let string = await window.gNetwork.getString(text);
+  let src = formDataURI(mimeType, encoding, string);
+  let maxDim = REQUESTS_TOOLTIP_IMAGE_MAX_DIM;
+  let { naturalWidth, naturalHeight } = await getImageDimensions(tooltip.doc, src);
+  let options = { maxDim, naturalWidth, naturalHeight };
+  setImageTooltip(tooltip, tooltip.doc, src, options);
+
+  return itemEl.querySelector(".requests-list-file-icon");
+}
+
 async function setTooltipStackTraceContent(tooltip, requestItem) {
   let {stacktrace} = requestItem.cause;
 
   if (!stacktrace || stacktrace.length == 0) {
     return false;
   }
 
   let doc = tooltip.doc;
@@ -72,10 +96,11 @@ async function setTooltipStackTraceConte
   }
 
   tooltip.setContent(el, {width: REQUESTS_TOOLTIP_STACK_TRACE_WIDTH});
 
   return true;
 }
 
 module.exports = {
+  setTooltipImageContent,
   setTooltipStackTraceContent,
 };
--- a/devtools/client/netmonitor/test/browser.ini
+++ b/devtools/client/netmonitor/test/browser.ini
@@ -3,16 +3,17 @@ tags = devtools
 subsuite = devtools
 support-files =
   dropmarker.svg
   head.js
   html_cause-test-page.html
   html_content-type-test-page.html
   html_content-type-without-cache-test-page.html
   html_brotli-test-page.html
+  html_image-tooltip-test-page.html
   html_cors-test-page.html
   html_custom-get-page.html
   html_cyrillic-test-page.html
   html_frame-test-page.html
   html_frame-subdocument.html
   html_filter-test-page.html
   html_infinite-get-page.html
   html_json-b64.html
@@ -101,16 +102,18 @@ skip-if = (os == 'linux' && bits == 32 &
 skip-if = (os == 'linux' && debug && bits == 32) # Bug 1321434
 [browser_net_filter-01.js]
 skip-if = (os == 'linux' && debug && bits == 32) # Bug 1303439
 [browser_net_filter-02.js]
 [browser_net_filter-03.js]
 [browser_net_filter-04.js]
 [browser_net_footer-summary.js]
 [browser_net_html-preview.js]
+[browser_net_icon-preview.js]
+[browser_net_image-tooltip.js]
 [browser_net_json-b64.js]
 [browser_net_json-null.js]
 [browser_net_json-long.js]
 [browser_net_json-malformed.js]
 [browser_net_json_custom_mime.js]
 [browser_net_json_text_mime.js]
 [browser_net_jsonp.js]
 [browser_net_large-response.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_icon-preview.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if image responses show a thumbnail in the requests menu.
+ */
+
+add_task(function* () {
+  let { tab, monitor } = yield initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL);
+  info("Starting test... ");
+
+  let { document, gStore, windowRequire, NetMonitorController } =
+    monitor.panelWin;
+  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
+  let { ACTIVITY_TYPE, EVENTS } = windowRequire("devtools/client/netmonitor/constants");
+
+  gStore.dispatch(Actions.batchEnable(false));
+
+  let wait = waitForEvents();
+  yield performRequests();
+  yield wait;
+
+  info("Checking the image thumbnail when all items are shown.");
+  checkImageThumbnail(5);
+
+  gStore.dispatch(Actions.toggleRequestFilterType("images"));
+  info("Checking the image thumbnail when only images are shown.");
+  checkImageThumbnail(0);
+
+  info("Reloading the debuggee and performing all requests again...");
+  wait = waitForEvents();
+  yield reloadAndPerformRequests();
+  yield wait;
+
+  info("Checking the image thumbnail after a reload.");
+  checkImageThumbnail(0);
+
+  yield teardown(monitor);
+
+  function waitForEvents() {
+    return promise.all([
+      waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS),
+      monitor.panelWin.once(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED)
+    ]);
+  }
+
+  function performRequests() {
+    return ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+      content.wrappedJSObject.performRequests();
+    });
+  }
+
+  function* reloadAndPerformRequests() {
+    yield NetMonitorController.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED);
+    yield performRequests();
+  }
+
+  function checkImageThumbnail(requestIndex) {
+    is(document.querySelectorAll(".requests-list-file-icon")[requestIndex].src,
+      TEST_IMAGE_DATA_URI,
+      "The image requests-list-icon thumbnail is displayed correctly.");
+  }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_image-tooltip.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const IMAGE_TOOLTIP_URL = EXAMPLE_URL + "html_image-tooltip-test-page.html";
+const IMAGE_TOOLTIP_REQUESTS = 1;
+
+/**
+ * Tests if image responses show a popup in the requests menu when hovered.
+ */
+add_task(function* test() {
+  let { tab, monitor } = yield initNetMonitor(IMAGE_TOOLTIP_URL);
+  info("Starting test... ");
+
+  let { document, gStore, windowRequire, NetMonitorController } = monitor.panelWin;
+  let Actions = windowRequire("devtools/client/netmonitor/actions/index");
+  let { ACTIVITY_TYPE, EVENTS } = windowRequire("devtools/client/netmonitor/constants");
+
+  gStore.dispatch(Actions.batchEnable(false));
+
+  let onEvents = waitForNetworkEvents(monitor, IMAGE_TOOLTIP_REQUESTS);
+  let onThumbnail = monitor.panelWin.once(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
+  yield performRequests();
+  yield onEvents;
+  yield onThumbnail;
+
+  info("Checking the image thumbnail after a few requests were made...");
+  yield showTooltipAndVerify(document.querySelectorAll(".request-list-item")[0]);
+
+  // Hide tooltip before next test, to avoid the situation that tooltip covers
+  // the icon for the request of the next test.
+  info("Checking the image thumbnail gets hidden...");
+  yield hideTooltipAndVerify(document.querySelectorAll(".request-list-item")[0]);
+
+  // +1 extra document reload
+  onEvents = waitForNetworkEvents(monitor, IMAGE_TOOLTIP_REQUESTS + 1);
+  onThumbnail = monitor.panelWin.once(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
+
+  info("Reloading the debuggee and performing all requests again...");
+  yield NetMonitorController.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED);
+  yield performRequests();
+  yield onEvents;
+  yield onThumbnail;
+
+  info("Checking the image thumbnail after a reload.");
+  yield showTooltipAndVerify(document.querySelectorAll(".request-list-item")[1]);
+
+  info("Checking if the image thumbnail is hidden when mouse leaves the menu widget");
+  EventUtils.synthesizeMouse(document.body, 0, 0, { type: "mouseout" }, monitor.panelWin);
+  yield waitUntil(() => !document.querySelector(".tooltip-container.tooltip-visible"));
+
+  yield teardown(monitor);
+
+  function performRequests() {
+    return ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+      content.wrappedJSObject.performRequests();
+    });
+  }
+
+  /**
+   * Show a tooltip on the {target} and verify that it was displayed
+   * with the expected content.
+   */
+  function* showTooltipAndVerify(target) {
+    let anchor = target.querySelector(".requests-list-file-icon");
+    yield showTooltipOn(anchor);
+
+    info("Tooltip was successfully opened for the image request.");
+    is(document.querySelector(".tooltip-panel img").src, TEST_IMAGE_DATA_URI,
+      "The tooltip's image content is displayed correctly.");
+  }
+
+  /**
+   * Trigger a tooltip over an element by sending mousemove event.
+   * @return a promise that resolves when the tooltip is shown
+   */
+  function* showTooltipOn(element) {
+    let win = element.ownerDocument.defaultView;
+    EventUtils.synthesizeMouseAtCenter(element, { type: "mousemove" }, win);
+    yield waitUntil(() => document.querySelector(".tooltip-panel img"));
+  }
+
+  /**
+   * Hide a tooltip on the {target} and verify that it was closed.
+   */
+  function* hideTooltipAndVerify(target) {
+    // Hovering over the "method" column hides the tooltip.
+    let anchor = target.querySelector(".requests-list-method");
+    let win = anchor.ownerDocument.defaultView;
+    EventUtils.synthesizeMouseAtCenter(anchor, { type: "mousemove" }, win);
+
+    yield waitUntil(
+      () => !document.querySelector(".tooltip-container.tooltip-visible"));
+    info("Tooltip was successfully closed.");
+  }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_image-tooltip-test-page.html
@@ -0,0 +1,29 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+    <meta http-equiv="Pragma" content="no-cache" />
+    <meta http-equiv="Expires" content="0" />
+    <title>Network Monitor test page</title>
+  </head>
+
+  <body>
+    <p>tooltip test</p>
+
+    <script type="text/javascript">
+      /* exported performRequests */
+      "use strict";
+
+      function performRequests() {
+        let xhr = new XMLHttpRequest();
+        xhr.open("GET", "test-image.png?v=" + Math.random(), true);
+        xhr.send(null);
+      }
+    </script>
+  </body>
+
+</html>
\ No newline at end of file
--- a/devtools/client/themes/netmonitor.css
+++ b/devtools/client/themes/netmonitor.css
@@ -300,16 +300,25 @@
 }
 
 /* File column */
 
 .requests-list-file {
   text-align: left;
 }
 
+.requests-list-file-icon {
+  display: inline-block;
+  background: transparent;
+  width: 15px;
+  height: 15px;
+  margin-inline-end: 4px;
+  vertical-align: middle;
+}
+
 /* Domain column */
 
 .requests-list-domain {
   text-align: left;
 }
 
 .requests-list-domain-url {
   display: inline-block;