Source code

Revision control

Other Tools

Test Info: Warnings

  • This test gets skipped with pattern: os == "android" serviceworker_e10s || !e10s || os == "win" || os =="linux" && bits == 64 || os =="mac"
<!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>
<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.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.getBoolPref("dom.serviceWorkers.parent_intercept", false) ?
"specialpowers-service-worker-shutdown" :
"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>