summaryrefslogtreecommitdiffstats
path: root/dom/push/test/test_serviceworker_lifetime.html
diff options
context:
space:
mode:
Diffstat (limited to 'dom/push/test/test_serviceworker_lifetime.html')
-rw-r--r--dom/push/test/test_serviceworker_lifetime.html364
1 files changed, 364 insertions, 0 deletions
diff --git a/dom/push/test/test_serviceworker_lifetime.html b/dom/push/test/test_serviceworker_lifetime.html
new file mode 100644
index 0000000000..30f191a119
--- /dev/null
+++ b/dom/push/test/test_serviceworker_lifetime.html
@@ -0,0 +1,364 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test the lifetime management of service workers. We keep this test in
+ dom/push/tests to pass the external network check when connecting to
+ the mozilla push service.
+
+ How this test works:
+ - the service worker maintains a state variable and a promise used for
+ extending its lifetime. Note that the terminating the worker will reset
+ these variables to their default values.
+ - we send 3 types of requests to the service worker:
+ |update|, |wait| and |release|. All three requests will cause the sw to update
+ its state to the new value and reply with a message containing
+ its previous state. Furthermore, |wait| will set a waitUntil or a respondWith
+ promise that's not resolved until the next |release| message.
+ - Each subtest will use a combination of values for the timeouts and check
+ if the service worker is in the correct state as we send it different
+ events.
+ - We also wait and assert for service worker termination using an event dispatched
+ through nsIObserverService.
+ -->
+<head>
+ <title>Test for Bug 1188545</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/dom/push/test/test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
+</head>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1188545">Mozilla Bug 118845</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="text/javascript">
+ function start() {
+ return navigator.serviceWorker.register("lifetime_worker.js", {scope: "./"})
+ .then((swr) => ({registration: swr}));
+ }
+
+ function waitForActiveServiceWorker(ctx) {
+ return waitForActive(ctx.registration).then(function(result) {
+ ok(ctx.registration.active, "Service Worker is active");
+ return ctx;
+ });
+ }
+
+ function unregister(ctx) {
+ return ctx.registration.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ }
+
+ function registerPushNotification(ctx) {
+ var p = new Promise(function(res, rej) {
+ ctx.registration.pushManager.subscribe().then(
+ function(pushSubscription) {
+ ok(true, "successful registered for push notification");
+ ctx.subscription = pushSubscription;
+ res(ctx);
+ }, function(error) {
+ ok(false, "could not register for push notification");
+ res(ctx);
+ });
+ });
+ return p;
+ }
+
+ var mockSocket = new MockWebSocket();
+ var endpoint = "https://example.com/endpoint/1";
+ var channelID = null;
+ mockSocket.onRegister = function(request) {
+ channelID = request.channelID;
+ this.serverSendMsg(JSON.stringify({
+ messageType: "register",
+ uaid: "fa8f2e4b-5ddc-4408-b1e3-5f25a02abff0",
+ channelID,
+ status: 200,
+ pushEndpoint: endpoint,
+ }));
+ };
+
+ function sendPushToPushServer(pushEndpoint) {
+ is(pushEndpoint, endpoint, "Got unexpected endpoint");
+ mockSocket.serverSendMsg(JSON.stringify({
+ messageType: "notification",
+ version: "vDummy",
+ channelID,
+ }));
+ }
+
+ function unregisterPushNotification(ctx) {
+ return ctx.subscription.unsubscribe().then(function(result) {
+ ok(result, "unsubscribe should succeed.");
+ ctx.subscription = null;
+ return ctx;
+ });
+ }
+
+ function createIframe(ctx) {
+ var p = new Promise(function(res, rej) {
+ var iframe = document.createElement("iframe");
+ // This file doesn't exist, the service worker will give us an empty
+ // document.
+ iframe.src = "http://mochi.test:8888/tests/dom/push/test/lifetime_frame.html";
+
+ iframe.onload = function() {
+ ctx.iframe = iframe;
+ res(ctx);
+ };
+ document.body.appendChild(iframe);
+ });
+ return p;
+ }
+
+ function closeIframe(ctx) {
+ ctx.iframe.remove();
+ return new Promise(function(res, rej) {
+ // XXXcatalinb: give the worker more time to "notice" it stopped
+ // controlling documents
+ ctx.iframe = null;
+ setTimeout(res, 0);
+ }).then(() => ctx);
+ }
+
+ function waitAndCheckMessage(contentWindow, expected) {
+ function checkMessage({ type, state }, resolve, event) {
+ ok(event.data.type == type, "Received correct message type: " + type);
+ ok(event.data.state == state, "Service worker is in the correct state: " + state);
+ this.navigator.serviceWorker.onmessage = null;
+ resolve();
+ }
+ return new Promise(function(res, rej) {
+ contentWindow.navigator.serviceWorker.onmessage =
+ checkMessage.bind(contentWindow, expected, res);
+ });
+ }
+
+ function fetchEvent(ctx, expected_state, new_state) {
+ var expected = { type: "fetch", state: expected_state };
+ var p = waitAndCheckMessage(ctx.iframe.contentWindow, expected);
+ ctx.iframe.contentWindow.fetch(new_state);
+ return p;
+ }
+
+ function pushEvent(ctx, expected_state, new_state) {
+ var expected = {type: "push", state: expected_state};
+ var p = waitAndCheckMessage(ctx.iframe.contentWindow, expected);
+ sendPushToPushServer(ctx.subscription.endpoint);
+ return p;
+ }
+
+ function messageEventIframe(ctx, expected_state, new_state) {
+ var expected = {type: "message", state: expected_state};
+ var p = waitAndCheckMessage(ctx.iframe.contentWindow, expected);
+ ctx.iframe.contentWindow.navigator.serviceWorker.controller.postMessage(new_state);
+ return p;
+ }
+
+ function messageEvent(ctx, expected_state, new_state) {
+ var expected = {type: "message", state: expected_state};
+ var p = waitAndCheckMessage(window, expected);
+ ctx.registration.active.postMessage(new_state);
+ return p;
+ }
+
+ function checkStateAndUpdate(eventFunction, expected_state, new_state) {
+ return function(ctx) {
+ return eventFunction(ctx, expected_state, new_state)
+ .then(() => ctx);
+ };
+ }
+
+ let shutdownTopic = "specialpowers-service-worker-shutdown";
+ SpecialPowers.registerObservers("service-worker-shutdown");
+
+ function setShutdownObserver(expectingEvent) {
+ info("Setting shutdown observer: expectingEvent=" + expectingEvent);
+ return function(ctx) {
+ cancelShutdownObserver(ctx);
+
+ ctx.observer_promise = new Promise(function(res, rej) {
+ ctx.observer = {
+ observe(subject, topic, data) {
+ ok((topic == shutdownTopic) && expectingEvent, "Service worker was terminated.");
+ this.remove(ctx);
+ },
+ remove(context) {
+ SpecialPowers.removeObserver(this, shutdownTopic);
+ context.observer = null;
+ res(context);
+ },
+ };
+ SpecialPowers.addObserver(ctx.observer, shutdownTopic);
+ });
+
+ return ctx;
+ };
+ }
+
+ function waitOnShutdownObserver(ctx) {
+ info("Waiting on worker to shutdown.");
+ return ctx.observer_promise;
+ }
+
+ function cancelShutdownObserver(ctx) {
+ if (ctx.observer) {
+ ctx.observer.remove(ctx);
+ }
+ return ctx.observer_promise;
+ }
+
+ function subTest(test) {
+ return function(ctx) {
+ return new Promise(function(res, rej) {
+ function run() {
+ test.steps(ctx).catch(function(e) {
+ ok(false, "Some test failed with error: " + e);
+ }).then(res);
+ }
+
+ SpecialPowers.pushPrefEnv({"set": test.prefs}, run);
+ });
+ };
+ }
+
+ var test1 = {
+ prefs: [
+ ["dom.serviceWorkers.idle_timeout", 0],
+ ["dom.serviceWorkers.idle_extended_timeout", 2999999],
+ ],
+ // Test that service workers are terminated after the grace period expires
+ // when there are no pending waitUntil or respondWith promises.
+ steps(ctx) {
+ // Test with fetch events and respondWith promises
+ return createIframe(ctx)
+ .then(setShutdownObserver(true))
+ .then(checkStateAndUpdate(fetchEvent, "from_scope", "update"))
+ .then(waitOnShutdownObserver)
+ .then(setShutdownObserver(false))
+ .then(checkStateAndUpdate(fetchEvent, "from_scope", "wait"))
+ .then(checkStateAndUpdate(fetchEvent, "wait", "update"))
+ .then(checkStateAndUpdate(fetchEvent, "update", "update"))
+ .then(setShutdownObserver(true))
+ // The service worker should be terminated when the promise is resolved.
+ .then(checkStateAndUpdate(fetchEvent, "update", "release"))
+ .then(waitOnShutdownObserver)
+ .then(setShutdownObserver(false))
+ .then(closeIframe)
+ .then(cancelShutdownObserver)
+
+ // Test with push events and message events
+ .then(setShutdownObserver(true))
+ .then(createIframe)
+ // Make sure we are shutdown before entering our "no shutdown" sequence
+ // to avoid races.
+ .then(waitOnShutdownObserver)
+ .then(setShutdownObserver(false))
+ .then(checkStateAndUpdate(pushEvent, "from_scope", "wait"))
+ .then(checkStateAndUpdate(messageEventIframe, "wait", "update"))
+ .then(checkStateAndUpdate(messageEventIframe, "update", "update"))
+ .then(setShutdownObserver(true))
+ .then(checkStateAndUpdate(messageEventIframe, "update", "release"))
+ .then(waitOnShutdownObserver)
+ .then(closeIframe);
+ },
+ };
+
+ var test2 = {
+ prefs: [
+ ["dom.serviceWorkers.idle_timeout", 0],
+ ["dom.serviceWorkers.idle_extended_timeout", 2999999],
+ ],
+ steps(ctx) {
+ // Older versions used to terminate workers when the last controlled
+ // window was closed. This should no longer happen, though. Verify
+ // the new behavior.
+ setShutdownObserver(true)(ctx);
+ return createIframe(ctx)
+ // Make sure we are shutdown before entering our "no shutdown" sequence
+ // to avoid races.
+ .then(waitOnShutdownObserver)
+ .then(setShutdownObserver(false))
+ .then(checkStateAndUpdate(fetchEvent, "from_scope", "wait"))
+ .then(closeIframe)
+ .then(setShutdownObserver(true))
+ .then(checkStateAndUpdate(messageEvent, "wait", "release"))
+ .then(waitOnShutdownObserver)
+
+ // Push workers were exempt from the old rule and should continue to
+ // survive past the closing of the last controlled window.
+ .then(setShutdownObserver(true))
+ .then(createIframe)
+ // Make sure we are shutdown before entering our "no shutdown" sequence
+ // to avoid races.
+ .then(waitOnShutdownObserver)
+ .then(setShutdownObserver(false))
+ .then(checkStateAndUpdate(pushEvent, "from_scope", "wait"))
+ .then(closeIframe)
+ .then(setShutdownObserver(true))
+ .then(checkStateAndUpdate(messageEvent, "wait", "release"))
+ .then(waitOnShutdownObserver);
+ },
+ };
+
+ var test3 = {
+ prefs: [
+ ["dom.serviceWorkers.idle_timeout", 2999999],
+ ["dom.serviceWorkers.idle_extended_timeout", 0],
+ ],
+ steps(ctx) {
+ // set the grace period to 0 and dispatch a message which will reset
+ // the internal sw timer to the new value.
+ var test3_1 = {
+ prefs: [
+ ["dom.serviceWorkers.idle_timeout", 0],
+ ["dom.serviceWorkers.idle_extended_timeout", 0],
+ ],
+ steps(context) {
+ return new Promise(function(res, rej) {
+ context.iframe.contentWindow.navigator.serviceWorker.controller.postMessage("ping");
+ res(context);
+ });
+ },
+ };
+
+ // Test that service worker is closed when the extended timeout expired
+ return createIframe(ctx)
+ .then(setShutdownObserver(false))
+ .then(checkStateAndUpdate(messageEvent, "from_scope", "update"))
+ .then(checkStateAndUpdate(messageEventIframe, "update", "update"))
+ .then(checkStateAndUpdate(fetchEvent, "update", "wait"))
+ .then(setShutdownObserver(true))
+ .then(subTest(test3_1)) // This should cause the internal timer to expire.
+ .then(waitOnShutdownObserver)
+ .then(closeIframe);
+ },
+ };
+
+ function runTest() {
+ start()
+ .then(waitForActiveServiceWorker)
+ .then(registerPushNotification)
+ .then(subTest(test1))
+ .then(subTest(test2))
+ .then(subTest(test3))
+ .then(unregisterPushNotification)
+ .then(unregister)
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ }).then(SimpleTest.finish);
+ }
+
+ setupPrefsAndMockSocket(mockSocket).then(_ => runTest());
+ SpecialPowers.addPermission("desktop-notification", true, document);
+ SimpleTest.waitForExplicitFinish();
+</script>
+</body>
+</html>