Bug 1498143 - Pass nodeActorID to screenshot actor to enable feature in iframes and shadowroots;r=pbro,yulia
authorJulian Descottes <jdescottes@mozilla.com>
Mon, 19 Nov 2018 13:27:51 +0000
changeset 503431 9ee66620694fe51d3713d28f689f88e6866b96eb
parent 503430 6904ca9f5d8f4623ab85deb8747337de01b2ddb3
child 503432 a1f1a5b1ba9ee5c5034f0769529989421115d3a4
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbro, yulia
bugs1498143
milestone65.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 1498143 - Pass nodeActorID to screenshot actor to enable feature in iframes and shadowroots;r=pbro,yulia Differential Revision: https://phabricator.services.mozilla.com/D12124
devtools/client/inspector/inspector.js
devtools/client/inspector/markup/test/browser.ini
devtools/client/inspector/markup/test/browser_markup_screenshot_node.js
devtools/client/inspector/markup/test/browser_markup_screenshot_node_iframe.js
devtools/client/inspector/markup/test/browser_markup_screenshot_node_shadowdom.js
devtools/client/inspector/markup/test/helper_screenshot_node.js
devtools/client/inspector/test/head.js
devtools/server/actors/screenshot.js
devtools/shared/screenshot/capture.js
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -2289,17 +2289,17 @@ Inspector.prototype = {
     // To avoid that, we have to hide it before taking the screenshot. The `hideBoxModel`
     // will do that, calling `hide` for the highlighter only if previously shown.
     await this.highlighter.hideBoxModel();
 
     const clipboardEnabled = Services.prefs
       .getBoolPref("devtools.screenshot.clipboard.enabled");
     const args = {
       file: true,
-      selector: this.selectionCssSelector,
+      nodeActorID: this.selection.nodeFront.actorID,
       clipboard: clipboardEnabled,
     };
     const screenshotFront = this.target.getFront("screenshot");
     const screenshot = await screenshotFront.capture(args);
     await saveScreenshot(this.panelWin, args, screenshot);
   },
 
   /**
--- a/devtools/client/inspector/markup/test/browser.ini
+++ b/devtools/client/inspector/markup/test/browser.ini
@@ -47,16 +47,17 @@ support-files =
   events_bundle.js.map
   events_original.js
   head.js
   helper_attributes_test_runner.js
   helper_diff.js
   helper_events_test_runner.js
   helper_markup_accessibility_navigation.js
   helper_outerhtml_test_runner.js
+  helper_screenshot_node.js
   helper_style_attr_test_runner.js
   lib_babel_6.21.0_min.js
   lib_jquery_1.0.js
   lib_jquery_1.1.js
   lib_jquery_1.2_min.js
   lib_jquery_1.3_min.js
   lib_jquery_1.4_min.js
   lib_jquery_1.6_min.js
@@ -165,16 +166,19 @@ skip-if = verify
 [browser_markup_node_names.js]
 [browser_markup_node_names_namespaced.js]
 [browser_markup_node_not_displayed_01.js]
 [browser_markup_node_not_displayed_02.js]
 [browser_markup_pagesize_01.js]
 [browser_markup_pagesize_02.js]
 [browser_markup_pseudo_on_reload.js]
 [browser_markup_remove_xul_attributes.js]
+[browser_markup_screenshot_node.js]
+[browser_markup_screenshot_node_iframe.js]
+[browser_markup_screenshot_node_shadowdom.js]
 [browser_markup_search_01.js]
 [browser_markup_shadowdom.js]
 [browser_markup_shadowdom_clickreveal.js]
 [browser_markup_shadowdom_clickreveal_scroll.js]
 [browser_markup_shadowdom_copy_paths.js]
 subsuite = clipboard
 [browser_markup_shadowdom_delete.js]
 [browser_markup_shadowdom_dynamic.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_screenshot_node.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_screenshot_node.js */
+
+"use strict";
+
+loadHelperScript("helper_screenshot_node.js");
+
+const TEST_URL = `data:text/html;charset=utf8,
+  <div id="blue-node" style="width:30px;height:30px;background:rgb(0, 0, 255)"></div>`;
+
+// Test that the "Screenshot Node" feature works with a regular node in the main document.
+add_task(async function() {
+  const { inspector, toolbox } = await openInspectorForURL(encodeURI(TEST_URL));
+
+  info("Select the blue node");
+  await selectNode("#blue-node", inspector);
+
+  info("Take a screenshot of the blue node and verify it looks as expected");
+  const blueScreenshot = await takeNodeScreenshot(inspector);
+  await assertSingleColorScreenshotImage(blueScreenshot, 30, 30, { r: 0, g: 0, b: 255 });
+
+  await toolbox.destroy();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_screenshot_node_iframe.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_screenshot_node.js */
+
+"use strict";
+
+loadHelperScript("helper_screenshot_node.js");
+
+const TEST_URL = `data:text/html;charset=utf8,
+  <iframe
+    src="data:text/html;charset=utf8,
+      <div style='width:30px;height:30px;background:rgb(255, 0, 0)'></div>"></iframe>`;
+
+// Test that the "Screenshot Node" feature works with a node inside an iframe.
+add_task(async function() {
+  const { inspector, toolbox } = await openInspectorForURL(encodeURI(TEST_URL));
+
+  info("Select the red node");
+  const redNode = await getNodeFrontInFrame("div", "iframe", inspector);
+  await selectNode(redNode, inspector);
+
+  info("Take a screenshot of the red node and verify it looks as expected");
+  const redScreenshot = await takeNodeScreenshot(inspector);
+  await assertSingleColorScreenshotImage(redScreenshot, 30, 30, { r: 255, g: 0, b: 0 });
+
+  await toolbox.destroy();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_screenshot_node_shadowdom.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_screenshot_node.js */
+
+"use strict";
+
+loadHelperScript("helper_screenshot_node.js");
+
+const TEST_URL = `data:text/html;charset=utf8,
+  <test-component></test-component>
+  <script>
+    'use strict';
+    customElements.define('test-component', class extends HTMLElement {
+      constructor() {
+        super();
+        let shadowRoot = this.attachShadow({mode: 'open'});
+        shadowRoot.innerHTML =
+          '<div style="width:30px;height:30px;background:rgb(0, 128, 0)"></div>';
+      }
+    });
+  </script>`;
+
+// Test that the "Screenshot Node" feature works with a node inside a shadow root.
+add_task(async function() {
+  const { inspector, toolbox } = await openInspectorForURL(encodeURI(TEST_URL));
+
+  info("Select the green node");
+  const greenNode = await getNodeFrontInShadowDom("div", "test-component", inspector);
+  await selectNode(greenNode, inspector);
+
+  info("Take a screenshot of the green node and verify it looks as expected");
+  const greenScreenshot = await takeNodeScreenshot(inspector);
+  await assertSingleColorScreenshotImage(greenScreenshot, 30, 30, { r: 0, g: 128, b: 0 });
+
+  await toolbox.destroy();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/helper_screenshot_node.js
@@ -0,0 +1,84 @@
+/* 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";
+
+function colorAt(image, x, y) {
+  // Create a test canvas element.
+  const HTML_NS = "http://www.w3.org/1999/xhtml";
+  const canvas = document.createElementNS(HTML_NS, "canvas");
+  canvas.width = image.width;
+  canvas.height = image.height;
+
+  // Draw the image in the canvas
+  const context = canvas.getContext("2d");
+  context.drawImage(image, 0, 0, image.width, image.height);
+
+  // Return the color found at the provided x,y coordinates as a "r, g, b" string.
+  const [r, g, b] = context.getImageData(x, y, 1, 1).data;
+  return { r, g, b };
+}
+
+function waitUntilScreenshot() {
+  return new Promise(async function(resolve) {
+    const { Downloads } = require("resource://gre/modules/Downloads.jsm");
+    const list = await Downloads.getList(Downloads.ALL);
+
+    const view = {
+      onDownloadAdded: download => {
+        download.whenSucceeded().then(() => {
+          resolve(download.target.path);
+          list.removeView(view);
+        });
+      },
+    };
+
+    await list.addView(view);
+  });
+}
+
+async function resetDownloads() {
+  info("Reset downloads");
+  const { Downloads } = require("resource://gre/modules/Downloads.jsm");
+  const publicList = await Downloads.getList(Downloads.PUBLIC);
+  const downloads = await publicList.getAll();
+  for (const download of downloads) {
+    publicList.remove(download);
+    await download.finalize(true);
+  }
+}
+
+async function takeNodeScreenshot(inspector) {
+  // Cleanup all downloads at the end of the test.
+  registerCleanupFunction(resetDownloads);
+
+  info("Call screenshotNode() and wait until the screenshot is found in the Downloads");
+  const whenScreenshotSucceeded = waitUntilScreenshot();
+  inspector.screenshotNode();
+  const filePath = await whenScreenshotSucceeded;
+
+  info("Create an image using the downloaded fileas source");
+  const image = new Image();
+  image.src = OS.Path.toFileURI(filePath);
+  await once(image, "load");
+
+  return image;
+}
+/* exported takeNodeScreenshot */
+
+/**
+ * Check that the provided image has the expected width, height, and color.
+ * NOTE: This test assumes that the image is only made of a single color and will only
+ * check one pixel.
+ */
+async function assertSingleColorScreenshotImage(image, width, height, { r, g, b }) {
+  is(image.width, width, "node screenshot has the expected width");
+  is(image.height, height, "node screenshot has the expected height");
+
+  const color = colorAt(image, 0, 0);
+  is(color.r, r, "node screenshot has the expected red component");
+  is(color.g, g, "node screenshot has the expected green component");
+  is(color.b, b, "node screenshot has the expected blue component");
+}
+/* exported assertSingleColorScreenshotImage */
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -231,16 +231,41 @@ var clickOnInspectMenuItem = async funct
  */
 var getNodeFrontInFrame = async function(selector, frameSelector,
                                                 inspector) {
   const iframe = await getNodeFront(frameSelector, inspector);
   const {nodes} = await inspector.walker.children(iframe);
   return inspector.walker.querySelector(nodes[0], selector);
 };
 
+/**
+ * Get the NodeFront for a node that matches a given css selector inside a shadow root.
+ *
+ * @param {String} selector
+ *        CSS selector of the node inside the shadow root.
+ * @param {String|NodeFront} hostSelector
+ *        Selector or front of the element to which the shadow root is attached.
+ * @param {InspectorPanel} inspector
+ *        The instance of InspectorPanel currently loaded in the toolbox
+ * @return {Promise} Resolves the node front when the inspector is updated with the new
+ *         node.
+ */
+var getNodeFrontInShadowDom = async function(selector, hostSelector, inspector) {
+  const hostFront = await getNodeFront(hostSelector, inspector);
+  const {nodes} = await inspector.walker.children(hostFront);
+
+  // Find the shadow root in the children of the host element.
+  const shadowRoot = nodes.filter(node => node.isShadowRoot)[0];
+  if (!shadowRoot) {
+    throw new Error("Could not find a shadow root under selector: " + hostSelector);
+  }
+
+  return inspector.walker.querySelector(shadowRoot, selector);
+};
+
 var focusSearchBoxUsingShortcut = async function(panelWin, callback) {
   info("Focusing search box");
   const searchBox = panelWin.document.getElementById("inspector-searchbox");
   const focused = once(searchBox, "focus");
 
   panelWin.focus();
 
   synthesizeKeyShortcut(INSPECTOR_L10N.getStr("inspector.searchHTML.key"));
--- a/devtools/server/actors/screenshot.js
+++ b/devtools/server/actors/screenshot.js
@@ -10,11 +10,19 @@ const {screenshotSpec} = require("devtoo
 
 exports.ScreenshotActor = protocol.ActorClassWithSpec(screenshotSpec, {
   initialize: function(conn, targetActor) {
     protocol.Actor.prototype.initialize.call(this, conn);
     this.document = targetActor.window.document;
   },
 
   capture: function(args) {
+    if (args.nodeActorID) {
+      const nodeActor = this.conn.getActor(args.nodeActorID);
+      if (!nodeActor) {
+        throw new Error(
+          `Screenshot actor failed to find Node actor for '${args.nodeActorID}'`);
+      }
+      args.rawNode = nodeActor.rawNode;
+    }
     return captureScreenshot(args, this.document);
   },
 });
--- a/devtools/shared/screenshot/capture.js
+++ b/devtools/shared/screenshot/capture.js
@@ -43,33 +43,36 @@ function captureScreenshot(args, documen
 
 exports.captureScreenshot = captureScreenshot;
 
 /**
  * This does the dirty work of creating a base64 string out of an
  * area of the browser window
  */
 function createScreenshotDataURL(document, args) {
-  const window = document.defaultView;
+  let window = document.defaultView;
   let left = 0;
   let top = 0;
   let width;
   let height;
   const currentX = window.scrollX;
   const currentY = window.scrollY;
 
   let filename = getFilename(args.filename);
 
   if (args.fullpage) {
     // Bug 961832: Screenshot shows fixed position element in wrong
     // position if we don't scroll to top
     window.scrollTo(0, 0);
     width = window.innerWidth + window.scrollMaxX - window.scrollMinX;
     height = window.innerHeight + window.scrollMaxY - window.scrollMinY;
     filename = filename.replace(".png", "-fullpage.png");
+  } else if (args.rawNode) {
+    window = args.rawNode.ownerDocument.defaultView;
+    ({ top, left, width, height } = getRect(window, args.rawNode, window));
   } else if (args.selector) {
     const node = window.document.querySelector(args.selector);
     ({ top, left, width, height } = getRect(window, node, window));
   } else {
     left = window.scrollX;
     top = window.scrollY;
     width = window.innerWidth;
     height = window.innerHeight;