Bug 1153292 - part4: aboutdebugging: display registering status for service workers;r=janx draft
authorJulian Descottes <jdescottes@mozilla.com>
Thu, 15 Sep 2016 15:12:06 +0200
changeset 420135 83bcd044a79b616b202f87cc9a93182fc12b4c24
parent 420134 f7e023708f8954b978b189025fd0b06c587d6a8e
child 420136 d27c8063dd690aded44ef3e9bf7ed4ddba838a89
push id31104
push userjdescottes@mozilla.com
push dateMon, 03 Oct 2016 13:33:13 +0000
reviewersjanx
bugs1153292
milestone52.0a1
Bug 1153292 - part4: aboutdebugging: display registering status for service workers;r=janx MozReview-Commit-ID: DuE46jPSDvR
devtools/client/aboutdebugging/components/workers/panel.js
devtools/client/aboutdebugging/components/workers/service-worker-target.js
devtools/client/aboutdebugging/test/browser.ini
devtools/client/aboutdebugging/test/browser_service_workers.js
devtools/client/aboutdebugging/test/browser_service_workers_push.js
devtools/client/aboutdebugging/test/browser_service_workers_push_service.js
devtools/client/aboutdebugging/test/browser_service_workers_start.js
devtools/client/aboutdebugging/test/browser_service_workers_status.js
devtools/client/aboutdebugging/test/browser_service_workers_timeout.js
devtools/client/aboutdebugging/test/browser_service_workers_unregister.js
devtools/client/aboutdebugging/test/head.js
devtools/client/aboutdebugging/test/service-workers/delay-sw.html
devtools/client/aboutdebugging/test/service-workers/delay-sw.js
devtools/client/locales/en-US/aboutdebugging.properties
devtools/server/actors/worker.js
devtools/shared/specs/worker.js
--- a/devtools/client/aboutdebugging/components/workers/panel.js
+++ b/devtools/client/aboutdebugging/components/workers/panel.js
@@ -37,59 +37,84 @@ module.exports = createClass({
     };
   },
 
   componentDidMount() {
     let client = this.props.client;
     client.addListener("workerListChanged", this.update);
     client.addListener("serviceWorkerRegistrationListChanged", this.update);
     client.addListener("processListChanged", this.update);
+    client.addListener("registration-changed", this.update);
+
     this.update();
   },
 
   componentWillUnmount() {
     let client = this.props.client;
     client.removeListener("processListChanged", this.update);
     client.removeListener("serviceWorkerRegistrationListChanged", this.update);
     client.removeListener("workerListChanged", this.update);
+    client.removeListener("registration-changed", this.update);
   },
 
   update() {
     let workers = this.getInitialState().workers;
 
     getWorkerForms(this.props.client).then(forms => {
       forms.registrations.forEach(form => {
+        // - In e10s: only active registrations are available, but if the worker is in
+        // activating state it won't be available as the activeWorker. Registrations with
+        // no worker are actually registrations with a hidden activating worker.
+        // - In non-e10s: registrations always have at least one worker, if it is an
+        // active worker, the registration is active.
+        let hasWorker = form.activeWorker || form.waitingWorker || form.installingWorker;
+        let isE10s = Services.appinfo.browserTabsRemoteAutostart;
+        let active = form.activeWorker || (isE10s && !hasWorker);
+
         workers.service.push({
           icon: WorkerIcon,
           name: form.url,
           url: form.url,
           scope: form.scope,
-          registrationActor: form.actor
+          registrationActor: form.actor,
+          active
         });
       });
 
       forms.workers.forEach(form => {
         let worker = {
           icon: WorkerIcon,
           name: form.url,
           url: form.url,
           workerActor: form.actor
         };
         switch (form.type) {
           case Ci.nsIWorkerDebugger.TYPE_SERVICE:
-            for (let registration of workers.service) {
-              if (registration.scope === form.scope) {
-                // XXX: Race, sometimes a ServiceWorkerRegistrationInfo doesn't
-                // have a scriptSpec, but its associated WorkerDebugger does.
-                if (!registration.url) {
-                  registration.name = registration.url = form.url;
-                }
-                registration.workerActor = form.actor;
-                break;
+            let registration = this.getRegistrationForWorker(form, workers.service);
+            if (registration) {
+              // XXX: Race, sometimes a ServiceWorkerRegistrationInfo doesn't
+              // have a scriptSpec, but its associated WorkerDebugger does.
+              if (!registration.url) {
+                registration.name = registration.url = form.url;
               }
+              registration.workerActor = form.actor;
+            } else {
+              // If a service worker registration could not be found, this means we are in
+              // e10s, and registrations are not forwarded to other processes until they
+              // reach the activated state. Add a temporary registration to display it in
+              // aboutdebugging.
+              workers.service.push({
+                icon: WorkerIcon,
+                name: form.url,
+                url: form.url,
+                scope: form.scope,
+                registrationActor: null,
+                workerActor: form.actor,
+                active: false
+              });
             }
             break;
           case Ci.nsIWorkerDebugger.TYPE_SHARED:
             workers.shared.push(worker);
             break;
           default:
             workers.other.push(worker);
         }
@@ -98,16 +123,25 @@ module.exports = createClass({
       // XXX: Filter out the service worker registrations for which we couldn't
       // find the scriptSpec.
       workers.service = workers.service.filter(reg => !!reg.url);
 
       this.setState({ workers });
     });
   },
 
+  getRegistrationForWorker(form, registrations) {
+    for (let registration of registrations) {
+      if (registration.scope === form.scope) {
+        return registration;
+      }
+    }
+    return null;
+  },
+
   render() {
     let { client, id } = this.props;
     let { workers } = this.state;
 
     let isWindowPrivate = PrivateBrowsingUtils.isContentWindowPrivate(window);
     let isPrivateBrowsingMode = PrivateBrowsingUtils.permanentPrivateBrowsing;
     let isServiceWorkerDisabled = !Services.prefs
                                     .getBoolPref("dom.serviceWorkers.enabled");
--- a/devtools/client/aboutdebugging/components/workers/service-worker-target.js
+++ b/devtools/client/aboutdebugging/components/workers/service-worker-target.js
@@ -20,53 +20,63 @@ module.exports = createClass({
   getInitialState() {
     return {
       pushSubscription: null
     };
   },
 
   componentDidMount() {
     let { client } = this.props;
-    client.addListener("push-subscription-modified",
-      this.onPushSubscriptionModified);
+    client.addListener("push-subscription-modified", this.onPushSubscriptionModified);
     this.updatePushSubscription();
   },
 
+  componentDidUpdate(oldProps, oldState) {
+    let wasActive = oldProps.target.active;
+    if (!wasActive && this.isActive()) {
+      // While the service worker isn't active, any calls to `updatePushSubscription`
+      // won't succeed. If we just became active, make sure we didn't miss a push
+      // subscription change by updating it now.
+      this.updatePushSubscription();
+    }
+  },
+
   componentWillUnmount() {
     let { client } = this.props;
-    client.removeListener("push-subscription-modified",
-      this.onPushSubscriptionModified);
+    client.removeListener("push-subscription-modified", this.onPushSubscriptionModified);
   },
 
   debug() {
     if (!this.isRunning()) {
       // If the worker is not running, we can't debug it.
       return;
     }
 
     let { client, target } = this.props;
     debugWorker(client, target.workerActor);
   },
 
   push() {
-    if (!this.isRunning()) {
+    if (!this.isActive() || !this.isRunning()) {
       // If the worker is not running, we can't push to it.
+      // If the worker is not active, the registration might be unavailable and the
+      // push will not succeed.
       return;
     }
 
     let { client, target } = this.props;
     client.request({
       to: target.workerActor,
       type: "push"
     });
   },
 
   start() {
-    if (this.isRunning()) {
-      // If the worker is already running, we can't start it.
+    if (!this.isActive() || this.isRunning()) {
+      // If the worker is not active or if it is already running, we can't start it.
       return;
     }
 
     let { client, target } = this.props;
     client.request({
       to: target.registrationActor,
       type: "start"
     });
@@ -83,38 +93,93 @@ module.exports = createClass({
   onPushSubscriptionModified(type, data) {
     let { target } = this.props;
     if (data.from === target.registrationActor) {
       this.updatePushSubscription();
     }
   },
 
   updatePushSubscription() {
+    if (!this.props.target.registrationActor) {
+      // A valid registrationActor is needed to retrieve the push subscription.
+      return;
+    }
+
     let { client, target } = this.props;
     client.request({
       to: target.registrationActor,
       type: "getPushSubscription"
     }, ({ subscription }) => {
       this.setState({ pushSubscription: subscription });
     });
   },
 
   isRunning() {
     // We know the target is running if it has a worker actor.
     return !!this.props.target.workerActor;
   },
 
+  isActive() {
+    return this.props.target.active;
+  },
+
   getServiceWorkerStatus() {
-    return this.isRunning() ? "running" : "stopped";
+    if (this.isActive() && this.isRunning()) {
+      return "running";
+    } else if (this.isActive()) {
+      return "stopped";
+    }
+    // We cannot get service worker registrations unless the registration is in
+    // ACTIVE state. Unable to know the actual state ("installing", "waiting"), we
+    // display a custom state "registering" for now. See Bug 1153292.
+    return "registering";
+  },
+
+  renderButtons() {
+    let pushButton = dom.button({
+      className: "push-button",
+      onClick: this.push
+    }, Strings.GetStringFromName("push"));
+
+    let debugButton = dom.button({
+      className: "debug-button",
+      onClick: this.debug,
+      disabled: this.props.debugDisabled
+    }, Strings.GetStringFromName("debug"));
+
+    let startButton = dom.button({
+      className: "start-button",
+      onClick: this.start,
+    }, Strings.GetStringFromName("start"));
+
+    if (this.isRunning()) {
+      if (this.isActive()) {
+        return [pushButton, debugButton];
+      }
+      // Only debug button is available if the service worker is not active.
+      return debugButton;
+    }
+    return startButton;
+  },
+
+  renderUnregisterLink() {
+    if (!this.isActive()) {
+      // If not active, there might be no registrationActor available.
+      return null;
+    }
+
+    return dom.a({
+      onClick: this.unregister,
+      className: "unregister-link"
+    }, Strings.GetStringFromName("unregister"));
   },
 
   render() {
-    let { target, debugDisabled } = this.props;
+    let { target } = this.props;
     let { pushSubscription } = this.state;
-    let isRunning = this.isRunning();
     let status = this.getServiceWorkerStatus();
 
     return dom.div({ className: "target-container" },
       dom.img({
         className: "target-icon",
         role: "presentation",
         src: target.icon
       }),
@@ -133,35 +198,16 @@ module.exports = createClass({
             null
           ),
           dom.li({ className: "target-detail" },
             dom.strong(null, Strings.GetStringFromName("scope")),
             dom.span({
               className: "service-worker-scope",
               title: target.scope
             }, target.scope),
-            dom.a({
-              onClick: this.unregister,
-              className: "unregister-link"
-            }, Strings.GetStringFromName("unregister"))
+            this.renderUnregisterLink()
           )
         )
       ),
-      (isRunning ?
-        [
-          dom.button({
-            className: "push-button",
-            onClick: this.push
-          }, Strings.GetStringFromName("push")),
-          dom.button({
-            className: "debug-button",
-            onClick: this.debug,
-            disabled: debugDisabled
-          }, Strings.GetStringFromName("debug"))
-        ] :
-        dom.button({
-          className: "start-button",
-          onClick: this.start
-        }, Strings.GetStringFromName("start"))
-      )
+      this.renderButtons()
     );
   }
 });
--- a/devtools/client/aboutdebugging/test/browser.ini
+++ b/devtools/client/aboutdebugging/test/browser.ini
@@ -4,16 +4,18 @@ subsuite = devtools
 support-files =
   head.js
   addons/unpacked/bootstrap.js
   addons/unpacked/install.rdf
   addons/bad/manifest.json
   addons/bug1273184.xpi
   addons/test-devtools-webextension/*
   addons/test-devtools-webextension-nobg/*
+  service-workers/delay-sw.html
+  service-workers/delay-sw.js
   service-workers/empty-sw.html
   service-workers/empty-sw.js
   service-workers/push-sw.html
   service-workers/push-sw.js
   !/devtools/client/framework/test/shared-head.js
 
 [browser_addons_debug_bootstrapped.js]
 [browser_addons_debug_webextension.js]
@@ -29,12 +31,13 @@ tags = webextensions
 [browser_addons_reload.js]
 [browser_addons_toggle_debug.js]
 [browser_page_not_found.js]
 [browser_service_workers.js]
 [browser_service_workers_not_compatible.js]
 [browser_service_workers_push.js]
 [browser_service_workers_push_service.js]
 [browser_service_workers_start.js]
+[browser_service_workers_status.js]
 [browser_service_workers_timeout.js]
 skip-if = true # Bug 1232931
 [browser_service_workers_unregister.js]
 [browser_tabs.js]
--- a/devtools/client/aboutdebugging/test/browser_service_workers.js
+++ b/devtools/client/aboutdebugging/test/browser_service_workers.js
@@ -26,29 +26,23 @@ add_task(function* () {
   yield waitForMutation(serviceWorkersElement, { childList: true });
 
   // Check that the service worker appears in the UI
   let names = [...document.querySelectorAll("#service-workers .target-name")];
   names = names.map(element => element.textContent);
   ok(names.includes(SERVICE_WORKER),
     "The service worker url appears in the list: " + names);
 
-  // Finally, unregister the service worker itself
-  let aboutDebuggingUpdate = waitForMutation(serviceWorkersElement,
-    { childList: true });
-
   try {
-    yield unregisterServiceWorker(swTab);
+    yield unregisterServiceWorker(swTab, serviceWorkersElement);
     ok(true, "Service worker registration unregistered");
   } catch (e) {
     ok(false, "SW not unregistered; " + e);
   }
 
-  yield aboutDebuggingUpdate;
-
   // Check that the service worker disappeared from the UI
   names = [...document.querySelectorAll("#service-workers .target-name")];
   names = names.map(element => element.textContent);
   ok(!names.includes(SERVICE_WORKER),
     "The service worker url is no longer in the list: " + names);
 
   yield removeTab(swTab);
   yield closeAboutDebugging(tab);
--- a/devtools/client/aboutdebugging/test/browser_service_workers_push.js
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_push.js
@@ -57,21 +57,25 @@ add_task(function* () {
   // 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");
 
+  yield waitForServiceWorkerActivation(SERVICE_WORKER, document);
+
   // 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 " +
@@ -83,17 +87,17 @@ add_task(function* () {
     });
   });
   pushBtn.click();
   yield onPushNotification;
   ok(true, "Service worker received a push notification");
 
   // Finally, unregister the service worker itself.
   try {
-    yield unregisterServiceWorker(swTab);
+    yield unregisterServiceWorker(swTab, serviceWorkersElement);
     ok(true, "Service worker registration unregistered");
   } catch (e) {
     ok(false, "SW not unregistered; " + e);
   }
 
   yield removeTab(swTab);
   yield closeAboutDebugging(tab);
 });
--- a/devtools/client/aboutdebugging/test/browser_service_workers_push_service.js
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_push_service.js
@@ -67,45 +67,53 @@ add_task(function* () {
   let swTab = yield addTab(TAB_URL);
 
   // 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);
 
+  yield waitForServiceWorkerActivation(SERVICE_WORKER, document);
+
   // Wait for the service worker details to update.
   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 targetContainer = name.parentNode.parentNode;
   let targetDetailsElement = targetContainer.querySelector(".target-details");
-  yield waitForMutation(targetDetailsElement, { childList: true });
 
   // Retrieve the push subscription endpoint URL, and verify it looks good.
   let pushURL = targetContainer.querySelector(".service-worker-push-url");
+  if (!pushURL) {
+    yield waitForMutation(targetDetailsElement, { childList: true });
+    pushURL = targetContainer.querySelector(".service-worker-push-url");
+  }
+
   ok(pushURL, "Found the push service URL in the service worker details");
   is(pushURL.textContent, FAKE_ENDPOINT, "The push service URL looks correct");
 
   // Unsubscribe from the push service.
   ContentTask.spawn(swTab.linkedBrowser, {}, function () {
     let win = content.wrappedJSObject;
     return win.sub.unsubscribe();
   });
 
   // Wait for the service worker details to update again.
   yield waitForMutation(targetDetailsElement, { childList: true });
   ok(!targetContainer.querySelector(".service-worker-push-url"),
     "The push service URL should be removed");
 
   // Finally, unregister the service worker itself.
-  yield unregisterServiceWorker(swTab).then(() => {
+  try {
+    yield unregisterServiceWorker(swTab, serviceWorkersElement);
     ok(true, "Service worker registration unregistered");
-  }).catch(function (e) {
-    ok(false, "Service worker not unregistered; " + e);
-  });
+  } catch (e) {
+    ok(false, "SW not unregistered; " + e);
+  }
 
   info("Unmock the push service");
   PushService.service = null;
 
   yield removeTab(swTab);
   yield closeAboutDebugging(tab);
 });
--- a/devtools/client/aboutdebugging/test/browser_service_workers_start.js
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_start.js
@@ -42,16 +42,18 @@ add_task(function* () {
   // 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");
 
+  yield waitForServiceWorkerActivation(SERVICE_WORKER, document);
+
   // Retrieve the Target element corresponding to the service 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;
 
   // The service worker may already be killed with the low 1s timeout
   if (!targetElement.querySelector(".start-button")) {
@@ -77,17 +79,17 @@ add_task(function* () {
   yield onStarted;
 
   // Check that we have a Debug button but not a Start button again.
   ok(targetElement.querySelector(".debug-button"), "Found its debug button");
   ok(!targetElement.querySelector(".start-button"), "No start button");
 
   // Finally, unregister the service worker itself.
   try {
-    yield unregisterServiceWorker(swTab);
+    yield unregisterServiceWorker(swTab, serviceWorkersElement);
     ok(true, "Service worker registration unregistered");
   } catch (e) {
     ok(false, "SW not unregistered; " + e);
   }
 
   yield removeTab(swTab);
   yield closeAboutDebugging(tab);
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_status.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Service workers can't be loaded from chrome://,
+// but http:// is ok with dom.serviceWorkers.testing.enabled turned on.
+const SERVICE_WORKER = URL_ROOT + "service-workers/delay-sw.js";
+const TAB_URL = URL_ROOT + "service-workers/delay-sw.html";
+const SW_TIMEOUT = 2000;
+
+requestLongerTimeout(2);
+
+add_task(function* () {
+  yield SpecialPowers.pushPrefEnv({
+    "set": [
+      // Accept workers from mochitest's http.
+      ["dom.serviceWorkers.testing.enabled", true],
+      ["dom.serviceWorkers.idle_timeout", SW_TIMEOUT],
+      ["dom.serviceWorkers.idle_extended_timeout", SW_TIMEOUT],
+    ]
+  });
+
+  let { tab, document } = yield openAboutDebugging("workers");
+
+  // Listen for mutations in the service-workers list.
+  let serviceWorkersElement = getServiceWorkerList(document);
+  let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
+
+  let swTab = yield addTab(TAB_URL);
+
+  info("Make the test page notify us when the service worker sends a message.");
+
+  // Wait for the service-workers list to update.
+  yield onMutation;
+
+  // Check that the service worker appears in the UI
+  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 status = targetElement.querySelector(".target-status");
+  is(status.textContent, "Registering", "Service worker is currently registering");
+
+  yield waitForMutation(serviceWorkersElement, { childList: true, subtree: true });
+  is(status.textContent, "Running", "Service worker is currently running");
+
+  yield waitForMutation(serviceWorkersElement, { attributes: true, subtree: true });
+  is(status.textContent, "Stopped", "Service worker is currently stopped");
+
+  try {
+    yield unregisterServiceWorker(swTab, serviceWorkersElement);
+    ok(true, "Service worker unregistered");
+  } catch (e) {
+    ok(false, "Service worker not unregistered; " + e);
+  }
+
+  // Check that the service worker disappeared from the UI
+  names = [...document.querySelectorAll("#service-workers .target-name")];
+  names = names.map(element => element.textContent);
+  ok(!names.includes(SERVICE_WORKER),
+    "The service worker url is no longer in the list: " + names);
+
+  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
@@ -72,22 +72,19 @@ add_task(function* () {
   // 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.
   try {
-    yield unregisterServiceWorker(swTab);
+    yield unregisterServiceWorker(swTab, serviceWorkersElement);
     ok(true, "Service worker registration unregistered");
   } catch (e) {
     ok(false, "SW not unregistered; " + e);
   }
 
-  // Now ensure that the worker registration is correctly removed.
-  // The list should update once the registration is destroyed.
-  yield waitForMutation(serviceWorkersElement, { childList: true });
   assertHasTarget(false, document, "service-workers", SERVICE_WORKER);
 
   yield removeTab(swTab);
   yield closeAboutDebugging(tab);
 });
--- a/devtools/client/aboutdebugging/test/browser_service_workers_unregister.js
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_unregister.js
@@ -34,16 +34,18 @@ add_task(function* () {
   let swTab = yield addTab(TAB_URL);
 
   // 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);
 
+  yield waitForServiceWorkerActivation(SERVICE_WORKER, document);
+
   info("Ensure that the registration resolved before trying to interact with " +
     "the service worker.");
   yield waitForServiceWorkerRegistered(swTab);
   ok(true, "Service worker registration resolved");
 
   let targets = document.querySelectorAll("#service-workers .target");
   is(targets.length, 1, "One service worker is now displayed.");
 
--- a/devtools/client/aboutdebugging/test/head.js
+++ b/devtools/client/aboutdebugging/test/head.js
@@ -1,17 +1,18 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /* eslint-env browser */
 /* exported openAboutDebugging, changeAboutDebuggingHash, closeAboutDebugging,
    installAddon, uninstallAddon, waitForMutation, assertHasTarget,
    getServiceWorkerList, getTabList, openPanel, waitForInitialAddonList,
    waitForServiceWorkerRegistered, unregisterServiceWorker,
-   waitForDelayedStartupFinished, setupTestAboutDebuggingWebExtension */
+   waitForDelayedStartupFinished, setupTestAboutDebuggingWebExtension,
+   waitForServiceWorkerActivation */
 /* import-globals-from ../../framework/test/shared-head.js */
 
 "use strict";
 
 // Load the shared-head file first.
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
   this);
@@ -254,27 +255,32 @@ function waitForServiceWorkerRegistered(
     // Retrieve the `sw` promise created in the html page.
     let { sw } = content.wrappedJSObject;
     yield sw;
   });
 }
 
 /**
  * Asks the service worker within the test page to unregister, and returns a
- * promise that will resolve when it has successfully unregistered itself.
+ * promise that will resolve when it has successfully unregistered itself and the
+ * about:debugging UI has fully processed this update.
+ *
  * @param {Tab} tab
+ * @param {Node} serviceWorkersElement
  * @return {Promise} Resolves when the service worker is unregistered.
  */
-function unregisterServiceWorker(tab) {
-  return ContentTask.spawn(tab.linkedBrowser, {}, function* () {
+function* unregisterServiceWorker(tab, serviceWorkersElement) {
+  let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
+  yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     // Retrieve the `sw` promise created in the html page
     let { sw } = content.wrappedJSObject;
     let registration = yield sw;
     yield registration.unregister();
   });
+  return onMutation;
 }
 
 /**
  * Waits for the creation of a new window, usually used with create private
  * browsing window.
  * Returns a promise that will resolve when the window is successfully created.
  * @param {window} win
  */
@@ -321,8 +327,25 @@ function* setupTestAboutDebuggingWebExte
   let nameEl = names.filter(element => element.textContent === name)[0];
   ok(name, "Found the addon in the list");
   let targetElement = nameEl.parentNode.parentNode;
   let debugBtn = targetElement.querySelector(".debug-button");
   ok(debugBtn, "Found its debug button");
 
   return { tab, document, debugBtn };
 }
+
+/**
+ * Wait for aboutdebugging to be notified about the activation of the service worker
+ * corresponding to the provided service worker url.
+ */
+function* waitForServiceWorkerActivation(swUrl, document) {
+  let serviceWorkersElement = getServiceWorkerList(document);
+  let names = serviceWorkersElement.querySelectorAll(".target-name");
+  let name = [...names].filter(element => element.textContent === swUrl)[0];
+
+  let targetElement = name.parentNode.parentNode;
+  let targetStatus = targetElement.querySelector(".target-status");
+  while (targetStatus.textContent === "Registering") {
+    // Wait for the status to leave the "registering" stage.
+    yield waitForMutation(serviceWorkersElement, { childList: true, subtree: true });
+  }
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/service-workers/delay-sw.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <title>Service worker test</title>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+
+var sw = navigator.serviceWorker.register("delay-sw.js");
+sw.then(
+  function () {
+    dump("SW registered\n");
+  },
+  function (e) {
+    dump("SW not registered: " + e + "\n");
+  }
+);
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/service-workers/delay-sw.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env worker */
+
+"use strict";
+
+function wait(ms) {
+  return new Promise(resolve => {
+    setTimeout(resolve, ms);
+  });
+}
+
+// Wait for one second to switch from installing to installed.
+self.addEventListener("install", function (event) {
+  event.waitUntil(wait(1000));
+});
--- a/devtools/client/locales/en-US/aboutdebugging.properties
+++ b/devtools/client/locales/en-US/aboutdebugging.properties
@@ -66,19 +66,30 @@ reloadDisabledTooltip = Only temporarily
 # LOCALIZATION NOTE (workers):
 # This string is displayed as a header of the about:debugging#workers page.
 workers = Workers
 
 serviceWorkers = Service Workers
 sharedWorkers = Shared Workers
 otherWorkers = Other Workers
 
+# LOCALIZATION NOTE (running):
+# This string is displayed as the state of a service worker in RUNNING state.
 running = Running
+
+# LOCALIZATION NOTE (stopped):
+# This string is displayed as the state of a service worker in STOPPED state.
 stopped = Stopped
 
+# LOCALIZATION NOTE (registering):
+# This string is displayed as the state of a service worker for which no service worker
+# registration could be found yet. Only active registrations are visible from
+# about:debugging, so such service workers are considered as registering.
+registering = Registering
+
 # LOCALIZATION NOTE (tabs):
 # This string is displayed as a header of the about:debugging#tabs page.
 tabs = Tabs
 
 # LOCALIZATION NOTE (pageNotFound):
 # This string is displayed as the main message at any error/invalid page.
 pageNotFound = Page not found
 
--- a/devtools/server/actors/worker.js
+++ b/devtools/server/actors/worker.js
@@ -351,34 +351,63 @@ protocol.ActorClassWithSpec(serviceWorke
    * @param ServiceWorkerRegistrationInfo registration
    *   The registration's information.
    */
   initialize(conn, registration) {
     protocol.Actor.prototype.initialize.call(this, conn);
     this._conn = conn;
     this._registration = registration;
     this._pushSubscriptionActor = null;
+    this._registration.addListener(this);
     Services.obs.addObserver(this, PushService.subscriptionModifiedTopic, false);
   },
 
+  get installingWorkerForm() {
+    return this._getWorkerForm(this._registration.installingWorker);
+  },
+
+  get activeWorkerForm() {
+    return this._getWorkerForm(this._registration.activeWorker);
+  },
+
+  get waitingWorkerForm() {
+    return this._getWorkerForm(this._registration.waitingWorker);
+  },
+
+  _getWorkerForm: function (worker) {
+    if (!worker) {
+      return null;
+    }
+
+    return { url: worker.scriptSpec, state: worker.state };
+  },
+
+  onChange: function () {
+    events.emit(this, "registration-changed");
+  },
+
   form(detail) {
     if (detail === "actorid") {
       return this.actorID;
     }
     let registration = this._registration;
     return {
       actor: this.actorID,
       scope: registration.scope,
-      url: registration.scriptSpec
+      url: registration.scriptSpec,
+      installingWorker: this.installingWorkerForm,
+      activeWorker: this.activeWorkerForm,
+      waitingWorker: this.waitingWorkerForm,
     };
   },
 
   destroy() {
     protocol.Actor.prototype.destroy.call(this);
     Services.obs.removeObserver(this, PushService.subscriptionModifiedTopic, false);
+    this._registration.removeListener(this);
     this._registration = null;
     if (this._pushSubscriptionActor) {
       this._pushSubscriptionActor.destroy();
     }
     this._pushSubscriptionActor = null;
   },
 
   disconnect() {
--- a/devtools/shared/specs/worker.js
+++ b/devtools/shared/specs/worker.js
@@ -39,16 +39,19 @@ const pushSubscriptionSpec = generateAct
 exports.pushSubscriptionSpec = pushSubscriptionSpec;
 
 const serviceWorkerRegistrationSpec = generateActorSpec({
   typeName: "serviceWorkerRegistration",
 
   events: {
     "push-subscription-modified": {
       type: "push-subscription-modified"
+    },
+    "registration-changed": {
+      type: "registration-changed"
     }
   },
 
   methods: {
     start: {
       request: {},
       response: RetVal("json")
     },