mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1188545 - Add tests for service workers' lifetime management. r=nsm
This commit is contained in:
parent
ddeb807fe7
commit
b7c0f4f391
73
dom/push/test/lifetime_worker.js
Normal file
73
dom/push/test/lifetime_worker.js
Normal file
@ -0,0 +1,73 @@
|
||||
var state = "from_scope";
|
||||
var resolvePromiseCallback;
|
||||
|
||||
onfetch = function(event) {
|
||||
if (event.request.url.indexOf("lifetime_frame.html") >= 0) {
|
||||
event.respondWith(new Response("iframe_lifetime"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!event.client) {
|
||||
dump("ERROR: no client to post the message to!\n");
|
||||
dump("request.url=" + event.request.url + "\n");
|
||||
return;
|
||||
}
|
||||
|
||||
event.client.postMessage({type: "fetch", state: state});
|
||||
|
||||
if (event.request.url.indexOf("update") >= 0) {
|
||||
state = "update";
|
||||
} else if (event.request.url.indexOf("wait") >= 0) {
|
||||
event.respondWith(new Promise(function(res, rej) {
|
||||
if (resolvePromiseCallback) {
|
||||
dump("ERROR: service worker was already waiting on a promise.\n");
|
||||
}
|
||||
resolvePromiseCallback = function() {
|
||||
res(new Response("resolve_respondWithPromise"));
|
||||
};
|
||||
}));
|
||||
state = "wait";
|
||||
} else if (event.request.url.indexOf("release") >= 0) {
|
||||
state = "release";
|
||||
resolvePromise();
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePromise() {
|
||||
if (resolvePromiseCallback === undefined || resolvePromiseCallback == null) {
|
||||
dump("ERROR: wait promise was not set.\n");
|
||||
return;
|
||||
}
|
||||
resolvePromiseCallback();
|
||||
resolvePromiseCallback = null;
|
||||
}
|
||||
|
||||
onmessage = function(event) {
|
||||
// FIXME(catalinb): we cannot treat these events as extendable
|
||||
// yet. Bug 1143717
|
||||
event.source.postMessage({type: "message", state: state});
|
||||
state = event.data;
|
||||
if (event.data === "release") {
|
||||
resolvePromise();
|
||||
}
|
||||
}
|
||||
|
||||
onpush = function(event) {
|
||||
// FIXME(catalinb): push message carry no data. So we assume the only
|
||||
// push message we get is "wait"
|
||||
clients.matchAll().then(function(client) {
|
||||
if (client.length == 0) {
|
||||
dump("ERROR: no clients to send the response to.\n");
|
||||
}
|
||||
|
||||
client[0].postMessage({type: "push", state: state});
|
||||
|
||||
state = "wait";
|
||||
event.waitUntil(new Promise(function(res, rej) {
|
||||
if (resolvePromiseCallback) {
|
||||
dump("ERROR: service worker was already waiting on a promise.\n");
|
||||
}
|
||||
resolvePromiseCallback = res;
|
||||
}));
|
||||
});
|
||||
}
|
@ -5,6 +5,7 @@ support-files =
|
||||
push-server.sjs
|
||||
frame.html
|
||||
webpush.js
|
||||
lifetime_worker.js
|
||||
|
||||
[test_has_permissions.html]
|
||||
skip-if = os == "android" || toolkit == "gonk"
|
||||
@ -25,3 +26,5 @@ skip-if = os == "android" || toolkit == "gonk"
|
||||
# Disabled for too many intermittent failures (bug 1164432)
|
||||
# [test_try_registering_offline_disabled.html]
|
||||
# skip-if = os == "android" || toolkit == "gonk"
|
||||
[test_serviceworker_lifetime.html]
|
||||
skip-if = os == "android" || toolkit == "gonk"
|
||||
|
331
dom/push/test/test_serviceworker_lifetime.html
Normal file
331
dom/push/test/test_serviceworker_lifetime.html
Normal file
@ -0,0 +1,331 @@
|
||||
<!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 type="text/javascript" src="/tests/SimpleTest/SimpleTest.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 navigator.serviceWorker.ready.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;
|
||||
}
|
||||
|
||||
function sendPushToPushServer(pushEndpoint) {
|
||||
// Work around CORS for now.
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', "http://mochi.test:8888/tests/dom/push/test/push-server.sjs", true);
|
||||
xhr.setRequestHeader("X-Push-Method", "PUT");
|
||||
xhr.setRequestHeader("X-Push-Server", pushEndpoint);
|
||||
xhr.send("version=24601");
|
||||
}
|
||||
|
||||
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.parentNode.removeChild(ctx.iframe);
|
||||
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(expected, resolve, event) {
|
||||
ok(event.data.type == expected.type, "Received correct message type: " + expected.type);
|
||||
ok(event.data.state == expected.state, "Service worker is in the correct state: " + expected.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);
|
||||
}
|
||||
}
|
||||
|
||||
function setShutdownObserver(expectingEvent) {
|
||||
return function(ctx) {
|
||||
cancelShutdownObserver(ctx);
|
||||
|
||||
ctx.observer_promise = new Promise(function(res, rej) {
|
||||
ctx.observer = {
|
||||
observe: function(subject, topic, data) {
|
||||
ok((topic == "service-worker-shutdown") && expectingEvent, "Service worker was terminated.");
|
||||
this.remove(ctx);
|
||||
},
|
||||
remove: function(ctx) {
|
||||
SpecialPowers.removeObserver(this, "service-worker-shutdown");
|
||||
ctx.observer = null;
|
||||
res(ctx);
|
||||
}
|
||||
}
|
||||
SpecialPowers.addObserver(ctx.observer, "service-worker-shutdown", false);
|
||||
});
|
||||
|
||||
return ctx;
|
||||
}
|
||||
}
|
||||
|
||||
function waitOnShutdownObserver(ctx) {
|
||||
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((ctx) => res(ctx));
|
||||
}
|
||||
|
||||
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: function(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(createIframe)
|
||||
.then(setShutdownObserver(false))
|
||||
.then(checkStateAndUpdate(pushEvent, "from_scope", "wait"))
|
||||
.then(setShutdownObserver(true))
|
||||
.then(checkStateAndUpdate(messageEventIframe, "wait", "update"))
|
||||
.then(waitOnShutdownObserver)
|
||||
.then(closeIframe)
|
||||
}
|
||||
}
|
||||
|
||||
var test2 = {
|
||||
prefs: [
|
||||
["dom.serviceWorkers.idle_timeout", 0],
|
||||
["dom.serviceWorkers.idle_extended_timeout", 2999999]
|
||||
],
|
||||
steps: function(ctx) {
|
||||
// Non push workers are terminated when they stop controlling documents.
|
||||
return createIframe(ctx)
|
||||
.then(setShutdownObserver(true))
|
||||
.then(checkStateAndUpdate(fetchEvent, "from_scope", "wait"))
|
||||
.then(closeIframe)
|
||||
.then(waitOnShutdownObserver)
|
||||
|
||||
// Push workers are exempt from this rule.
|
||||
.then(createIframe)
|
||||
.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: function(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]
|
||||
],
|
||||
steps: function(ctx) {
|
||||
return new Promise(function(res, rej) {
|
||||
ctx.registration.active.postMessage("update");
|
||||
res(ctx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
SpecialPowers.pushPrefEnv({"set": [
|
||||
["dom.push.enabled", true],
|
||||
["dom.serviceWorkers.exemptFromPerDomainMax", true],
|
||||
["dom.serviceWorkers.enabled", true],
|
||||
["dom.serviceWorkers.testing.enabled", true],
|
||||
["dom.serviceWorkers.interception.enabled", true]
|
||||
]}, runTest);
|
||||
SpecialPowers.addPermission('push', true, document);
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -13,8 +13,7 @@ using namespace mozilla::dom;
|
||||
|
||||
BEGIN_WORKERS_NAMESPACE
|
||||
|
||||
#define SERVICE_WORKER_IDLE_TIMEOUT 30000 // ms
|
||||
#define SERVICE_WORKER_WAITUNTIL_TIMEOUT 300000 // ms
|
||||
NS_IMPL_ISUPPORTS0(ServiceWorkerPrivate)
|
||||
|
||||
// Tracks the "dom.disable_open_click_delay" preference. Modified on main
|
||||
// thread, read on worker threads.
|
||||
@ -1229,6 +1228,13 @@ ServiceWorkerPrivate::TerminateWorker()
|
||||
mIdleWorkerTimer->Cancel();
|
||||
mKeepAliveToken = nullptr;
|
||||
if (mWorkerPrivate) {
|
||||
if (Preferences::GetBool("dom.serviceWorkers.testing.enabled")) {
|
||||
nsCOMPtr<nsIObserverService> os = services::GetObserverService();
|
||||
if (os) {
|
||||
os->NotifyObservers(this, "service-worker-shutdown", nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
AutoJSAPI jsapi;
|
||||
jsapi.Init();
|
||||
NS_WARN_IF(!mWorkerPrivate->Terminate(jsapi.cx()));
|
||||
@ -1272,10 +1278,11 @@ ServiceWorkerPrivate::NoteIdleWorkerCallback(nsITimer* aTimer, void* aPrivate)
|
||||
// If we still have a workerPrivate at this point it means there are pending
|
||||
// waitUntil promises. Wait a bit more until we forcibly terminate the
|
||||
// worker.
|
||||
uint32_t timeout = Preferences::GetInt("dom.serviceWorkers.idle_extended_timeout");
|
||||
DebugOnly<nsresult> rv =
|
||||
swp->mIdleWorkerTimer->InitWithFuncCallback(ServiceWorkerPrivate::TerminateWorkerCallback,
|
||||
aPrivate,
|
||||
SERVICE_WORKER_WAITUNTIL_TIMEOUT,
|
||||
timeout,
|
||||
nsITimer::TYPE_ONE_SHOT);
|
||||
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
||||
}
|
||||
@ -1306,9 +1313,10 @@ ServiceWorkerPrivate::ResetIdleTimeout(WakeUpReason aWhy)
|
||||
mIsPushWorker = true;
|
||||
}
|
||||
|
||||
uint32_t timeout = Preferences::GetInt("dom.serviceWorkers.idle_timeout");
|
||||
DebugOnly<nsresult> rv =
|
||||
mIdleWorkerTimer->InitWithFuncCallback(ServiceWorkerPrivate::NoteIdleWorkerCallback,
|
||||
this, SERVICE_WORKER_IDLE_TIMEOUT,
|
||||
this, timeout,
|
||||
nsITimer::TYPE_ONE_SHOT);
|
||||
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
||||
if (!mKeepAliveToken) {
|
||||
|
@ -34,9 +34,10 @@ public:
|
||||
// object which can be cancelled if no events are received for a certain
|
||||
// amount of time. The worker is kept alive by holding a |KeepAliveToken|
|
||||
// reference.
|
||||
//
|
||||
// Extendable events hold tokens for the duration of their handler execution
|
||||
// and until their waitUntil promise is resolved, while ServiceWorkerPrivate
|
||||
// will hold a token for |SERVICE_WORKER_IDLE_TIMEOUT| seconds after each
|
||||
// will hold a token for |dom.serviceWorkers.idle_timeout| seconds after each
|
||||
// new event.
|
||||
//
|
||||
// Note: All timer events must be handled on the main thread because the
|
||||
@ -45,7 +46,7 @@ public:
|
||||
//
|
||||
// There are 3 cases where we may ignore keep alive tokens:
|
||||
// 1. When ServiceWorkerPrivate's token expired, if there are still waitUntil
|
||||
// handlers holding tokens, we wait another |SERVICE_WORKER_WAITUNTIL_TIMEOUT|
|
||||
// handlers holding tokens, we wait another |dom.serviceWorkers.idle_extended_timeout|
|
||||
// seconds before forcibly terminating the worker.
|
||||
// 2. If the worker stopped controlling documents and it is not handling push
|
||||
// events.
|
||||
@ -55,12 +56,12 @@ public:
|
||||
// with an appropriate reason before any runnable is dispatched to the worker.
|
||||
// If the event is extendable then the runnable should inherit
|
||||
// ExtendableEventWorkerRunnable.
|
||||
class ServiceWorkerPrivate final
|
||||
class ServiceWorkerPrivate final : public nsISupports
|
||||
{
|
||||
friend class KeepAliveToken;
|
||||
|
||||
public:
|
||||
NS_INLINE_DECL_REFCOUNTING(ServiceWorkerPrivate)
|
||||
NS_DECL_ISUPPORTS
|
||||
|
||||
explicit ServiceWorkerPrivate(ServiceWorkerInfo* aInfo);
|
||||
|
||||
@ -171,7 +172,7 @@ private:
|
||||
// is created.
|
||||
bool mIsPushWorker;
|
||||
|
||||
// We keep a token for |SERVICE_WORKER_IDLE_TIMEOUT| seconds to give the
|
||||
// We keep a token for |dom.serviceWorkers.idle_timeout| seconds to give the
|
||||
// worker a grace period after each event.
|
||||
nsRefPtr<KeepAliveToken> mKeepAliveToken;
|
||||
|
||||
|
@ -154,6 +154,12 @@ pref("dom.serviceWorkers.interception.enabled", false);
|
||||
// Allow service workers to intercept opaque (cross origin) responses
|
||||
pref("dom.serviceWorkers.interception.opaque.enabled", false);
|
||||
|
||||
// The amount of time (milliseconds) service workers keep running after each event.
|
||||
pref("dom.serviceWorkers.idle_timeout", 30000);
|
||||
|
||||
// The amount of time (milliseconds) service workers can be kept running using waitUntil promises.
|
||||
pref("dom.serviceWorkers.idle_extended_timeout", 300000);
|
||||
|
||||
// Whether nonzero values can be returned from performance.timing.*
|
||||
pref("dom.enable_performance", true);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user