tor-browser

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

about-blank-replacement.https.html (8068B)


      1 <!DOCTYPE html>
      2 <title>Service Worker: about:blank replacement handling</title>
      3 <meta name=timeout content=long>
      4 <script src="/resources/testharness.js"></script>
      5 <script src="/resources/testharnessreport.js"></script>
      6 <script src="/common/get-host-info.sub.js"></script>
      7 <script src="resources/test-helpers.sub.js"></script>
      8 <body>
      9 <script>
     10 // This test attempts to verify various initial about:blank document
     11 // creation is accurately reflected via the Clients API.  The goal is
     12 // for Clients API to reflect what the browser actually does and not
     13 // to make special cases for the API.
     14 //
     15 // If your browser does not create an about:blank document in certain
     16 // cases then please just mark the test expected fail for now.  The
     17 // reuse of globals from about:blank documents to the final load document
     18 // has particularly bad interop at the moment.  Hopefully we can evolve
     19 // tests like this to eventually align browsers.
     20 
     21 const worker = 'resources/about-blank-replacement-worker.js';
     22 
     23 // Helper routine that creates an iframe that internally has some kind
     24 // of nested window.  The nested window could be another iframe or
     25 // it could be a popup window.
     26 function createFrameWithNestedWindow(url) {
     27  return new Promise((resolve, reject) => {
     28    let frame = document.createElement('iframe');
     29    frame.src = url;
     30    document.body.appendChild(frame);
     31 
     32    window.addEventListener('message', function onMsg(evt) {
     33      if (evt.data.type !== 'NESTED_LOADED') {
     34        return;
     35      }
     36      window.removeEventListener('message', onMsg);
     37      if (evt.data.result && evt.data.result.startsWith('failure:')) {
     38        reject(evt.data.result);
     39        return;
     40      }
     41      resolve(frame);
     42    });
     43  });
     44 }
     45 
     46 // Helper routine to request the given worker find the client with
     47 // the specified URL using the clients.matchAll() API.
     48 function getClientIdByURL(worker, url) {
     49  return new Promise(resolve => {
     50    navigator.serviceWorker.addEventListener('message', function onMsg(evt) {
     51      if (evt.data.type !== 'GET_CLIENT_ID') {
     52        return;
     53      }
     54      navigator.serviceWorker.removeEventListener('message', onMsg);
     55      resolve(evt.data.result);
     56    });
     57    worker.postMessage({ type: 'GET_CLIENT_ID', url: url.toString() });
     58  });
     59 }
     60 
     61 async function doAsyncTest(t, scope) {
     62  let reg = await service_worker_unregister_and_register(t, worker, scope);
     63 
     64  t.add_cleanup(() => service_worker_unregister(t, scope));
     65 
     66  await wait_for_state(t, reg.installing, 'activated');
     67 
     68  // Load the scope as a frame.  We expect this in turn to have a nested
     69  // iframe.  The service worker will intercept the load of the nested
     70  // iframe and populate its body with the client ID of the initial
     71  // about:blank document it sees via clients.matchAll().
     72  let frame = await createFrameWithNestedWindow(scope);
     73  let initialResult = frame.contentWindow.nested().document.body.textContent;
     74  assert_false(initialResult.startsWith('failure:'), `result: ${initialResult}`);
     75 
     76  assert_equals(frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
     77                frame.contentWindow.nested().navigator.serviceWorker.controller.scriptURL,
     78                'nested about:blank should have same controlling service worker');
     79 
     80  // Next, ask the service worker to find the final client ID for the fully
     81  // loaded nested frame.
     82  let nestedURL = new URL(frame.contentWindow.nested().location);
     83  let finalResult = await getClientIdByURL(reg.active, nestedURL);
     84  assert_false(finalResult.startsWith('failure:'), `result: ${finalResult}`);
     85 
     86  // If the nested frame doesn't have a URL to load, then there is no fetch
     87  // event and the body should be empty.  We can't verify the final client ID
     88  // against anything.
     89  if (nestedURL.href === 'about:blank' ||
     90      nestedURL.href === 'about:srcdoc') {
     91    assert_equals('', initialResult, 'about:blank text content should be blank');
     92  }
     93 
     94  // If the nested URL is not about:blank, though, then the fetch event handler
     95  // should have populated the body with the client id of the initial about:blank.
     96  // Verify the final client id matches.
     97  else {
     98    assert_equals(initialResult, finalResult, 'client ID values should match');
     99  }
    100 
    101  frame.remove();
    102 }
    103 
    104 promise_test(async function(t) {
    105  // Execute a test where the nested frame is simply loaded normally.
    106  await doAsyncTest(t, 'resources/about-blank-replacement-frame.py');
    107 }, 'Initial about:blank is controlled, exposed to clients.matchAll(), and ' +
    108   'matches final Client.');
    109 
    110 promise_test(async function(t) {
    111  // Execute a test where the nested frame is modified immediately by
    112  // its parent.  In this case we add a message listener so the service
    113  // worker can ping the client to verify its existence.  This ping-pong
    114  // check is performed during the initial load and when verifying the
    115  // final loaded client.
    116  await doAsyncTest(t, 'resources/about-blank-replacement-ping-frame.py');
    117 }, 'Initial about:blank modified by parent is controlled, exposed to ' +
    118   'clients.matchAll(), and matches final Client.');
    119 
    120 promise_test(async function(t) {
    121  // Execute a test where the nested window is a popup window instead of
    122  // an iframe.  This should behave the same as the simple iframe case.
    123  await doAsyncTest(t, 'resources/about-blank-replacement-popup-frame.py');
    124 }, 'Popup initial about:blank is controlled, exposed to clients.matchAll(), and ' +
    125   'matches final Client.');
    126 
    127 promise_test(async function(t) {
    128  const scope = 'resources/about-blank-replacement-uncontrolled-nested-frame.html';
    129 
    130  let reg = await service_worker_unregister_and_register(t, worker, scope);
    131 
    132  t.add_cleanup(() => service_worker_unregister(t, scope));
    133 
    134  await wait_for_state(t, reg.installing, 'activated');
    135 
    136  // Load the scope as a frame.  We expect this in turn to have a nested
    137  // iframe.  Unlike the other tests in this file the nested iframe URL
    138  // is not covered by a service worker scope.  It should end up as
    139  // uncontrolled even though its initial about:blank is controlled.
    140  let frame = await createFrameWithNestedWindow(scope);
    141  let nested = frame.contentWindow.nested();
    142  let initialResult = nested.document.body.textContent;
    143 
    144  // The nested iframe should not have been intercepted by the service
    145  // worker.  The empty.html nested frame has "hello world" for its body.
    146  assert_equals(initialResult.trim(), 'hello world', `result: ${initialResult}`);
    147 
    148  assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller, null,
    149                'outer frame should be controlled');
    150 
    151  assert_equals(nested.navigator.serviceWorker.controller, null,
    152                'nested frame should not be controlled');
    153 
    154  frame.remove();
    155 }, 'Initial about:blank is controlled, exposed to clients.matchAll(), and ' +
    156   'final Client is not controlled by a service worker.');
    157 
    158 promise_test(async function(t) {
    159  // Execute a test where the nested frame is an iframe without a src
    160  // attribute.  This simple nested about:blank should still inherit the
    161  // controller and be visible to clients.matchAll().
    162  await doAsyncTest(t, 'resources/about-blank-replacement-blank-nested-frame.html');
    163 }, 'Simple about:blank is controlled and is exposed to clients.matchAll().');
    164 
    165 promise_test(async function(t) {
    166  // Execute a test where the nested frame is an iframe using a non-empty
    167  // srcdoc containing only a tag pair so its textContent is still empty.
    168  // This nested iframe should still inherit the controller and be visible
    169  // to clients.matchAll().
    170  await doAsyncTest(t, 'resources/about-blank-replacement-srcdoc-nested-frame.html');
    171 }, 'Nested about:srcdoc is controlled and is exposed to clients.matchAll().');
    172 
    173 promise_test(async function(t) {
    174  // Execute a test where the nested frame is dynamically added without a src
    175  // attribute.  This simple nested about:blank should still inherit the
    176  // controller and be visible to clients.matchAll().
    177  await doAsyncTest(t, 'resources/about-blank-replacement-blank-dynamic-nested-frame.html');
    178 }, 'Dynamic about:blank is controlled and is exposed to clients.matchAll().');
    179 
    180 </script>
    181 </body>