Bug 905650 - Added ability to get the hash of a screenshot; r?ato
MozReview-Commit-ID: 3NL7nkqpG6I
--- 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();