Bug 1209699 - Add a 'Push' button for service workers in about:debugging. r=jdescottes
authorJan Keromnes <janx@linux.com>
Mon, 29 Feb 2016 03:20:00 +0100
changeset 286219 7f9ea43e7bf11c2b062f5e401fa84016c5e16e47
parent 286218 5e6d66db9b05790dea3c4654ec7309bfac424b73
child 286220 93e15509f449e9edd5d4b7fef410b796e438abd7
push id30042
push userkwierso@gmail.com
push dateTue, 01 Mar 2016 22:20:44 +0000
treeherdermozilla-central@d6788a70c97b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdescottes
bugs1209699
milestone47.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 1209699 - Add a 'Push' button for service workers in about:debugging. r=jdescottes
devtools/client/aboutdebugging/aboutdebugging.css
devtools/client/aboutdebugging/components/target.js
devtools/client/aboutdebugging/test/browser.ini
devtools/client/aboutdebugging/test/browser_service_workers_push.js
devtools/client/aboutdebugging/test/browser_service_workers_timeout.js
devtools/client/aboutdebugging/test/head.js
devtools/client/aboutdebugging/test/service-workers/push-sw.html
devtools/client/aboutdebugging/test/service-workers/push-sw.js
devtools/client/locales/en-US/aboutdebugging.properties
devtools/server/actors/worker.js
devtools/shared/Loader.jsm
--- a/devtools/client/aboutdebugging/aboutdebugging.css
+++ b/devtools/client/aboutdebugging/aboutdebugging.css
@@ -46,16 +46,17 @@ button {
 /* Targets */
 
 .targets {
   margin-bottom: 25px;
 }
 
 .target {
   margin-top: 5px;
+  min-height: 34px;
   display: flex;
   flex-direction: row;
   align-items: center;
 }
 
 .target-icon {
   height: 24px;
   margin-right: 5px;
--- a/devtools/client/aboutdebugging/components/target.js
+++ b/devtools/client/aboutdebugging/components/target.js
@@ -33,16 +33,23 @@ module.exports = createClass({
     return dom.div({ className: "target" },
       dom.img({
         className: "target-icon",
         role: "presentation",
         src: target.icon }),
       dom.div({ className: "target-details" },
         dom.div({ className: "target-name" }, target.name)
       ),
+      (isRunning && isServiceWorker ?
+        dom.button({
+          className: "push-button",
+          onClick: this.push
+        }, Strings.GetStringFromName("push")) :
+        null
+      ),
       (isRunning ?
         dom.button({
           className: "debug-button",
           onClick: this.debug,
           disabled: debugDisabled,
         }, Strings.GetStringFromName("debug")) :
         null
       )
@@ -67,16 +74,26 @@ module.exports = createClass({
         this.openWorkerToolbox(target.workerActor);
         break;
       default:
         window.alert("Not implemented yet!");
         break;
     }
   },
 
+  push() {
+    let { client, target } = this.props;
+    if (target.workerActor) {
+      client.request({
+        to: target.workerActor,
+        type: "push"
+      });
+    }
+  },
+
   openWorkerToolbox(workerActor) {
     let { client } = this.props;
     client.attachWorker(workerActor, (response, workerClient) => {
       gDevTools.showToolbox(TargetFactory.forWorker(workerClient),
         "jsdebugger", Toolbox.HostType.WINDOW)
         .then(toolbox => {
           toolbox.once("destroy", () => workerClient.detach());
         });
--- a/devtools/client/aboutdebugging/test/browser.ini
+++ b/devtools/client/aboutdebugging/test/browser.ini
@@ -2,14 +2,17 @@
 tags = devtools
 subsuite = devtools
 support-files =
   head.js
   addons/unpacked/bootstrap.js
   addons/unpacked/install.rdf
   service-workers/empty-sw.html
   service-workers/empty-sw.js
+  service-workers/push-sw.html
+  service-workers/push-sw.js
 
 [browser_addons_debugging_initial_state.js]
 [browser_addons_install.js]
 [browser_addons_toggle_debug.js]
 [browser_service_workers.js]
+[browser_service_workers_push.js]
 [browser_service_workers_timeout.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_push.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-disable mozilla/no-cpows-in-tests */
+/* global sendAsyncMessage */
+
+"use strict";
+
+// Test that clicking on the Push button next to a Service Worker works as
+// intended in about:debugging.
+// It should trigger a "push" notification in the worker.
+
+// Service workers can't be loaded from chrome://, but http:// is ok with
+// dom.serviceWorkers.testing.enabled turned on.
+const HTTP_ROOT = CHROME_ROOT.replace(
+  "chrome://mochitests/content/", "http://mochi.test:8888/");
+const SERVICE_WORKER = HTTP_ROOT + "service-workers/push-sw.js";
+const TAB_URL = HTTP_ROOT + "service-workers/push-sw.html";
+
+add_task(function* () {
+  info("Turn on workers via mochitest http.");
+  yield new Promise(done => {
+    let options = { "set": [
+      // Accept workers from mochitest's http.
+      ["dom.serviceWorkers.testing.enabled", true],
+    ]};
+    SpecialPowers.pushPrefEnv(options, done);
+  });
+
+  let { tab, document } = yield openAboutDebugging("workers");
+
+  // Listen for mutations in the service-workers list.
+  let serviceWorkersElement = document.getElementById("service-workers");
+  let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
+
+  // Open a tab that registers a push service worker.
+  let swTab = yield addTab(TAB_URL);
+
+  info("Make the test page notify us when the service worker sends a message.");
+  let frameScript = function() {
+    let win = content.wrappedJSObject;
+    win.navigator.serviceWorker.addEventListener("message", function(event) {
+      sendAsyncMessage(event.data);
+    }, false);
+  };
+  let mm = swTab.linkedBrowser.messageManager;
+  mm.loadFrameScript("data:,(" + encodeURIComponent(frameScript) + ")()", true);
+
+  // Expect the service worker to claim the test window when activating.
+  let onClaimed = new Promise(done => {
+    mm.addMessageListener("sw-claimed", function listener() {
+      mm.removeMessageListener("sw-claimed", listener);
+      done();
+    });
+  });
+
+  // Wait for the service-workers list to update.
+  yield onMutation;
+
+  // Check that the service worker appears in the UI.
+  assertHasTarget(true, document, "service-workers", SERVICE_WORKER);
+
+  info("Ensure that the registration resolved before trying to interact with " +
+    "the service worker.");
+  yield waitForServiceWorkerRegistered(swTab);
+  ok(true, "Service worker registration resolved");
+
+  // Retrieve the Push button for the worker.
+  let names = [...document.querySelectorAll("#service-workers .target-name")];
+  let name = names.filter(element => element.textContent === SERVICE_WORKER)[0];
+  ok(name, "Found the service worker in the list");
+  let targetElement = name.parentNode.parentNode;
+  let pushBtn = targetElement.querySelector(".push-button");
+  ok(pushBtn, "Found its push button");
+
+  info("Wait for the service worker to claim the test window before " +
+    "proceeding.");
+  yield onClaimed;
+
+  info("Click on the Push button and wait for the service worker to receive " +
+    "a push notification");
+  let onPushNotification = new Promise(done => {
+    mm.addMessageListener("sw-pushed", function listener() {
+      mm.removeMessageListener("sw-pushed", listener);
+      done();
+    });
+  });
+  pushBtn.click();
+  yield onPushNotification;
+  ok(true, "Service worker received a push notification");
+
+  // Finally, unregister the service worker itself.
+  yield unregisterServiceWorker(swTab);
+  ok(true, "Service worker registration unregistered");
+
+  yield removeTab(swTab);
+  yield closeAboutDebugging(tab);
+});
--- a/devtools/client/aboutdebugging/test/browser_service_workers_timeout.js
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_timeout.js
@@ -1,32 +1,22 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-/* eslint-disable mozilla/no-cpows-in-tests */
-/* global sendAsyncMessage */
-
 "use strict";
 
 // Service workers can't be loaded from chrome://,
 // but http:// is ok with dom.serviceWorkers.testing.enabled turned on.
 const HTTP_ROOT = CHROME_ROOT.replace("chrome://mochitests/content/",
                                       "http://mochi.test:8888/");
 const SERVICE_WORKER = HTTP_ROOT + "service-workers/empty-sw.js";
 const TAB_URL = HTTP_ROOT + "service-workers/empty-sw.html";
 
 const SW_TIMEOUT = 1000;
 
-function assertHasWorker(expected, document, type, name) {
-  let names = [...document.querySelectorAll("#" + type + " .target-name")];
-  names = names.map(element => element.textContent);
-  is(names.includes(name), expected,
-      "The " + type + " url appears in the list: " + names);
-}
-
 add_task(function* () {
   yield new Promise(done => {
     let options = {"set": [
       // Accept workers from mochitest's http
       ["dom.serviceWorkers.testing.enabled", true],
       // Reduce the timeout to expose issues when service worker
       // freezing is broken
       ["dom.serviceWorkers.idle_timeout", SW_TIMEOUT],
@@ -37,35 +27,20 @@ add_task(function* () {
 
   let { tab, document } = yield openAboutDebugging("workers");
 
   let swTab = yield addTab(TAB_URL);
 
   let serviceWorkersElement = document.getElementById("service-workers");
   yield waitForMutation(serviceWorkersElement, { childList: true });
 
-  assertHasWorker(true, document, "service-workers", SERVICE_WORKER);
+  assertHasTarget(true, document, "service-workers", SERVICE_WORKER);
 
   // Ensure that the registration resolved before trying to connect to the sw
-  let frameScript = function() {
-    // Retrieve the `sw` promise created in the html page
-    let { sw } = content.wrappedJSObject;
-    sw.then(function() {
-      sendAsyncMessage("sw-registered");
-    });
-  };
-  let mm = swTab.linkedBrowser.messageManager;
-  mm.loadFrameScript("data:,(" + encodeURIComponent(frameScript) + ")()", true);
-
-  yield new Promise(done => {
-    mm.addMessageListener("sw-registered", function listener() {
-      mm.removeMessageListener("sw-registered", listener);
-      done();
-    });
-  });
+  yield waitForServiceWorkerRegistered(swTab);
   ok(true, "Service worker registration resolved");
 
   // Retrieve the DEBUG button for the worker
   let names = [...document.querySelectorAll("#service-workers .target-name")];
   let name = names.filter(element => element.textContent === SERVICE_WORKER)[0];
   ok(name, "Found the service worker in the list");
   let targetElement = name.parentNode.parentNode;
   let debugBtn = targetElement.querySelector(".debug-button");
@@ -83,55 +58,34 @@ add_task(function* () {
 
   // Wait for more than the regular timeout,
   // so that if the worker freezing doesn't work,
   // it will be destroyed and removed from the list
   yield new Promise(done => {
     setTimeout(done, SW_TIMEOUT * 2);
   });
 
-  assertHasWorker(true, document, "service-workers", SERVICE_WORKER);
+  assertHasTarget(true, document, "service-workers", SERVICE_WORKER);
   ok(targetElement.querySelector(".debug-button"),
     "The debug button is still there");
 
   yield toolbox.destroy();
   toolbox = null;
 
   // Now ensure that the worker is correctly destroyed
   // after we destroy the toolbox.
   // The DEBUG button should disappear once the worker is destroyed.
   yield waitForMutation(targetElement, { childList: true });
   ok(!targetElement.querySelector(".debug-button"),
     "The debug button was removed when the worker was killed");
 
-  // Finally, unregister the service worker itself
-  // Use message manager to work with e10s
-  frameScript = function() {
-    // Retrieve the `sw` promise created in the html page
-    let { sw } = content.wrappedJSObject;
-    sw.then(function(registration) {
-      registration.unregister().then(function() {
-        sendAsyncMessage("sw-unregistered");
-      },
-      function(e) {
-        dump("SW not unregistered; " + e + "\n");
-      });
-    });
-  };
-  mm = swTab.linkedBrowser.messageManager;
-  mm.loadFrameScript("data:,(" + encodeURIComponent(frameScript) + ")()", true);
-
-  yield new Promise(done => {
-    mm.addMessageListener("sw-unregistered", function listener() {
-      mm.removeMessageListener("sw-unregistered", listener);
-      done();
-    });
-  });
+  // Finally, unregister the service worker itself.
+  yield unregisterServiceWorker(swTab);
   ok(true, "Service worker registration unregistered");
 
   // Now ensure that the worker registration is correctly removed.
   // The list should update once the registration is destroyed.
   yield waitForMutation(serviceWorkersElement, { childList: true });
-  assertHasWorker(false, document, "service-workers", SERVICE_WORKER);
+  assertHasTarget(false, document, "service-workers", SERVICE_WORKER);
 
   yield removeTab(swTab);
   yield closeAboutDebugging(tab);
 });
--- a/devtools/client/aboutdebugging/test/head.js
+++ b/devtools/client/aboutdebugging/test/head.js
@@ -1,21 +1,24 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /* eslint-env browser */
+/* eslint-disable mozilla/no-cpows-in-tests */
 /* exported openAboutDebugging, closeAboutDebugging, installAddon,
-   uninstallAddon, waitForMutation */
+   uninstallAddon, waitForMutation, assertHasTarget,
+   waitForServiceWorkerRegistered, unregisterServiceWorker */
+/* global sendAsyncMessage */
 
 "use strict";
 
-var {utils: Cu, classes: Cc, interfaces: Ci} = Components;
+var { utils: Cu, classes: Cc, interfaces: Ci } = Components;
 
-const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
-const {AddonManager} = Cu.import("resource://gre/modules/AddonManager.jsm", {});
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
 const Services = require("Services");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 DevToolsUtils.testing = true;
 
 const CHROME_ROOT = gTestPath.substr(0, gTestPath.lastIndexOf("/") + 1);
 
 registerCleanupFunction(() => {
   DevToolsUtils.testing = false;
@@ -141,8 +144,78 @@ function waitForMutation(target, mutatio
   return new Promise(resolve => {
     let observer = new MutationObserver(() => {
       observer.disconnect();
       resolve();
     });
     observer.observe(target, mutationOptions);
   });
 }
+
+/**
+ * Checks if an about:debugging TargetList element contains a Target element
+ * corresponding to the specified name.
+ * @param {Boolean} expected
+ * @param {Document} document
+ * @param {String} type
+ * @param {String} name
+ */
+function assertHasTarget(expected, document, type, name) {
+  let names = [...document.querySelectorAll("#" + type + " .target-name")];
+  names = names.map(element => element.textContent);
+  is(names.includes(name), expected,
+    "The " + type + " url appears in the list: " + names);
+}
+
+/**
+ * Returns a promise that will resolve after the service worker in the page
+ * has successfully registered itself.
+ * @param {Tab} tab
+ */
+function waitForServiceWorkerRegistered(tab) {
+  // Make the test page notify us when the service worker is registered.
+  let frameScript = function() {
+    // Retrieve the `sw` promise created in the html page.
+    let { sw } = content.wrappedJSObject;
+    sw.then(function(registration) {
+      sendAsyncMessage("sw-registered");
+    });
+  };
+  let mm = tab.linkedBrowser.messageManager;
+  mm.loadFrameScript("data:,(" + encodeURIComponent(frameScript) + ")()", true);
+
+  return new Promise(done => {
+    mm.addMessageListener("sw-registered", function listener() {
+      mm.removeMessageListener("sw-registered", listener);
+      done();
+    });
+  });
+}
+
+/**
+ * Asks the service worker within the test page to unregister, and returns a
+ * promise that will resolve when it has successfully unregistered itself.
+ * @param {Tab} tab
+ */
+function unregisterServiceWorker(tab) {
+  // Use message manager to work with e10s.
+  let frameScript = function() {
+    // Retrieve the `sw` promise created in the html page.
+    let { sw } = content.wrappedJSObject;
+    sw.then(function(registration) {
+      registration.unregister().then(function() {
+        sendAsyncMessage("sw-unregistered");
+      },
+      function(e) {
+        dump("SW not unregistered; " + e + "\n");
+      });
+    });
+  };
+  let mm = tab.linkedBrowser.messageManager;
+  mm.loadFrameScript("data:,(" + encodeURIComponent(frameScript) + ")()", true);
+
+  return new Promise(done => {
+    mm.addMessageListener("sw-unregistered", function listener() {
+      mm.removeMessageListener("sw-unregistered", listener);
+      done();
+    });
+  });
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/service-workers/push-sw.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <title>Service worker push test</title>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+var sw = navigator.serviceWorker.register("push-sw.js");
+sw.then(
+  function(registration) {
+    dump("SW registered\n");
+  },
+  function(error) {
+    dump("SW not registered: " + error + "\n");
+  }
+);
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/service-workers/push-sw.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env worker */
+/* global clients */
+
+"use strict";
+
+// Send a message to all controlled windows.
+function postMessage(message) {
+  return clients.matchAll().then(function(clientlist) {
+    clientlist.forEach(function(client) {
+      client.postMessage(message);
+    });
+  });
+}
+
+// Don't wait for the next page load to become the active service worker.
+self.addEventListener("install", function(event) {
+  event.waitUntil(self.skipWaiting());
+});
+
+// Claim control over the currently open test page when activating.
+self.addEventListener("activate", function(event) {
+  event.waitUntil(self.clients.claim().then(function() {
+    return postMessage("sw-claimed");
+  }));
+});
+
+// Forward all "push" events to the controlled window.
+self.addEventListener("push", function(event) {
+  event.waitUntil(postMessage("sw-pushed"));
+});
--- a/devtools/client/locales/en-US/aboutdebugging.properties
+++ b/devtools/client/locales/en-US/aboutdebugging.properties
@@ -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/.
 
 debug = Debug
+push = Push
 
 addons = Add-ons
 addonDebugging.label = Enable add-on debugging
 addonDebugging.tooltip = Turning this on will allow you to debug add-ons and various other parts of the browser chrome
 addonDebugging.moreInfo = more info
 loadTemporaryAddon = Load Temporary Add-on
 extensions = Extensions
 selectAddonFromFile = Select Add-on Directory or XPI File
--- a/devtools/server/actors/worker.js
+++ b/devtools/server/actors/worker.js
@@ -1,15 +1,17 @@
 "use strict";
 
 var { Ci, Cu } = require("chrome");
 var { DebuggerServer } = require("devtools/server/main");
 const protocol = require("devtools/server/protocol");
 const { Arg, method, RetVal } = protocol;
 
+loader.lazyRequireGetter(this, "ChromeUtils");
+
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(
   this, "wdm",
   "@mozilla.org/dom/workers/workerdebuggermanager;1",
   "nsIWorkerDebuggerManager"
 );
 
@@ -135,16 +137,30 @@ let WorkerActor = protocol.ActorClass({
     });
   }, {
     request: {
       options: Arg(0, "json"),
     },
     response: RetVal("json")
   }),
 
+  push: method(function () {
+    if (this._dbg.type !== Ci.nsIWorkerDebugger.TYPE_SERVICE) {
+      return { error: "wrongType" };
+    }
+    let registration = this._getServiceWorkerRegistrationInfo();
+    let originAttributes = ChromeUtils.originAttributesToSuffix(
+      this._dbg.principal.originAttributes);
+    swm.sendPushEvent(originAttributes, registration.scope);
+    return { type: "pushed" };
+  }, {
+    request: {},
+    response: RetVal("json")
+  }),
+
   onClose: function () {
     if (this._attached) {
       this._detach();
     }
 
     this.conn.sendActorEvent(this.actorID, "close");
   },
 
--- a/devtools/shared/Loader.jsm
+++ b/devtools/shared/Loader.jsm
@@ -27,16 +27,17 @@ this.EXPORTED_SYMBOLS = ["DevToolsLoader
 /**
  * Providers are different strategies for loading the devtools.
  */
 
 var loaderModules = {
   "Services": Object.create(Services),
   "toolkit/loader": Loader,
   PromiseDebugging,
+  ChromeUtils,
   ThreadSafeChromeUtils,
   HeapSnapshot,
 };
 XPCOMUtils.defineLazyGetter(loaderModules, "Debugger", () => {
   // addDebuggerToGlobal only allows adding the Debugger object to a global. The
   // this object is not guaranteed to be a global (in particular on B2G, due to
   // compartment sharing), so add the Debugger object to a sandbox instead.
   let sandbox = Cu.Sandbox(CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')());