Bug 1239461 - Screenshot button for taking a screenshot of the current viewport; r=jryans
authorMatteo Ferretti <mferretti@mozilla.com>
Wed, 30 Mar 2016 22:31:37 +0200
changeset 291868 e003ad2de364adc21bd7ed89715bc6f397ba9739
parent 291867 70a08c72c3e89eaf689af284e30fd574676ced70
child 291869 4209cc859abcb5986b7b0e1406e9a00b5fff17a7
push id74700
push usercbook@mozilla.com
push dateWed, 06 Apr 2016 10:06:38 +0000
treeherdermozilla-inbound@49d808d13f4f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjryans
bugs1239461
milestone48.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 1239461 - Screenshot button for taking a screenshot of the current viewport; r=jryans MozReview-Commit-ID: AMbzmf1uO0P
devtools/client/locales/en-US/responsive.properties
devtools/client/responsive.html/actions/index.js
devtools/client/responsive.html/actions/moz.build
devtools/client/responsive.html/actions/screenshot.js
devtools/client/responsive.html/app.js
devtools/client/responsive.html/audio/camera-click.mp3
devtools/client/responsive.html/audio/moz.build
devtools/client/responsive.html/components/device-selector.js
devtools/client/responsive.html/components/global-toolbar.js
devtools/client/responsive.html/components/moz.build
devtools/client/responsive.html/components/resizable-viewport.js
devtools/client/responsive.html/components/utils/l10n.js
devtools/client/responsive.html/components/utils/moz.build
devtools/client/responsive.html/components/viewport.js
devtools/client/responsive.html/components/viewports.js
devtools/client/responsive.html/images/moz.build
devtools/client/responsive.html/images/screenshot.svg
devtools/client/responsive.html/index.css
devtools/client/responsive.html/index.js
devtools/client/responsive.html/moz.build
devtools/client/responsive.html/reducers.js
devtools/client/responsive.html/reducers/moz.build
devtools/client/responsive.html/reducers/screenshot.js
devtools/client/responsive.html/test/browser/browser.ini
devtools/client/responsive.html/test/browser/browser_screenshot_button.js
devtools/client/responsive.html/types.js
devtools/client/responsive.html/utils/l10n.js
devtools/client/responsive.html/utils/moz.build
--- a/devtools/client/locales/en-US/responsive.properties
+++ b/devtools/client/locales/en-US/responsive.properties
@@ -16,8 +16,17 @@
 responsive.title=Responsive Design Mode
 
 # LOCALIZATION NOTE (responsive.exit): tooltip text of the exit button.
 responsive.exit=Close Responsive Design Mode
 
 # LOCALIZATION NOTE (responsive.noDeviceSelected): placeholder text for the
 # device selector
 responsive.noDeviceSelected=no device selected
+
+# LOCALIZATION NOTE  (responsive.screenshot): tooltip of the screenshot button.
+responsive.screenshot=Take a screenshot of the viewport
+
+# LOCALIZATION NOTE (responsive.screenshotGeneratedFilename): The auto generated
+# filename.
+# The first argument (%1$S) is the date string in yyyy-mm-dd format and the
+# second argument (%2$S) is the time string in HH.MM.SS format.
+responsive.screenshotGeneratedFilename=Screen Shot %1$S at %2$S
--- a/devtools/client/responsive.html/actions/index.js
+++ b/devtools/client/responsive.html/actions/index.js
@@ -27,16 +27,22 @@ createEnum([
   "CHANGE_LOCATION",
 
   // Resize the viewport.
   "RESIZE_VIEWPORT",
 
   // Rotate the viewport.
   "ROTATE_VIEWPORT",
 
+  // Take a screenshot of the viewport.
+  "TAKE_SCREENSHOT_START",
+
+  // Indicates when the screenshot action ends.
+  "TAKE_SCREENSHOT_END",
+
 ], module.exports);
 
 /**
  * Create a simple enum-like object with keys mirrored to values from an array.
  * This makes comparison to a specfic value simpler without having to repeat and
  * mis-type the value.
  */
 function createEnum(array, target) {
--- a/devtools/client/responsive.html/actions/moz.build
+++ b/devtools/client/responsive.html/actions/moz.build
@@ -3,10 +3,11 @@
 # 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/.
 
 DevToolsModules(
     'devices.js',
     'index.js',
     'location.js',
+    'screenshot.js',
     'viewports.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/actions/screenshot.js
@@ -0,0 +1,89 @@
+/* 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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+const {
+  TAKE_SCREENSHOT_START,
+  TAKE_SCREENSHOT_END,
+} = require("./index");
+
+const { getRect } = require("devtools/shared/layout/utils");
+const { getFormatStr } = require("../utils/l10n");
+const { getToplevelWindow } = require("sdk/window/utils");
+const { Task: { spawn, async } } = require("resource://gre/modules/Task.jsm");
+
+const BASE_URL = "resource://devtools/client/responsive.html";
+const audioCamera = new window.Audio(`${BASE_URL}/audio/camera-click.mp3`);
+
+function getFileName() {
+  let date = new Date();
+  let month = ("0" + (date.getMonth() + 1)).substr(-2);
+  let day = ("0" + date.getDate()).substr(-2);
+  let dateString = [date.getFullYear(), month, day].join("-");
+  let timeString = date.toTimeString().replace(/:/g, ".").split(" ")[0];
+
+  return getFormatStr("responsive.screenshotGeneratedFilename", dateString,
+                      timeString);
+}
+
+function createScreenshotFor(node) {
+  let { top, left, width, height } = getRect(window, node, window);
+
+  const canvas = document.createElementNS(HTML_NS, "canvas");
+  const ctx = canvas.getContext("2d");
+  const ratio = window.devicePixelRatio;
+  canvas.width = width * ratio;
+  canvas.height = height * ratio;
+  ctx.scale(ratio, ratio);
+  ctx.drawWindow(window, left, top, width, height, "#fff");
+
+  return canvas.toDataURL("image/png", "");
+}
+
+function saveToFile(data, filename) {
+  return spawn(function* () {
+    const chromeWindow = getToplevelWindow(window);
+    const chromeDocument = chromeWindow.document;
+
+    // append .png extension to filename if it doesn't exist
+    filename = filename.replace(/\.png$|$/i, ".png");
+
+    chromeWindow.saveURL(data, filename, null,
+                         true, true,
+                         chromeDocument.documentURIObject, chromeDocument);
+  });
+}
+
+function simulateCameraEffects(node) {
+  audioCamera.play();
+  node.animate({ opacity: [ 0, 1 ] }, 500);
+}
+
+module.exports = {
+
+  takeScreenshot() {
+    return function* (dispatch, getState) {
+      yield dispatch({ type: TAKE_SCREENSHOT_START });
+
+      // Waiting the next repaint, to ensure the react components
+      // can be properly render after the action dispatched above
+      window.requestAnimationFrame(async(function* () {
+        let iframe = document.querySelector("iframe");
+        let data = createScreenshotFor(iframe);
+
+        simulateCameraEffects(iframe);
+
+        yield saveToFile(data, getFileName());
+
+        dispatch({ type: TAKE_SCREENSHOT_END });
+      }));
+    };
+  }
+
+};
--- a/devtools/client/responsive.html/app.js
+++ b/devtools/client/responsive.html/app.js
@@ -1,74 +1,90 @@
 /* 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/. */
 
+ /* eslint-env browser */
+
 "use strict";
 
 const { createClass, createFactory, PropTypes, DOM: dom } =
   require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 
 const {
   changeDevice,
   resizeViewport,
   rotateViewport
 } = require("./actions/viewports");
+const { takeScreenshot } = require("./actions/screenshot");
 const Types = require("./types");
 const Viewports = createFactory(require("./components/viewports"));
 const GlobalToolbar = createFactory(require("./components/global-toolbar"));
 
 let App = createClass({
 
   displayName: "App",
 
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
     location: Types.location.isRequired,
     viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
-    onExit: PropTypes.func.isRequired,
+    screenshot: PropTypes.shape(Types.screenshot).isRequired,
   },
 
   onChangeViewportDevice(id, device) {
     this.props.dispatch(changeDevice(id, device));
   },
 
+  onExit() {
+    window.postMessage({ type: "exit" }, "*");
+  },
+
   onResizeViewport(id, width, height) {
     this.props.dispatch(resizeViewport(id, width, height));
   },
 
   onRotateViewport(id) {
     this.props.dispatch(rotateViewport(id));
   },
 
+  onScreenshot() {
+    this.props.dispatch(takeScreenshot());
+  },
+
   render() {
     let {
       devices,
       location,
+      screenshot,
       viewports,
-      onExit,
     } = this.props;
 
     let {
       onChangeViewportDevice,
+      onExit,
       onResizeViewport,
       onRotateViewport,
+      onScreenshot,
     } = this;
 
     return dom.div(
       {
         id: "app",
       },
       GlobalToolbar({
+        screenshot,
         onExit,
+        onScreenshot,
       }),
       Viewports({
         devices,
         location,
+        screenshot,
         viewports,
         onChangeViewportDevice,
         onRotateViewport,
         onResizeViewport,
       })
     );
   },
 
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..6d9af013315d873e910ecf6e15bc298093dc1e99
GIT binary patch
literal 27634
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
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/audio/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+    'camera-click.mp3',
+)
--- a/devtools/client/responsive.html/components/device-selector.js
+++ b/devtools/client/responsive.html/components/device-selector.js
@@ -1,15 +1,15 @@
 /* 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 { getStr } = require("./utils/l10n");
+const { getStr } = require("../utils/l10n");
 const { DOM: dom, createClass, PropTypes, addons } =
   require("devtools/client/shared/vendor/react");
 
 const Types = require("../types");
 
 module.exports = createClass({
 
   displayName: "DeviceSelector",
--- a/devtools/client/responsive.html/components/global-toolbar.js
+++ b/devtools/client/responsive.html/components/global-toolbar.js
@@ -1,44 +1,56 @@
 /* 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 { getStr } = require("./utils/l10n");
+const { getStr } = require("../utils/l10n");
 const { DOM: dom, createClass, PropTypes, addons } =
   require("devtools/client/shared/vendor/react");
+const Types = require("../types");
 
 module.exports = createClass({
 
   displayName: "GlobalToolbar",
 
-  mixins: [ addons.PureRenderMixin ],
-
   propTypes: {
     onExit: PropTypes.func.isRequired,
+    onScreenshot: PropTypes.func.isRequired,
+    screenshot: PropTypes.shape(Types.screenshot).isRequired,
   },
 
+  mixins: [ addons.PureRenderMixin ],
+
   render() {
     let {
       onExit,
+      onScreenshot,
+      screenshot,
     } = this.props;
 
     return dom.header(
       {
         id: "global-toolbar",
         className: "toolbar",
       },
       dom.span(
         {
           className: "title",
         },
         getStr("responsive.title")),
       dom.button({
+        id: "global-screenshot-button",
+        className: "toolbar-button devtools-button",
+        title: getStr("responsive.screenshot"),
+        onClick: onScreenshot,
+        disabled: screenshot.isCapturing,
+      }),
+      dom.button({
         id: "global-exit-button",
         className: "toolbar-button devtools-button",
         title: getStr("responsive.exit"),
         onClick: onExit,
       })
     );
   },
 
--- a/devtools/client/responsive.html/components/moz.build
+++ b/devtools/client/responsive.html/components/moz.build
@@ -1,18 +1,14 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
-DIRS += [
-    'utils',
-]
-
 DevToolsModules(
     'browser.js',
     'device-selector.js',
     'global-toolbar.js',
     'resizable-viewport.js',
     'viewport-dimension.js',
     'viewport-toolbar.js',
     'viewport.js',
--- a/devtools/client/responsive.html/components/resizable-viewport.js
+++ b/devtools/client/responsive.html/components/resizable-viewport.js
@@ -19,16 +19,17 @@ const VIEWPORT_MIN_HEIGHT = Constants.MI
 
 module.exports = createClass({
 
   displayName: "ResizableViewport",
 
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
     location: Types.location.isRequired,
+    screenshot: PropTypes.shape(Types.screenshot).isRequired,
     viewport: PropTypes.shape(Types.viewport).isRequired,
     onChangeViewportDevice: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
     onRotateViewport: PropTypes.func.isRequired,
   },
 
   getInitialState() {
     return {
@@ -107,22 +108,29 @@ module.exports = createClass({
       lastClientY
     });
   },
 
   render() {
     let {
       devices,
       location,
+      screenshot,
       viewport,
       onChangeViewportDevice,
       onResizeViewport,
       onRotateViewport,
     } = this.props;
 
+    let resizeHandleClass = "viewport-resize-handle";
+
+    if (screenshot.isCapturing) {
+      resizeHandleClass += " hidden";
+    }
+
     return dom.div(
       {
         className: "resizable-viewport",
       },
       ViewportToolbar({
         devices,
         selectedDevice: viewport.device,
         onChangeViewportDevice,
@@ -131,17 +139,17 @@ module.exports = createClass({
       }),
       Browser({
         location,
         width: viewport.width,
         height: viewport.height,
         isResizing: this.state.isResizing
       }),
       dom.div({
-        className: "viewport-resize-handle",
+        className: resizeHandleClass,
         onMouseDown: this.onResizeStart,
       }),
       dom.div({
         ref: "resizeBarX",
         className: "viewport-horizontal-resize-handle",
         onMouseDown: this.onResizeStart,
       }),
       dom.div({
--- a/devtools/client/responsive.html/components/viewport.js
+++ b/devtools/client/responsive.html/components/viewport.js
@@ -13,16 +13,17 @@ const ViewportDimension = createFactory(
 
 module.exports = createClass({
 
   displayName: "Viewport",
 
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
     location: Types.location.isRequired,
+    screenshot: PropTypes.shape(Types.screenshot).isRequired,
     viewport: PropTypes.shape(Types.viewport).isRequired,
     onChangeViewportDevice: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
     onRotateViewport: PropTypes.func.isRequired,
   },
 
   onChangeViewportDevice(device) {
     let {
@@ -50,32 +51,34 @@ module.exports = createClass({
 
     onRotateViewport(viewport.id);
   },
 
   render() {
     let {
       devices,
       location,
+      screenshot,
       viewport,
     } = this.props;
 
     let {
       onChangeViewportDevice,
       onRotateViewport,
       onResizeViewport,
     } = this;
 
     return dom.div(
       {
         className: "viewport",
       },
       ResizableViewport({
         devices,
         location,
+        screenshot,
         viewport,
         onChangeViewportDevice,
         onResizeViewport,
         onRotateViewport,
       }),
       ViewportDimension({
         viewport,
         onChangeViewportDevice,
--- a/devtools/client/responsive.html/components/viewports.js
+++ b/devtools/client/responsive.html/components/viewports.js
@@ -12,41 +12,44 @@ const Viewport = createFactory(require("
 
 module.exports = createClass({
 
   displayName: "Viewports",
 
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
     location: Types.location.isRequired,
+    screenshot: PropTypes.shape(Types.screenshot).isRequired,
     viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
     onChangeViewportDevice: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
     onRotateViewport: PropTypes.func.isRequired,
   },
 
   render() {
     let {
       devices,
       location,
+      screenshot,
       viewports,
       onChangeViewportDevice,
       onResizeViewport,
       onRotateViewport,
     } = this.props;
 
     return dom.div(
       {
         id: "viewports",
       },
       viewports.map(viewport => {
         return Viewport({
           key: viewport.id,
           devices,
           location,
+          screenshot,
           viewport,
           onChangeViewportDevice,
           onResizeViewport,
           onRotateViewport,
         });
       })
     );
   },
--- a/devtools/client/responsive.html/images/moz.build
+++ b/devtools/client/responsive.html/images/moz.build
@@ -3,10 +3,11 @@
 # 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/.
 
 DevToolsModules(
     'close.svg',
     'grippers.svg',
     'rotate-viewport.svg',
+    'screenshot.svg',
     'select-arrow.svg',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/images/screenshot.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="#babec3">
+  <path d="M15.5 14H.5c-.3 0-.5-.2-.5-.5v-8c0-.3.2-.5.5-.5H4V2.5c0-.3.2-.5.5-.5h7c.3 0 .5.2.5.5V5h3.5c.3 0 .5.2.5.5v8c0 .3-.2.5-.5.5zM1 13h14V6h-3.5c-.3 0-.5-.2-.5-.5V3H5v2.5c0 .3-.2.5-.5.5H1v7z"/>
+  <path d="M8 12c-1.6 0-2.9-1.3-2.9-2.9S6.4 6.2 8 6.2c1.6 0 2.9 1.3 2.9 2.9S9.6 12 8 12zm0-4.8c-1.1 0-1.9.8-1.9 1.9 0 1.1.8 1.9 1.9 1.9 1.1 0 1.9-.9 1.9-1.9C9.9 8 9.1 7.2 8 7.2z"/>
+</svg>
--- a/devtools/client/responsive.html/index.css
+++ b/devtools/client/responsive.html/index.css
@@ -86,27 +86,38 @@ body {
   border-right: 1px solid var(--theme-splitter-color);
   padding: 1px 6px 0 2px;
 }
 
 #global-toolbar .toolbar-button {
   margin: 0 0 0 5px;
 }
 
-#global-exit-button,
-#global-exit-button::before {
+#global-toolbar .toolbar-button,
+#global-toolbar .toolbar-button::before {
   width: 12px;
   height: 12px;
 }
 
+#global-screenshot-button::before {
+  background-image: url("./images/screenshot.svg");
+  margin: -6px 0 0 -6px;
+}
+
 #global-exit-button::before {
   background-image: url("./images/close.svg");
   margin: -6px 0 0 -6px;
 }
 
+#global-screenshot-button:disabled {
+  filter: url("chrome://devtools/skin/images/filters.svg#checked-icon-state");
+  opacity: 1 !important;
+}
+
+
 #viewports {
   /* Snap to the top of the app when there isn't enough vertical space anymore
      to center the viewports (so we don't loose the toolbar) */
   position: sticky;
   top: 0;
   /* Make sure left-most viewport is visible when there's horizontal overflow.
      That is, when the horizontal space become smaller than the viewports and a
      scrollbar appears, then the first viewport will still be visible */
@@ -215,16 +226,20 @@ body {
   background-image: url("./images/grippers.svg");
   background-position: bottom right;
   padding: 0 1px 1px 0;
   background-repeat: no-repeat;
   background-origin: content-box;
   cursor: se-resize;
 }
 
+.viewport-resize-handle.hidden {
+  display: none;
+}
+
 .viewport-horizontal-resize-handle {
   position: absolute;
   width: 5px;
   height: calc(100% - 16px);
   right: -4px;
   top: 0;
   cursor: e-resize;
 }
--- a/devtools/client/responsive.html/index.js
+++ b/devtools/client/responsive.html/index.js
@@ -37,20 +37,18 @@ let bootstrap = {
   init() {
     // Load a special UA stylesheet to reset certain styles such as dropdown
     // lists.
     loadSheet(window,
               "resource://devtools/client/responsive.html/responsive-ua.css",
               "agent");
     this.telemetry.toolOpened("responsive");
     let store = this.store = Store();
-    let app = App({
-      onExit: () => window.postMessage({ type: "exit" }, "*"),
-    });
-    let provider = createElement(Provider, { store }, app);
+    let provider = createElement(Provider, { store }, App());
+
     ReactDOM.render(provider, document.querySelector("#root"));
     this.initDevices();
     window.postMessage({ type: "init" }, "*");
   },
 
   destroy() {
     this.store = null;
     this.telemetry.toolClosed("responsive");
--- a/devtools/client/responsive.html/moz.build
+++ b/devtools/client/responsive.html/moz.build
@@ -1,19 +1,21 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 DIRS += [
     'actions',
+    'audio',
     'components',
     'images',
     'reducers',
+    'utils',
 ]
 
 DevToolsModules(
     'app.js',
     'constants.js',
     'index.css',
     'manager.js',
     'reducers.js',
--- a/devtools/client/responsive.html/reducers.js
+++ b/devtools/client/responsive.html/reducers.js
@@ -1,9 +1,10 @@
 /* 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";
 
 exports.devices = require("./reducers/devices");
 exports.location = require("./reducers/location");
+exports.screenshot = require("./reducers/screenshot");
 exports.viewports = require("./reducers/viewports");
--- a/devtools/client/responsive.html/reducers/moz.build
+++ b/devtools/client/responsive.html/reducers/moz.build
@@ -2,10 +2,11 @@
 # vim: set filetype=python:
 # 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/.
 
 DevToolsModules(
     'devices.js',
     'location.js',
+    'screenshot.js',
     'viewports.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/reducers/screenshot.js
@@ -0,0 +1,31 @@
+/* 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 {
+  TAKE_SCREENSHOT_END,
+  TAKE_SCREENSHOT_START,
+} = require("../actions/index");
+
+const INITIAL_SCREENSHOT = { isCapturing: false };
+
+let reducers = {
+
+  [TAKE_SCREENSHOT_END](screenshot, action) {
+    return Object.assign({}, screenshot, { isCapturing: false });
+  },
+
+  [TAKE_SCREENSHOT_START](screenshot, action) {
+    return Object.assign({}, screenshot, { isCapturing: true });
+  },
+};
+
+module.exports = function(screenshot = INITIAL_SCREENSHOT, action) {
+  let reducer = reducers[action.type];
+  if (!reducer) {
+    return screenshot;
+  }
+  return reducer(screenshot, action);
+};
--- a/devtools/client/responsive.html/test/browser/browser.ini
+++ b/devtools/client/responsive.html/test/browser/browser.ini
@@ -3,9 +3,10 @@ tags = devtools
 subsuite = devtools
 support-files =
   devices.json
   head.js
   !/devtools/client/framework/test/shared-head.js
   !/devtools/client/framework/test/shared-redux-head.js
 
 [browser_exit_button.js]
+[browser_screenshot_button.js]
 [browser_viewport_basics.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_screenshot_button.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test global exit button
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+
+const { OS } = require("resource://gre/modules/osfile.jsm");
+
+function* waitUntilScreenshot() {
+  return new Promise(Task.async(function* (resolve) {
+    let { Downloads } = require("resource://gre/modules/Downloads.jsm");
+    let list = yield Downloads.getList(Downloads.ALL);
+
+    let view = {
+      onDownloadAdded: download => {
+        download.whenSucceeded().then(() => {
+          resolve(download.target.path);
+          list.removeView(view);
+        });
+      }
+    };
+
+    yield list.addView(view);
+  }));
+}
+
+addRDMTask(TEST_URL, function* ({ ui: {toolWindow} }) {
+  let { store, document } = toolWindow;
+
+  // Wait until the viewport has been added
+  yield waitUntilState(store, state => state.viewports.length == 1);
+
+  info("Click the screenshot button");
+  let screenshotButton = document.getElementById("global-screenshot-button");
+  screenshotButton.click();
+
+  let whenScreenshotSucceeded = waitUntilScreenshot();
+
+  let filePath = yield whenScreenshotSucceeded;
+  let image = new Image();
+  image.src = OS.Path.toFileURI(filePath);
+
+  yield once(image, "load");
+
+  // We have only one viewport at the moment
+  let viewport = store.getState().viewports[0];
+  let ratio = window.devicePixelRatio;
+
+  is(image.width, viewport.width * ratio,
+    "screenshot width has the expected width");
+
+  is(image.height, viewport.height * ratio,
+    "screenshot width has the expected height");
+
+  yield OS.File.remove(filePath);
+});
--- a/devtools/client/responsive.html/types.js
+++ b/devtools/client/responsive.html/types.js
@@ -66,16 +66,25 @@ exports.devices = {
 };
 
 /**
  * The location of the document displayed in the viewport(s).
  */
 exports.location = PropTypes.string;
 
 /**
+ * The progression of the screenshot
+ */
+exports.screenshot = {
+
+  isCapturing: PropTypes.bool.isRequired,
+
+};
+
+/**
  * A single viewport displaying a document.
  */
 exports.viewport = {
 
   // The id of the viewport
   id: PropTypes.number.isRequired,
 
   // The currently selected device applied to the viewport.
rename from devtools/client/responsive.html/components/utils/l10n.js
rename to devtools/client/responsive.html/utils/l10n.js
rename from devtools/client/responsive.html/components/utils/moz.build
rename to devtools/client/responsive.html/utils/moz.build