Bug 905650 - Added ability to get the hash of a screenshot; r?ato draft
authorKim Brown <kbmoz1515@gmail.com>
Sun, 17 Apr 2016 21:37:14 -0400
changeset 357859 4fe0ce7db673bacda0cd68b53ad6b316c7a16c6a
parent 357858 7cefd969e315c6d672b88d38f9770be243ee0691
child 519723 a402d50e931c3208b1cdb215074afd0b37ffbc36
push id16865
push userbmo:kbmoz1515@gmail.com
push dateFri, 29 Apr 2016 16:18:50 +0000
reviewersato
bugs905650
milestone49.0a1
Bug 905650 - Added ability to get the hash of a screenshot; r?ato MozReview-Commit-ID: 3NL7nkqpG6I
testing/marionette/capture.js
testing/marionette/client/marionette_driver/marionette.py
testing/marionette/driver.js
testing/marionette/harness/marionette/tests/unit/test_screenshot.py
testing/marionette/listener.js
--- a/testing/marionette/capture.js
+++ b/testing/marionette/capture.js
@@ -1,14 +1,17 @@
 /* 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 {utils: Cu} = Components;
+Cu.importGlobalProperties(["crypto"]);
+
 this.EXPORTED_SYMBOLS = ["capture"];
 
 const CONTEXT_2D = "2d";
 const BG_COLOUR = "rgb(255,255,255)";
 const PNG_MIME = "image/png";
 const XHTML_NS = "http://www.w3.org/1999/xhtml";
 
 /** Provides primitives to capture screenshots. */
@@ -135,8 +138,45 @@ capture.highlight_ = function(context, h
  *
  * @return {string}
  *     A Base64 encoded string.
  */
 capture.toBase64 = function(canvas) {
   let u = canvas.toDataURL(PNG_MIME);
   return u.substring(u.indexOf(",") + 1);
 };
+
+/**
+* Hash the contents of an HTMLCanvasElement to a SHA-256 hex digest.
+*
+* @param {HTMLCanvasElement} canvas
+*     The canvas to encode.
+*
+* @return {string}
+*     A hex digest of the SHA-256 hash of the base64 encoded string.
+*/
+capture.toHash = function(canvas) {
+  let u = capture.toBase64(canvas);
+  let buffer = new TextEncoder("utf-8").encode(u);
+  return crypto.subtle.digest("SHA-256", buffer).then(hash => hex(hash));
+};
+
+/**
+* Convert buffer into to hex.
+*
+* @param {ArrayBuffer} buffer
+*     The buffer containing the data to convert to hex.
+*
+* @return {string}
+*     A hex digest of the input buffer.
+*/
+function hex(buffer) {
+  let hexCodes = [];
+  let view = new DataView(buffer);
+  for (let i = 0; i < view.byteLength; i += 4) {
+    let value = view.getUint32(i);
+    let stringValue = value.toString(16);
+    let padding = '00000000';
+    let paddedValue = (padding + stringValue).slice(-padding.length);
+    hexCodes.push(paddedValue);
+  }
+  return hexCodes.join("");
+};
\ No newline at end of file
--- a/testing/marionette/client/marionette_driver/marionette.py
+++ b/testing/marionette/client/marionette_driver/marionette.py
@@ -1922,35 +1922,39 @@ class Marionette(object):
         :param element: The element to take a screenshot of.  If None, will
             take a screenshot of the current frame.
 
         :param highlights: A list of HTMLElement objects to draw a red
             box around in the returned screenshot.
 
         :param format: if "base64" (the default), returns the screenshot
             as a base64-string. If "binary", the data is decoded and
-            returned as raw binary.
+            returned as raw binary. If "hash", the data is hashed using
+            the SHA-256 algorithm and the result is returned as a hex digest.
 
         :param full: If True (the default), the capture area will be the
             complete frame. Else only the viewport is captured. Only applies
             when `element` is None.
         """
 
         if element:
             element = element.id
         lights = None
         if highlights:
             lights = [highlight.id for highlight in highlights]
 
         body = {"id": element,
                 "highlights": lights,
-                "full": full}
+                "full": full,
+                "hash": False}
+        if format == "hash":
+            body["hash"] = True
         data = self._send_message("takeScreenshot", body, key="value")
 
-        if format == "base64":
+        if format == "base64" or format == "hash":
             return data
         elif format == "binary":
             return base64.b64decode(data.encode("ascii"))
         else:
             raise ValueError("format parameter must be either 'base64'"
                              " or 'binary', not {0}".format(repr(format)))
 
     @property
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -2503,22 +2503,26 @@ GeckoDriver.prototype.clearImportedScrip
  *
  * If called in the chrome context, the screenshot will always represent the
  * entire viewport.
  *
  * @param {string} id
  *     Reference to a web element.
  * @param {string} highlights
  *     List of web elements to highlight.
+ * @param {boolean} hash
+ *     True if the user requests a hash of the image data.
  *
  * @return {string}
- *     PNG image encoded as base64 encoded string.
+ *     If {@code hash} is false, PNG image encoded as base64 encoded string. If
+ *     'hash' is True, hex digest of the SHA-256 hash of the base64 encoded
+ *     string.
  */
 GeckoDriver.prototype.takeScreenshot = function(cmd, resp) {
-  let {id, highlights, full} = cmd.parameters;
+  let {id, highlights, full, hash} = cmd.parameters;
   highlights = highlights || [];
 
   switch (this.context) {
     case Context.CHROME:
       let win = this.getCurrentWindow();
       let canvas = win.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
       let doc;
       if (this.appName == "B2G") {
@@ -2553,17 +2557,21 @@ GeckoDriver.prototype.takeScreenshot = f
       context.scale(scale, scale);
       context.drawWindow(win, 0, 0, width, height, "rgb(255,255,255)", flags);
       let dataUrl = canvas.toDataURL("image/png", "");
       let data = dataUrl.substring(dataUrl.indexOf(",") + 1);
       resp.body.value = data;
       break;
 
     case Context.CONTENT:
-      return this.listener.takeScreenshot(id, full, highlights);
+      if (hash) {
+        return this.listener.getScreenshotHash(id, full, highlights);
+      } else {
+        return this.listener.takeScreenshot(id, full, highlights);
+      }
   }
 };
 
 /**
  * Get the current browser orientation.
  *
  * Will return one of the valid primary orientation values
  * portrait-primary, landscape-primary, portrait-secondary, or
--- a/testing/marionette/harness/marionette/tests/unit/test_screenshot.py
+++ b/testing/marionette/harness/marionette/tests/unit/test_screenshot.py
@@ -1,13 +1,14 @@
 # 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/.
 
 import base64
+import hashlib
 import imghdr
 import struct
 import urllib
 
 from unittest import skip
 
 from marionette import MarionetteTestCase
 from marionette_driver.by import By
@@ -186,8 +187,15 @@ class Content(ScreenCaptureTestCase):
         el = self.marionette.find_element(By.TAG_NAME, "div")
         bin = self.marionette.screenshot(element=el, format="binary")
         enc = base64.b64encode(bin)
         self.assertEqual(ELEMENT, enc)
 
     def test_unknown_format(self):
         with self.assertRaises(ValueError):
             self.marionette.screenshot(format="cheese")
+
+    def test_hash_format(self):
+        self.marionette.navigate(box)
+        el = self.marionette.find_element(By.TAG_NAME, "div")
+        content = self.marionette.screenshot(element=el, format="hash")
+        hash = hashlib.sha256(ELEMENT).hexdigest()
+        self.assertEqual(content, hash)
\ No newline at end of file
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -217,16 +217,17 @@ var findElementsContentFn = dispatch(fin
 var isElementSelectedFn = dispatch(isElementSelected);
 var clearElementFn = dispatch(clearElement);
 var isElementDisplayedFn = dispatch(isElementDisplayed);
 var getElementValueOfCssPropertyFn = dispatch(getElementValueOfCssProperty);
 var switchToShadowRootFn = dispatch(switchToShadowRoot);
 var getCookiesFn = dispatch(getCookies);
 var singleTapFn = dispatch(singleTap);
 var takeScreenshotFn = dispatch(takeScreenshot);
+var getScreenshotHashFn = dispatch(getScreenshotHash);
 var actionChainFn = dispatch(actionChain);
 var multiActionFn = dispatch(multiAction);
 var addCookieFn = dispatch(addCookie);
 var deleteCookieFn = dispatch(deleteCookie);
 var deleteAllCookiesFn = dispatch(deleteAllCookies);
 
 /**
  * Start all message listeners
@@ -267,16 +268,17 @@ function startListeners() {
   addMessageListenerId("Marionette:switchToParentFrame", switchToParentFrame);
   addMessageListenerId("Marionette:switchToShadowRoot", switchToShadowRootFn);
   addMessageListenerId("Marionette:deleteSession", deleteSession);
   addMessageListenerId("Marionette:sleepSession", sleepSession);
   addMessageListenerId("Marionette:emulatorCmdResult", emulatorCmdResult);
   addMessageListenerId("Marionette:getAppCacheStatus", getAppCacheStatus);
   addMessageListenerId("Marionette:setTestName", setTestName);
   addMessageListenerId("Marionette:takeScreenshot", takeScreenshotFn);
+  addMessageListenerId("Marionette:getScreenshotHash", getScreenshotHashFn);
   addMessageListenerId("Marionette:addCookie", addCookieFn);
   addMessageListenerId("Marionette:getCookies", getCookiesFn);
   addMessageListenerId("Marionette:deleteAllCookies", deleteAllCookiesFn);
   addMessageListenerId("Marionette:deleteCookie", deleteCookieFn);
 }
 
 /**
  * Used during newSession and restart, called to set up the modal dialog listener in b2g
@@ -371,16 +373,17 @@ function deleteSession(msg) {
   removeMessageListenerId("Marionette:switchToParentFrame", switchToParentFrame);
   removeMessageListenerId("Marionette:switchToShadowRoot", switchToShadowRootFn);
   removeMessageListenerId("Marionette:deleteSession", deleteSession);
   removeMessageListenerId("Marionette:sleepSession", sleepSession);
   removeMessageListenerId("Marionette:emulatorCmdResult", emulatorCmdResult);
   removeMessageListenerId("Marionette:getAppCacheStatus", getAppCacheStatus);
   removeMessageListenerId("Marionette:setTestName", setTestName);
   removeMessageListenerId("Marionette:takeScreenshot", takeScreenshotFn);
+  removeMessageListenerId("Marionette:getScreenshotHash", getScreenshotHashFn);
   removeMessageListenerId("Marionette:addCookie", addCookieFn);
   removeMessageListenerId("Marionette:getCookies", getCookiesFn);
   removeMessageListenerId("Marionette:deleteAllCookies", deleteAllCookiesFn);
   removeMessageListenerId("Marionette:deleteCookie", deleteCookieFn);
   if (isB2G) {
     content.removeEventListener("mozbrowsershowmodalprompt", modalHandler, false);
   }
   elementManager.reset();
@@ -1746,16 +1749,59 @@ function emulatorCmdResult(msg) {
  * @param {Array.<UUID>=} highlights
  *     Draw a border around the elements found by their web element
  *     references.
  *
  * @return {string}
  *     Base64 encoded string of an image/png type.
  */
 function takeScreenshot(id, full=true, highlights=[]) {
+  let canvas = screenshot(id, full, highlights);
+  return capture.toBase64(canvas);
+}
+
+/**
+* Perform a screen capture in content context.
+*
+* @param {UUID=} id
+*     Optional web element reference of an element to take a screenshot
+*     of.
+* @param {boolean=} full
+*     True to take a screenshot of the entire document element.  Is not
+*     considered if {@code id} is not defined.  Defaults to true.
+* @param {Array.<UUID>=} highlights
+*     Draw a border around the elements found by their web element
+*     references.
+*
+* @return {string}
+*     Hex Digest of a SHA-256 hash of the base64 encoded string of an
+*     image/png type.
+*/
+function getScreenshotHash(id, full=true, highlights=[]) {
+  let canvas = screenshot(id, full, highlights);
+  return capture.toHash(canvas);
+}
+
+/**
+* Perform a screen capture in content context.
+*
+* @param {UUID=} id
+*     Optional web element reference of an element to take a screenshot
+*     of.
+* @param {boolean=} full
+*     True to take a screenshot of the entire document element.  Is not
+*     considered if {@code id} is not defined.  Defaults to true.
+* @param {Array.<UUID>=} highlights
+*     Draw a border around the elements found by their web element
+*     references.
+*
+* @return {HTMLCanvasElement}
+*     The canvas element to be encoded or hashed.
+*/
+function screenshot(id, full=true, highlights=[]) {
   let canvas;
 
   let highlightEls = [];
   for (let h of highlights) {
     let el = elementManager.getKnownElement(h, curContainer);
     highlightEls.push(el);
   }
 
@@ -1770,13 +1816,13 @@ function takeScreenshot(id, full=true, h
       node = elementManager.getKnownElement(id, curContainer);
     } else {
       node = curContainer.frame.document.documentElement;
     }
 
     canvas = capture.element(node, highlightEls);
   }
 
-  return capture.toBase64(canvas);
+  return canvas;
 }
 
 // Call register self when we get loaded
 registerSelf();