test_serviceworker_lifetime.html (12660B)
1 <!DOCTYPE HTML> 2 <html> 3 <!-- 4 Test the lifetime management of service workers. We keep this test in 5 dom/push/tests to pass the external network check when connecting to 6 the mozilla push service. 7 8 How this test works: 9 - the service worker maintains a state variable and a promise used for 10 extending its lifetime. Note that the terminating the worker will reset 11 these variables to their default values. 12 - we send 3 types of requests to the service worker: 13 |update|, |wait| and |release|. All three requests will cause the sw to update 14 its state to the new value and reply with a message containing 15 its previous state. Furthermore, |wait| will set a waitUntil or a respondWith 16 promise that's not resolved until the next |release| message. 17 - Each subtest will use a combination of values for the timeouts and check 18 if the service worker is in the correct state as we send it different 19 events. 20 - We also wait and assert for service worker termination using an event dispatched 21 through nsIObserverService. 22 --> 23 <head> 24 <title>Test for Bug 1188545</title> 25 <script src="/tests/SimpleTest/SimpleTest.js"></script> 26 <script type="text/javascript" src="/tests/dom/push/test/test_utils.js"></script> 27 <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> 28 <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> 29 </head> 30 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1188545">Mozilla Bug 118845</a> 31 <p id="display"></p> 32 <div id="content" style="display: none"> 33 34 </div> 35 <pre id="test"> 36 </pre> 37 38 <script class="testbody" type="text/javascript"> 39 function start() { 40 return navigator.serviceWorker.register("lifetime_worker.js", {scope: "./"}) 41 .then((swr) => ({registration: swr})); 42 } 43 44 function waitForActiveServiceWorker(ctx) { 45 return waitForActive(ctx.registration).then(function() { 46 ok(ctx.registration.active, "Service Worker is active"); 47 return ctx; 48 }); 49 } 50 51 function unregister(ctx) { 52 return ctx.registration.unregister().then(function(result) { 53 ok(result, "Unregister should return true."); 54 }, function(e) { 55 dump("Unregistering the SW failed with " + e + "\n"); 56 }); 57 } 58 59 function registerPushNotification(ctx) { 60 var p = new Promise(function(res) { 61 ctx.registration.pushManager.subscribe().then( 62 function(pushSubscription) { 63 ok(true, "successful registered for push notification"); 64 ctx.subscription = pushSubscription; 65 res(ctx); 66 }, function() { 67 ok(false, "could not register for push notification"); 68 res(ctx); 69 }); 70 }); 71 return p; 72 } 73 74 var mockSocket = new MockWebSocket(); 75 var endpoint = "https://example.com/endpoint/1"; 76 var channelID = null; 77 mockSocket.onRegister = function(request) { 78 channelID = request.channelID; 79 this.serverSendMsg(JSON.stringify({ 80 messageType: "register", 81 uaid: "fa8f2e4b-5ddc-4408-b1e3-5f25a02abff0", 82 channelID, 83 status: 200, 84 pushEndpoint: endpoint, 85 })); 86 }; 87 88 function sendPushToPushServer(pushEndpoint) { 89 is(pushEndpoint, endpoint, "Got unexpected endpoint"); 90 mockSocket.serverSendMsg(JSON.stringify({ 91 messageType: "notification", 92 version: "vDummy", 93 channelID, 94 })); 95 } 96 97 function unregisterPushNotification(ctx) { 98 return ctx.subscription.unsubscribe().then(function(result) { 99 ok(result, "unsubscribe should succeed."); 100 ctx.subscription = null; 101 return ctx; 102 }); 103 } 104 105 function createIframe(ctx) { 106 var p = new Promise(function(res) { 107 var iframe = document.createElement("iframe"); 108 // This file doesn't exist, the service worker will give us an empty 109 // document. 110 iframe.src = "http://mochi.test:8888/tests/dom/push/test/lifetime_frame.html"; 111 112 iframe.onload = function() { 113 ctx.iframe = iframe; 114 res(ctx); 115 }; 116 document.body.appendChild(iframe); 117 }); 118 return p; 119 } 120 121 function closeIframe(ctx) { 122 ctx.iframe.remove(); 123 return new Promise(function(res) { 124 // XXXcatalinb: give the worker more time to "notice" it stopped 125 // controlling documents 126 ctx.iframe = null; 127 setTimeout(res, 0); 128 }).then(() => ctx); 129 } 130 131 function waitAndCheckMessage(contentWindow, expected) { 132 function checkMessage({ type, state }, resolve, event) { 133 ok(event.data.type == type, "Received correct message type: " + type); 134 ok(event.data.state == state, "Service worker is in the correct state: " + state); 135 this.navigator.serviceWorker.onmessage = null; 136 resolve(); 137 } 138 return new Promise(function(res) { 139 contentWindow.navigator.serviceWorker.onmessage = 140 checkMessage.bind(contentWindow, expected, res); 141 }); 142 } 143 144 function fetchEvent(ctx, expected_state, new_state) { 145 var expected = { type: "fetch", state: expected_state }; 146 var p = waitAndCheckMessage(ctx.iframe.contentWindow, expected); 147 ctx.iframe.contentWindow.fetch(new_state); 148 return p; 149 } 150 151 function pushEvent(ctx, expected_state) { 152 var expected = {type: "push", state: expected_state}; 153 var p = waitAndCheckMessage(ctx.iframe.contentWindow, expected); 154 sendPushToPushServer(ctx.subscription.endpoint); 155 return p; 156 } 157 158 function messageEventIframe(ctx, expected_state, new_state) { 159 var expected = {type: "message", state: expected_state}; 160 var p = waitAndCheckMessage(ctx.iframe.contentWindow, expected); 161 ctx.iframe.contentWindow.navigator.serviceWorker.controller.postMessage(new_state); 162 return p; 163 } 164 165 function messageEvent(ctx, expected_state, new_state) { 166 var expected = {type: "message", state: expected_state}; 167 var p = waitAndCheckMessage(window, expected); 168 ctx.registration.active.postMessage(new_state); 169 return p; 170 } 171 172 function checkStateAndUpdate(eventFunction, expected_state, new_state) { 173 return function(ctx) { 174 return eventFunction(ctx, expected_state, new_state) 175 .then(() => ctx); 176 }; 177 } 178 179 let shutdownTopic = "specialpowers-service-worker-shutdown"; 180 SpecialPowers.registerObservers("service-worker-shutdown"); 181 182 function setShutdownObserver(expectingEvent) { 183 info("Setting shutdown observer: expectingEvent=" + expectingEvent); 184 return function(ctx) { 185 cancelShutdownObserver(ctx); 186 187 ctx.observer_promise = new Promise(function(res) { 188 ctx.observer = { 189 observe(subject, topic) { 190 ok((topic == shutdownTopic) && expectingEvent, "Service worker was terminated."); 191 this.remove(ctx); 192 }, 193 remove(context) { 194 SpecialPowers.removeObserver(this, shutdownTopic); 195 context.observer = null; 196 res(context); 197 }, 198 }; 199 SpecialPowers.addObserver(ctx.observer, shutdownTopic); 200 }); 201 202 return ctx; 203 }; 204 } 205 206 function waitOnShutdownObserver(ctx) { 207 info("Waiting on worker to shutdown."); 208 return ctx.observer_promise; 209 } 210 211 function cancelShutdownObserver(ctx) { 212 if (ctx.observer) { 213 ctx.observer.remove(ctx); 214 } 215 return ctx.observer_promise; 216 } 217 218 function subTest(test) { 219 return function(ctx) { 220 return new Promise(function(res) { 221 function run() { 222 test.steps(ctx).catch(function(e) { 223 ok(false, "Some test failed with error: " + e); 224 }).then(res); 225 } 226 227 SpecialPowers.pushPrefEnv({"set": test.prefs}, run); 228 }); 229 }; 230 } 231 232 var test1 = { 233 prefs: [ 234 ["dom.serviceWorkers.idle_timeout", 0], 235 ["dom.serviceWorkers.idle_extended_timeout", 2999999], 236 ], 237 // Test that service workers are terminated after the grace period expires 238 // when there are no pending waitUntil or respondWith promises. 239 steps(ctx) { 240 // Test with fetch events and respondWith promises 241 return createIframe(ctx) 242 .then(setShutdownObserver(true)) 243 .then(checkStateAndUpdate(fetchEvent, "from_scope", "update")) 244 .then(waitOnShutdownObserver) 245 .then(setShutdownObserver(false)) 246 .then(checkStateAndUpdate(fetchEvent, "from_scope", "wait")) 247 .then(checkStateAndUpdate(fetchEvent, "wait", "update")) 248 .then(checkStateAndUpdate(fetchEvent, "update", "update")) 249 .then(setShutdownObserver(true)) 250 // The service worker should be terminated when the promise is resolved. 251 .then(checkStateAndUpdate(fetchEvent, "update", "release")) 252 .then(waitOnShutdownObserver) 253 .then(setShutdownObserver(false)) 254 .then(closeIframe) 255 .then(cancelShutdownObserver) 256 257 // Test with push events and message events 258 .then(setShutdownObserver(true)) 259 .then(createIframe) 260 // Make sure we are shutdown before entering our "no shutdown" sequence 261 // to avoid races. 262 .then(waitOnShutdownObserver) 263 .then(setShutdownObserver(false)) 264 .then(checkStateAndUpdate(pushEvent, "from_scope", "wait")) 265 .then(checkStateAndUpdate(messageEventIframe, "wait", "update")) 266 .then(checkStateAndUpdate(messageEventIframe, "update", "update")) 267 .then(setShutdownObserver(true)) 268 .then(checkStateAndUpdate(messageEventIframe, "update", "release")) 269 .then(waitOnShutdownObserver) 270 .then(closeIframe); 271 }, 272 }; 273 274 var test2 = { 275 prefs: [ 276 ["dom.serviceWorkers.idle_timeout", 0], 277 ["dom.serviceWorkers.idle_extended_timeout", 2999999], 278 ], 279 steps(ctx) { 280 // Older versions used to terminate workers when the last controlled 281 // window was closed. This should no longer happen, though. Verify 282 // the new behavior. 283 setShutdownObserver(true)(ctx); 284 return createIframe(ctx) 285 // Make sure we are shutdown before entering our "no shutdown" sequence 286 // to avoid races. 287 .then(waitOnShutdownObserver) 288 .then(setShutdownObserver(false)) 289 .then(checkStateAndUpdate(fetchEvent, "from_scope", "wait")) 290 .then(closeIframe) 291 .then(setShutdownObserver(true)) 292 .then(checkStateAndUpdate(messageEvent, "wait", "release")) 293 .then(waitOnShutdownObserver) 294 295 // Push workers were exempt from the old rule and should continue to 296 // survive past the closing of the last controlled window. 297 .then(setShutdownObserver(true)) 298 .then(createIframe) 299 // Make sure we are shutdown before entering our "no shutdown" sequence 300 // to avoid races. 301 .then(waitOnShutdownObserver) 302 .then(setShutdownObserver(false)) 303 .then(checkStateAndUpdate(pushEvent, "from_scope", "wait")) 304 .then(closeIframe) 305 .then(setShutdownObserver(true)) 306 .then(checkStateAndUpdate(messageEvent, "wait", "release")) 307 .then(waitOnShutdownObserver); 308 }, 309 }; 310 311 var test3 = { 312 prefs: [ 313 ["dom.serviceWorkers.idle_timeout", 2999999], 314 ["dom.serviceWorkers.idle_extended_timeout", 0], 315 ], 316 steps(ctx) { 317 // set the grace period to 0 and dispatch a message which will reset 318 // the internal sw timer to the new value. 319 var test3_1 = { 320 prefs: [ 321 ["dom.serviceWorkers.idle_timeout", 0], 322 ["dom.serviceWorkers.idle_extended_timeout", 0], 323 ], 324 steps(context) { 325 return new Promise(function(res) { 326 context.iframe.contentWindow.navigator.serviceWorker.controller.postMessage("ping"); 327 res(context); 328 }); 329 }, 330 }; 331 332 // Test that service worker is closed when the extended timeout expired 333 return createIframe(ctx) 334 .then(setShutdownObserver(false)) 335 .then(checkStateAndUpdate(messageEvent, "from_scope", "update")) 336 .then(checkStateAndUpdate(messageEventIframe, "update", "update")) 337 .then(checkStateAndUpdate(fetchEvent, "update", "wait")) 338 .then(setShutdownObserver(true)) 339 .then(subTest(test3_1)) // This should cause the internal timer to expire. 340 .then(waitOnShutdownObserver) 341 .then(closeIframe); 342 }, 343 }; 344 345 function runTest() { 346 start() 347 .then(waitForActiveServiceWorker) 348 .then(registerPushNotification) 349 .then(subTest(test1)) 350 .then(subTest(test2)) 351 .then(subTest(test3)) 352 .then(unregisterPushNotification) 353 .then(unregister) 354 .catch(function(e) { 355 ok(false, "Some test failed with error " + e); 356 }).then(SimpleTest.finish); 357 } 358 359 setupPrefsAndMockSocket(mockSocket).then(_ => runTest()); 360 SpecialPowers.addPermission("desktop-notification", true, document); 361 SimpleTest.waitForExplicitFinish(); 362 </script> 363 </body> 364 </html>