tor-browser

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

nav-cancelation-2.sub.html (8430B)


      1 <!DOCTYPE html>
      2 <meta charset="utf-8">
      3 <title>Grandparent main frame cancels a navigation in a cross-origin grandchild</title>
      4 <script src="/resources/testharness.js"></script>
      5 <script src="/resources/testharnessreport.js"></script>
      6 
      7 <!--
      8  This test asserts that an ancestor canceling a cross-origin descendant's
      9  ongoing navigation does not result in load events firing in the ancestor
     10  synchronously.
     11 
     12  The reason this test uses a grandparent/grandchild pair to represent the
     13  ancestor/descendant, instead of a parent/child pair, is because if a child
     14  frame is blocking its parent window's load event, that means the child frame
     15  navigation is being made from the initial about:blank Document to some
     16  resource, and the initial about:blank child is synchronously scriptable from
     17  the parent since they share the same window agent. This test is trying to
     18  capture the scenario where the descendant document (that owns the ongoing
     19  navigation) is hosted/scheduled on a different agent than the ancestor
     20  document that cancels the descendant's ongoing navigation. The only way to do
     21  this is to have a grandparent frame load a cross-origin child, whose document
     22  itself loads a child frame that has a very slow ongoing navigation. That way
     23  the grandparent can reach the grandchild via `window.frames[0].frames[0]`,
     24  which is a proxy to the document living in a different agent.
     25 -->
     26 
     27 <body>
     28 
     29 <iframe src="http://{{domains[www1]}}:{{ports[http][0]}}/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/resources/nav-cancelation-2-helper.html"></iframe>
     30 
     31 <script>
     32 promise_test(async t => {
     33  let window_load_fired = false;
     34  let iframe_load_fired = false;
     35  let grandchild_iframe_load_fired = false;
     36  const iframe = document.querySelector('iframe');
     37 
     38  const window_load_promise = new Promise(resolve => {
     39    window.onload = () => {
     40      window_load_fired = true;
     41      resolve();
     42    }
     43  });
     44 
     45  const iframe_onload_promise = new Promise(resolve => {
     46    iframe.onload = () => {
     47      iframe_load_fired = true;
     48      resolve();
     49    }
     50  });
     51 
     52  // Let the grandchild frame get registered in window.frames.
     53  await new Promise((resolve, reject) => {
     54    window.addEventListener('message', e => {
     55      if (e.data != "grandchild frame created") {
     56        reject(Error(`Expected 'grandchild frame created', but got ${e.data}`));
     57      }
     58 
     59      resolve();
     60    }, {once: true});
     61  });
     62 
     63  // Set up a message handler to listen to the grandchild's iframe element's
     64  // load event being fired. We should never get this message, and we assert
     65  // this below. If we ever get this message, that means one of two things:
     66  //   1.) The grandparent (this document)'s load event was blocked on the
     67  //       completion of its grandchild's subsequent navigation (after
     68  //       cancelation)
     69  //   2.) After the grandchild's navigation was canceled, its <iframe>'s load
     70  //       event was fired before its subsequent navigation completed
     71  // Both of these are wrong.
     72  addEventListener('message', e => {
     73    assert_equals(e.data, "grandchild frame loaded",
     74        `Expected 'grandchild frame loaded', but got ${e.data}`);
     75    grandchild_iframe_load_fired = true;
     76  });
     77 
     78  // While the grandchild navigation is in-flight, cancel it and record when the
     79  // our `load` event fires. The second navigation is a slow resource so that
     80  // the speed of the network doesn't cause the grandchild load event to fire
     81  // early and confuse the grandparent when running the assertions below. We're
     82  // trying to clearly separate out when the grandparent load event fires vs
     83  // when the grandchild load event fires.
     84  window.frames[0].frames[0].location.href = "resources/slow.py?different";
     85 
     86  // Synchronously after cancelation, no load events should have been fired.
     87  assert_false(window_load_fired,
     88      "Grandparent's load event does not synchronously fire after grandchild " +
     89      "navigation cancelation");
     90  assert_false(iframe_load_fired,
     91      "<iframe> load event does not synchronously fire after grandchild " +
     92      "navigation cancelation");
     93  assert_false(grandchild_iframe_load_fired,
     94      "Grandchild <iframe>'s load event does not synchronously fire upon " +
     95      "navigation cancelation");
     96 
     97  // Load events did not fire in a microtask after cancelation.
     98  await Promise.resolve();
     99  assert_false(window_load_fired,
    100      "Grandparent's load event does not fire in the microtask after " +
    101      "navigation canceled");
    102  assert_false(iframe_load_fired,
    103      "<iframe> load event does not fire in the microtask after navigation " +
    104      "canceled");
    105  assert_false(grandchild_iframe_load_fired,
    106      "Grandchild <iframe> load event does not fire in the microtask after " +
    107      "navigation canceled");
    108 
    109  // Canceling the navigation should however, asynchronously unblock, in this
    110  // order:
    111  //   1.) Our child window's load event, captured by our `iframe`'s load event
    112  //   2.) Our window load event
    113  // On the other hand, the grandchild navigation should still be ongoing, so
    114  // inside our child's document, the nested <iframe> representing our
    115  // grandchild should not have had its load event fired yet.
    116  await iframe_onload_promise;
    117  assert_true(iframe_load_fired);
    118  assert_false(window_load_fired,
    119      "Grandparent's load event does not fire before its child iframe's load " +
    120      "event");
    121  assert_false(grandchild_iframe_load_fired,
    122      "Grandchild <iframe>'s load event does not fire before its parent's load " +
    123      "event and grandparent's load event");
    124 
    125  // We want to assert that the grandparent is not (incorrectly) blocked on its
    126  // grandchild's second navigation from completing. One sign that it was
    127  // incorrectly blocked on its grandchild's second navigation is if the
    128  // grandparent receives a message (saying that the grandchild <iframe>
    129  // element's load event fired) before the grandparent's load event fires.
    130  //
    131  // This indicates a weird state where the grandparent's immediate child fired
    132  // its load event in response to navigation cancelation (see the assertions
    133  // above), but the grandparent itself is still blocked on the grandchild
    134  // loading. If this is the case, the postMessage() (that sets
    135  // `grandchild_iframe_load_fired = true`) is received by the grandparent just
    136  // before the grandparent's load event is unblocked and fired. Therefore we
    137  // can detect this situation by checking `grandchild_iframe_load_fired`.
    138  await window_load_promise;
    139  assert_true(iframe_load_fired);
    140  assert_true(window_load_fired,
    141      "Grandparent's load event fires asynchronously after grandchild " +
    142      "navigation cancelation");
    143  assert_false(grandchild_iframe_load_fired,
    144      "Grandchild <iframe> load event doesn't fire before grandparent's " +
    145      "load event");
    146 
    147  // Verify that the grandchild <iframe>'s load event does not fire within one
    148  // task of the grandchild's load event from being fired. This is to further
    149  // verify that the grandparent's load event is not tied to its grandchild's
    150  // second navigation.
    151  //
    152  // If for example, the grandparent's load event *is* blocked on the
    153  // grandchild's second navigation from finishing, it is still possible for the
    154  // grandparent's load event to fire. For example, Chromium has a bug where if
    155  // both are true:
    156  //   1.) The grandparent frame is in the same process as the grandchild frame
    157  //   2.) The grandparent frame's load event is blocked on its grandchild's
    158  //       second navigation
    159  //
    160  // ...then the following will happen:
    161  //   1.) The grandchild's load event will fire, triggering a postMessage() to
    162  //       the grandparent frame. This queues a task to run the grandparent's
    163  //       message handler.
    164  //   2.) The grandparent's load event will *immediately* fire, and the
    165  //       postMessage() will fire a single task later since it is queued.
    166  //
    167  // Therefore, we assert that `grandchild_iframe_load_fired` is not true up to
    168  // a single task after the grandparent's load event fires.
    169  await new Promise(resolve => {
    170    t.step_timeout(resolve, 0);
    171  });
    172 
    173  assert_false(grandchild_iframe_load_fired,
    174      "Grandchild <iframe>'s load event does not fire at least one task " +
    175      "after the grandparent's window load event fires. It should only fire " +
    176      "when its subsequent navigation is complete");
    177 }, "grandparent cancels a pending navigation in a cross-origin grandchild");
    178 </script>