Bug 810259 - Add support for getting screenshot from Marionette. r=mdas, a=NPOTB
authorDavid Burns <dburns@mozilla.com>
Fri, 30 Nov 2012 23:25:26 +0000
changeset 121812 036243b85532236f801e3021109fe480af01f4d2
parent 121811 a63f586248174df16503deece65b68a5348ae476
child 121813 4c2ca6b7c432f48402b572ba08a70999c522feb1
push id1997
push userakeybl@mozilla.com
push dateMon, 07 Jan 2013 21:25:26 +0000
treeherdermozilla-beta@4baf45cdcf21 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmdas, NPOTB
bugs810259
milestone19.0a2
Bug 810259 - Add support for getting screenshot from Marionette. r=mdas, a=NPOTB
testing/marionette/client/marionette/marionette.py
testing/marionette/client/marionette/tests/unit/test_screenshot.py
testing/marionette/client/marionette/tests/unit/unit-tests.ini
testing/marionette/marionette-actors.js
testing/marionette/marionette-listener.js
--- a/testing/marionette/client/marionette/marionette.py
+++ b/testing/marionette/client/marionette/marionette.py
@@ -493,8 +493,13 @@ class Marionette(object):
         js = ''
         with open(js_file, 'r') as f:
             js = f.read()
         return self._send_message('importScript', 'ok', script=js)
 
     @property
     def application_cache(self):
         return ApplicationCache(self)
+
+    def screenshot(self, element=None, highlights=None):
+        if element is not None:
+            element = element.id
+        return self._send_message("screenShot", 'value', element=element, highlights=highlights)
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/test_screenshot.py
@@ -0,0 +1,18 @@
+from marionette_test import MarionetteTestCase
+
+
+class ScreenshotTests(MarionetteTestCase):
+
+    def testWeCanTakeAScreenShotOfAnElement(self):
+        test_url = self.marionette.absolute_url('html5Page.html')
+        self.marionette.navigate(test_url)
+        el = self.marionette.find_element('id', 'red')
+        self.assertEqual('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAVUlEQVRoge3PsQ0AIAzAsI78fzBwBhHykD2ePev80LweAAGJB1ILpBZILZBaILVAaoHUAqkFUgukFkgtkFogtUBqgdQCqQVSC6QWSC2QWiC1QGp9A7ma+7nyXgOpzQAAAABJRU5ErkJggg==',
+                         self.marionette.screenshot(element=el))
+
+    def testWeCanTakeAScreenShotEntireCanvas(self):
+        test_url = self.marionette.absolute_url('html5Page.html')
+        self.marionette.navigate(test_url)
+        self.assertTrue('data:image/png;base64,iVBORw0KGgo' in
+                        self.marionette.screenshot())
+
--- a/testing/marionette/client/marionette/tests/unit/unit-tests.ini
+++ b/testing/marionette/client/marionette/tests/unit/unit-tests.ini
@@ -46,9 +46,9 @@ b2g = false
 [test_specialpowers.py]
 [test_switch_frame.py]
 b2g = false
 
 [test_window_management.py]
 b2g = false
 
 [test_appcache.py]
-
+[test_screenshot.py]
--- a/testing/marionette/marionette-actors.js
+++ b/testing/marionette/marionette-actors.js
@@ -1590,16 +1590,25 @@ MarionetteDriverActor.prototype = {
       this.sendOk();
     }
     else {
       this.sendAsync("importScript", {script: aRequest.script});
     }
   },
 
   /**
+   * Takes a screenshot of a DOM node. If there is no node given a screenshot
+   * of the window will be taken.
+   */
+  screenShot: function MDA_saveScreenshot(aRequest) {
+    this.sendAsync("screenShot", {element: aRequest.element,
+                                  highlights: aRequest.highlights});
+  },
+
+  /**
    * Helper function to convert an outerWindowID into a UID that Marionette
    * tracks.
    */
   generateFrameId: function MDA_generateFrameId(id) {
     let uid = id + (appName == "B2G" ? "-b2g" : "");
     return uid;
   },
 
@@ -1766,17 +1775,18 @@ MarionetteDriverActor.prototype.requestT
   "getWindows":  MarionetteDriverActor.prototype.getWindows,
   "switchToFrame": MarionetteDriverActor.prototype.switchToFrame,
   "switchToWindow": MarionetteDriverActor.prototype.switchToWindow,
   "deleteSession": MarionetteDriverActor.prototype.deleteSession,
   "emulatorCmdResult": MarionetteDriverActor.prototype.emulatorCmdResult,
   "importScript": MarionetteDriverActor.prototype.importScript,
   "getAppCacheStatus": MarionetteDriverActor.prototype.getAppCacheStatus,
   "closeWindow": MarionetteDriverActor.prototype.closeWindow,
-  "setTestName": MarionetteDriverActor.prototype.setTestName
+  "setTestName": MarionetteDriverActor.prototype.setTestName,
+  "screenShot": MarionetteDriverActor.prototype.screenShot
 };
 
 /**
  * Creates a BrowserObj. BrowserObjs handle interactions with the
  * browser, according to the current environment (desktop, b2g, etc.)
  *
  * @param nsIDOMWindow win
  *        The window whose browser needs to be accessed
--- a/testing/marionette/marionette-listener.js
+++ b/testing/marionette/marionette-listener.js
@@ -115,16 +115,17 @@ function startListeners() {
   addMessageListenerId("Marionette:switchToFrame", switchToFrame);
   addMessageListenerId("Marionette:deleteSession", deleteSession);
   addMessageListenerId("Marionette:sleepSession", sleepSession);
   addMessageListenerId("Marionette:emulatorCmdResult", emulatorCmdResult);
   addMessageListenerId("Marionette:importScript", importScript);
   addMessageListenerId("Marionette:getAppCacheStatus", getAppCacheStatus);
   addMessageListenerId("Marionette:setTestName", setTestName);
   addMessageListenerId("Marionette:setState", setState);
+  addMessageListenerId("Marionette:screenShot", screenShot);
 }
 
 /**
  * Called when we start a new session. It registers the
  * current environment, and resets all values
  */
 function newSession(msg) {
   isB2G = msg.json.B2G;
@@ -195,16 +196,17 @@ function deleteSession(msg) {
   removeMessageListenerId("Marionette:switchToFrame", switchToFrame);
   removeMessageListenerId("Marionette:deleteSession", deleteSession);
   removeMessageListenerId("Marionette:sleepSession", sleepSession);
   removeMessageListenerId("Marionette:emulatorCmdResult", emulatorCmdResult);
   removeMessageListenerId("Marionette:importScript", importScript);
   removeMessageListenerId("Marionette:getAppCacheStatus", getAppCacheStatus);
   removeMessageListenerId("Marionette:setTestName", setTestName);
   removeMessageListenerId("Marionette:setState", setState);
+  removeMessageListenerId("Marionette:screenShot", screenShot);
   this.elementManager.reset();
   // reset frame to the top-most frame
   curWindow = content;
   curWindow.focus();
   try {
     importedScripts.remove(false);
   }
   catch (e) {
@@ -939,11 +941,93 @@ function importScript(msg) {
     file = FileUtils.openFileOutputStream(importedScripts, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE);
     importedScripts.permissions = parseInt("0666", 8); //actually set permissions
   }
   file.write(msg.json.script, msg.json.script.length);
   file.close();
   sendOk();
 }
 
+/**
+ * Saves a screenshot and returns a Base64 string
+ */
+function screenShot(msg) {
+  let node = null;
+  if (msg.json.element) {
+    try {
+      node = elementManager.getKnownElement(msg.json.element, curWindow)
+    }
+    catch (e) {
+      sendResponse(e.message, e.code, e.stack);
+      return;
+    }
+  }
+  else {
+      node = curWindow;
+  }
+  let highlights = msg.json.highlights;
+
+  var document = curWindow.document;
+  var rect, win, width, height, left, top, needsOffset;
+  // node can be either a window or an arbitrary DOM node
+  if (node == curWindow) {
+    // node is a window
+    win = node;
+    width = win.innerWidth;
+    height = win.innerHeight;
+    top = 0;
+    left = 0;
+    // offset needed for highlights to take 'outerHeight' of window into account
+    needsOffset = true;
+  }
+  else {
+    // node is an arbitrary DOM node
+    win = node.ownerDocument.defaultView;
+    rect = node.getBoundingClientRect();
+    width = rect.width;
+    height = rect.height;
+    top = rect.top;
+    left = rect.left;
+    // offset for highlights not needed as they will be relative to this node
+    needsOffset = false;
+  }
+
+  var canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+  canvas.width = width;
+  canvas.height = height;
+  var ctx = canvas.getContext("2d");
+  // Draws the DOM contents of the window to the canvas
+  ctx.drawWindow(win, left, top, width, height, 'rgb(255,255,255)');
+
+  // This section is for drawing a red rectangle around each element passed in via the highlights array
+  if (highlights) {
+    ctx.lineWidth = "2";
+    ctx.strokeStyle = "red";
+    ctx.save();
+
+    for (var i = 0; i < highlights.length; ++i) {
+      var elem = highlights[i];
+      rect = elem.getBoundingClientRect();
+
+      var offsetY = 0, offsetX = 0;
+      if (needsOffset) {
+        var offset = getChromeOffset(elem);
+        offsetX = offset.x;
+        offsetY = offset.y;
+      } else {
+        // Don't need to offset the window chrome, just make relative to containing node
+        offsetY = -top;
+        offsetX = -left;
+      }
+
+      // Draw the rectangle
+      ctx.strokeRect(rect.left + offsetX, rect.top + offsetY, rect.width, rect.height);
+    }
+  }
+
+  // Return the Base64 String back to the client bindings and they can manage
+  // saving the file to disk if it is required
+  sendResponse({value:canvas.toDataURL("image/png","")});
+}
+
 //call register self when we get loaded
 registerSelf();