tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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>