tor-browser

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

test-helpers.sub.js (10485B)


      1 // Adapter for testharness.js-style tests with Service Workers
      2 
      3 /**
      4 * @param options an object that represents RegistrationOptions except for scope.
      5 * @param options.type a WorkerType.
      6 * @param options.updateViaCache a ServiceWorkerUpdateViaCache.
      7 * @see https://w3c.github.io/ServiceWorker/#dictdef-registrationoptions
      8 */
      9 function service_worker_unregister_and_register(test, url, scope, options) {
     10  if (!scope || scope.length == 0)
     11    return Promise.reject(new Error('tests must define a scope'));
     12 
     13  if (options && options.scope)
     14    return Promise.reject(new Error('scope must not be passed in options'));
     15 
     16  options = Object.assign({ scope: scope }, options);
     17  return service_worker_unregister(test, scope)
     18    .then(function() {
     19        return navigator.serviceWorker.register(url, options);
     20      })
     21    .catch(unreached_rejection(test,
     22                               'unregister and register should not fail'));
     23 }
     24 
     25 // This unregisters the registration that precisely matches scope. Use this
     26 // when unregistering by scope. If no registration is found, it just resolves.
     27 function service_worker_unregister(test, scope) {
     28  var absoluteScope = (new URL(scope, window.location).href);
     29  return navigator.serviceWorker.getRegistration(scope)
     30    .then(function(registration) {
     31        if (registration && registration.scope === absoluteScope)
     32          return registration.unregister();
     33      })
     34    .catch(unreached_rejection(test, 'unregister should not fail'));
     35 }
     36 
     37 function service_worker_unregister_and_done(test, scope) {
     38  return service_worker_unregister(test, scope)
     39    .then(test.done.bind(test));
     40 }
     41 
     42 function unreached_fulfillment(test, prefix) {
     43  return test.step_func(function(result) {
     44      var error_prefix = prefix || 'unexpected fulfillment';
     45      assert_unreached(error_prefix + ': ' + result);
     46    });
     47 }
     48 
     49 // Rejection-specific helper that provides more details
     50 function unreached_rejection(test, prefix) {
     51  return test.step_func(function(error) {
     52      var reason = error.message || error.name || error;
     53      var error_prefix = prefix || 'unexpected rejection';
     54      assert_unreached(error_prefix + ': ' + reason);
     55    });
     56 }
     57 
     58 /**
     59 * Adds an iframe to the document and returns a promise that resolves to the
     60 * iframe when it finishes loading. The caller is responsible for removing the
     61 * iframe later if needed.
     62 *
     63 * @param {string} url
     64 * @returns {HTMLIFrameElement}
     65 */
     66 function with_iframe(url) {
     67  return new Promise(function(resolve) {
     68      var frame = document.createElement('iframe');
     69      frame.className = 'test-iframe';
     70      frame.src = url;
     71      frame.onload = function() { resolve(frame); };
     72      document.body.appendChild(frame);
     73    });
     74 }
     75 
     76 function normalizeURL(url) {
     77  return new URL(url, self.location).toString().replace(/#.*$/, '');
     78 }
     79 
     80 function wait_for_update(test, registration) {
     81  if (!registration || registration.unregister == undefined) {
     82    return Promise.reject(new Error(
     83      'wait_for_update must be passed a ServiceWorkerRegistration'));
     84  }
     85 
     86  return new Promise(test.step_func(function(resolve) {
     87      var handler = test.step_func(function() {
     88        registration.removeEventListener('updatefound', handler);
     89        resolve(registration.installing);
     90      });
     91      registration.addEventListener('updatefound', handler);
     92    }));
     93 }
     94 
     95 // Return true if |state_a| is more advanced than |state_b|.
     96 function is_state_advanced(state_a, state_b) {
     97  if (state_b === 'installing') {
     98    switch (state_a) {
     99      case 'installed':
    100      case 'activating':
    101      case 'activated':
    102      case 'redundant':
    103        return true;
    104    }
    105  }
    106 
    107  if (state_b === 'installed') {
    108    switch (state_a) {
    109      case 'activating':
    110      case 'activated':
    111      case 'redundant':
    112        return true;
    113    }
    114  }
    115 
    116  if (state_b === 'activating') {
    117    switch (state_a) {
    118      case 'activated':
    119      case 'redundant':
    120        return true;
    121    }
    122  }
    123 
    124  if (state_b === 'activated') {
    125    switch (state_a) {
    126      case 'redundant':
    127        return true;
    128    }
    129  }
    130  return false;
    131 }
    132 
    133 function wait_for_state(test, worker, state) {
    134  if (!worker || worker.state == undefined) {
    135    return Promise.reject(new Error(
    136      'wait_for_state needs a ServiceWorker object to be passed.'));
    137  }
    138  if (worker.state === state)
    139    return Promise.resolve(state);
    140 
    141  if (is_state_advanced(worker.state, state)) {
    142    return Promise.reject(new Error(
    143      `Waiting for ${state} but the worker is already ${worker.state}.`));
    144  }
    145  return new Promise(test.step_func(function(resolve, reject) {
    146      worker.addEventListener('statechange', test.step_func(function() {
    147          if (worker.state === state)
    148            resolve(state);
    149 
    150          if (is_state_advanced(worker.state, state)) {
    151            reject(new Error(
    152              `The state of the worker becomes ${worker.state} while waiting` +
    153                `for ${state}.`));
    154          }
    155        }));
    156    }));
    157 }
    158 
    159 // Declare a test that runs entirely in the ServiceWorkerGlobalScope. The |url|
    160 // is the service worker script URL. This function:
    161 // - Instantiates a new test with the description specified in |description|.
    162 //   The test will succeed if the specified service worker can be successfully
    163 //   registered and installed.
    164 // - Creates a new ServiceWorker registration with a scope unique to the current
    165 //   document URL. Note that this doesn't allow more than one
    166 //   service_worker_test() to be run from the same document.
    167 // - Waits for the new worker to begin installing.
    168 // - Imports tests results from tests running inside the ServiceWorker.
    169 function service_worker_test(url, description) {
    170  // If the document URL is https://example.com/document and the script URL is
    171  // https://example.com/script/worker.js, then the scope would be
    172  // https://example.com/script/scope/document.
    173  var scope = new URL('scope' + window.location.pathname,
    174                      new URL(url, window.location)).toString();
    175  promise_test(function(test) {
    176      return service_worker_unregister_and_register(test, url, scope)
    177        .then(function(registration) {
    178            add_completion_callback(function() {
    179                registration.unregister();
    180              });
    181            return wait_for_update(test, registration)
    182              .then(function(worker) {
    183                  return fetch_tests_from_worker(worker);
    184                });
    185          });
    186    }, description);
    187 }
    188 
    189 function base_path() {
    190  return location.pathname.replace(/\/[^\/]*$/, '/');
    191 }
    192 
    193 function test_login(test, origin, username, password, cookie) {
    194  return new Promise(function(resolve, reject) {
    195      with_iframe(
    196        origin + base_path() +
    197        'resources/fetch-access-control-login.html')
    198        .then(test.step_func(function(frame) {
    199            var channel = new MessageChannel();
    200            channel.port1.onmessage = test.step_func(function() {
    201                frame.remove();
    202                resolve();
    203              });
    204            frame.contentWindow.postMessage(
    205              {username: username, password: password, cookie: cookie},
    206              origin, [channel.port2]);
    207          }));
    208    });
    209 }
    210 
    211 function test_websocket(test, frame, url) {
    212  return new Promise(function(resolve, reject) {
    213      var ws = new frame.contentWindow.WebSocket(url, ['echo', 'chat']);
    214      var openCalled = false;
    215      ws.addEventListener('open', test.step_func(function(e) {
    216          assert_equals(ws.readyState, 1, "The WebSocket should be open");
    217          openCalled = true;
    218          ws.close();
    219        }), true);
    220 
    221      ws.addEventListener('close', test.step_func(function(e) {
    222          assert_true(openCalled, "The WebSocket should be closed after being opened");
    223          resolve();
    224        }), true);
    225 
    226      ws.addEventListener('error', reject);
    227    });
    228 }
    229 
    230 function login_https(test) {
    231  var host_info = get_host_info();
    232  return test_login(test, host_info.HTTPS_REMOTE_ORIGIN,
    233                    'username1s', 'password1s', 'cookie1')
    234    .then(function() {
    235        return test_login(test, host_info.HTTPS_ORIGIN,
    236                          'username2s', 'password2s', 'cookie2');
    237      });
    238 }
    239 
    240 function websocket(test, frame) {
    241  return test_websocket(test, frame, get_websocket_url());
    242 }
    243 
    244 function get_websocket_url() {
    245  return 'wss://{{host}}:{{ports[wss][0]}}/echo';
    246 }
    247 
    248 // The navigator.serviceWorker.register() method guarantees that the newly
    249 // installing worker is available as registration.installing when its promise
    250 // resolves. However some tests test installation using a <link> element where
    251 // it is possible for the installing worker to have already become the waiting
    252 // or active worker. So this method is used to get the newest worker when these
    253 // tests need access to the ServiceWorker itself.
    254 function get_newest_worker(registration) {
    255  if (registration.installing)
    256    return registration.installing;
    257  if (registration.waiting)
    258    return registration.waiting;
    259  if (registration.active)
    260    return registration.active;
    261 }
    262 
    263 function register_using_link(script, options) {
    264  var scope = options.scope;
    265  var link = document.createElement('link');
    266  link.setAttribute('rel', 'serviceworker');
    267  link.setAttribute('href', script);
    268  link.setAttribute('scope', scope);
    269  document.getElementsByTagName('head')[0].appendChild(link);
    270  return new Promise(function(resolve, reject) {
    271        link.onload = resolve;
    272        link.onerror = reject;
    273      })
    274    .then(() => navigator.serviceWorker.getRegistration(scope));
    275 }
    276 
    277 function with_sandboxed_iframe(url, sandbox) {
    278  return new Promise(function(resolve) {
    279      var frame = document.createElement('iframe');
    280      frame.sandbox = sandbox;
    281      frame.src = url;
    282      frame.onload = function() { resolve(frame); };
    283      document.body.appendChild(frame);
    284    });
    285 }
    286 
    287 // Registers, waits for activation, then unregisters on a sample scope.
    288 //
    289 // This can be used to wait for a period of time needed to register,
    290 // activate, and then unregister a service worker.  When checking that
    291 // certain behavior does *NOT* happen, this is preferable to using an
    292 // arbitrary delay.
    293 async function wait_for_activation_on_sample_scope(t, window_or_workerglobalscope) {
    294  const script = '/service-workers/service-worker/resources/empty-worker.js';
    295  const scope = 'resources/there/is/no/there/there?' + Date.now();
    296  let registration = await window_or_workerglobalscope.navigator.serviceWorker.register(script, { scope });
    297  await wait_for_state(t, registration.installing, 'activated');
    298  await registration.unregister();
    299 }