tor-browser

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

insertion-removing-steps-iframe.window.js (7241B)


      1 // These tests ensure that:
      2 //   1. The HTML element insertion steps for iframes [1] run *after* all DOM
      3 //      insertion mutations associated with any given call to
      4 //      #concept-node-insert [2] (which may insert many elements at once).
      5 //      Consequently, a preceding element's insertion steps can observe the
      6 //      side-effects of later elements being connected to the DOM, but cannot
      7 //      observe the side-effects of the later element's own insertion steps [1],
      8 //      since insertion steps are run in order after all DOM insertion mutations
      9 //      are complete.
     10 //   2. The HTML element removing steps for iframes [3] *do not* synchronously
     11 //      run script during child navigable destruction. Therefore, script cannot
     12 //      observe the state of the DOM in the middle of iframe removal, even when
     13 //      multiple iframes are being removed in the same task. Iframe removal,
     14 //      from the perspective of the parent's DOM tree, is atomic.
     15 //
     16 // [1]: https://html.spec.whatwg.org/C#the-iframe-element:html-element-insertion-steps
     17 // [2]: https://dom.spec.whatwg.org/#concept-node-insert
     18 // [3]: https://html.spec.whatwg.org/C#the-iframe-element:html-element-removing-steps
     19 
     20 promise_test(async t => {
     21  const fragment = new DocumentFragment();
     22 
     23  const iframe1 = fragment.appendChild(document.createElement('iframe'));
     24  const iframe2 = fragment.appendChild(document.createElement('iframe'));
     25 
     26  t.add_cleanup(() => {
     27    iframe1.remove();
     28    iframe2.remove();
     29  });
     30 
     31  let iframe1Loaded = false, iframe2Loaded = false;
     32  iframe1.onload = t.step_func(e => {
     33    // iframe1 assertions:
     34    iframe1Loaded = true;
     35    assert_equals(window.frames.length, 1,
     36        "iframe1 load event can observe its own participation in the frame " +
     37        "tree");
     38    assert_equals(iframe1.contentWindow, window.frames[0]);
     39 
     40    // iframe2 assertions:
     41    assert_false(iframe2Loaded,
     42        "iframe2's load event hasn't fired before iframe1's");
     43    assert_true(iframe2.isConnected,
     44        "iframe1 can observe that iframe2 is connected to the DOM...");
     45    assert_equals(iframe2.contentWindow, null,
     46        "... but iframe1 cannot observe iframe2's contentWindow because " +
     47        "iframe2's insertion steps have not been run yet");
     48  });
     49 
     50  iframe2.onload = t.step_func(e => {
     51    iframe2Loaded = true;
     52    assert_equals(window.frames.length, 2,
     53        "iframe2 load event can observe its own participation in the frame tree");
     54    assert_equals(iframe1.contentWindow, window.frames[0]);
     55    assert_equals(iframe2.contentWindow, window.frames[1]);
     56  });
     57 
     58  // Synchronously consecutively adds both `iframe1` and `iframe2` to the DOM,
     59  // invoking their insertion steps (and thus firing each of their `load`
     60  // events) in order. `iframe1` will be able to observe itself in the DOM but
     61  // not `iframe2`, and `iframe2` will be able to observe both itself and
     62  // `iframe1`.
     63  document.body.append(fragment);
     64  assert_true(iframe1Loaded, "iframe1 loaded");
     65  assert_true(iframe2Loaded, "iframe2 loaded");
     66 }, "Insertion steps: load event fires synchronously *after* iframe DOM " +
     67   "insertion, as part of the iframe element's insertion steps");
     68 
     69 // There are several versions of the removal variant, since there are several
     70 // ways to remove multiple elements "at once". For example:
     71 //   1. `node.innerHTML = ''` ultimately runs
     72 //      https://dom.spec.whatwg.org/#concept-node-replace-all which removes all
     73 //      of a node's children.
     74 //   2. `node.replaceChildren()` which follows roughly the same path above.
     75 //   3. `node.remove()` on a parent of many children will invoke not the DOM
     76 //      remove algorithm, but rather the "removing steps" hook [1], for each
     77 //      child.
     78 //
     79 // [1]: https://dom.spec.whatwg.org/#concept-node-remove-ext
     80 
     81 function runRemovalTest(removal_method) {
     82  promise_test(async t => {
     83    const div = document.createElement('div');
     84 
     85    const iframe1 = div.appendChild(document.createElement('iframe'));
     86    const iframe2 = div.appendChild(document.createElement('iframe'));
     87    document.body.append(div);
     88 
     89    // Now that both iframes have been inserted into the DOM, we'll set up a
     90    // MutationObserver that we'll use to ensure that multiple synchronous
     91    // mutations (removals) are only observed atomically at the end. Specifically,
     92    // the observer's callback is not invoked synchronously for each removal.
     93    let observerCallbackInvoked = false;
     94    const removalObserver = new MutationObserver(t.step_func(mutations => {
     95      assert_false(observerCallbackInvoked,
     96          "MO callback is only invoked once, not multiple times, i.e., for " +
     97          "each removal");
     98      observerCallbackInvoked = true;
     99      assert_equals(mutations.length, 1, "Exactly one MutationRecord is recorded");
    100      assert_equals(mutations[0].removedNodes.length, 2);
    101      assert_equals(window.frames.length, 0,
    102          "No iframe Windows exist when the MO callback is run");
    103      assert_equals(document.querySelector('iframe'), null,
    104          "No iframe elements are connected to the DOM when the MO callback is " +
    105          "run");
    106    }));
    107 
    108    removalObserver.observe(div, {childList: true});
    109    t.add_cleanup(() => removalObserver.disconnect());
    110 
    111    let iframe1UnloadFired = false, iframe2UnloadFired = false;
    112    let iframe1PagehideFired = false, iframe2PagehideFired = false;
    113    iframe1.contentWindow.addEventListener('pagehide', e => {
    114      assert_false(iframe1UnloadFired, "iframe1 pagehide fires before unload");
    115      iframe1PagehideFired = true;
    116    });
    117    iframe2.contentWindow.addEventListener('pagehide', e => {
    118      assert_false(iframe2UnloadFired, "iframe2 pagehide fires before unload");
    119      iframe2PagehideFired = true;
    120    });
    121    iframe1.contentWindow.addEventListener('unload', e => iframe1UnloadFired = true);
    122    iframe2.contentWindow.addEventListener('unload', e => iframe2UnloadFired = true);
    123 
    124    // Each `removal_method` will trigger the synchronous removal of each of
    125    // `div`'s (iframe) children. This will synchronously, consecutively
    126    // invoke HTML's "destroy a child navigable" (per [1]), for each iframe.
    127    //
    128    // [1]: https://html.spec.whatwg.org/C#the-iframe-element:destroy-a-child-navigable
    129 
    130    if (removal_method === 'replaceChildren') {
    131      div.replaceChildren();
    132    } else if (removal_method === 'remove') {
    133      div.remove();
    134    } else if (removal_method === 'innerHTML') {
    135      div.innerHTML = '';
    136    }
    137 
    138    assert_false(iframe1PagehideFired, "iframe1 pagehide did not fire");
    139    assert_false(iframe2PagehideFired, "iframe2 pagehide did not fire");
    140    assert_false(iframe1UnloadFired, "iframe1 unload did not fire");
    141    assert_false(iframe2UnloadFired, "iframe2 unload did not fire");
    142 
    143    assert_false(observerCallbackInvoked,
    144        "MO callback is not invoked synchronously after removals");
    145 
    146    // Wait one microtask.
    147    await Promise.resolve();
    148 
    149    if (removal_method !== 'remove') {
    150      assert_true(observerCallbackInvoked,
    151          "MO callback is invoked asynchronously after removals");
    152    }
    153  }, `Removing steps (${removal_method}): script does not run synchronously during iframe destruction`);
    154 }
    155 
    156 runRemovalTest('innerHTML');
    157 runRemovalTest('replaceChildren');
    158 runRemovalTest('remove');