tor-browser

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

moving-between-documents-helper.js (8749B)


      1 "use strict";
      2 
      3 function createDocument(documentType, result, inlineOrExternal, type, hasBlockingStylesheet) {
      4  return new Promise((resolve, reject) => {
      5    const iframe = document.createElement("iframe");
      6    iframe.src =
      7      "resources/moving-between-documents-iframe.py" +
      8      "?result=" + result +
      9      "&inlineOrExternal=" + inlineOrExternal +
     10      "&type=" + type +
     11      "&hasBlockingStylesheet=" + hasBlockingStylesheet +
     12      "&cache=" + Math.random();
     13    // As blocking stylesheets delays Document load events, we use
     14    // DOMContentLoaded here.
     15    // After that point, we expect iframe.contentDocument exists
     16    // while still waiting for blocking stylesheet loading.
     17    document.body.appendChild(iframe);
     18 
     19    window.addEventListener('message', (event) => {
     20      if (documentType === "iframe") {
     21        resolve([iframe.contentWindow, iframe.contentDocument]);
     22      } else if (documentType === "createHTMLDocument") {
     23        resolve([
     24            iframe.contentWindow,
     25            iframe.contentDocument.implementation.createHTMLDocument("")]);
     26      } else {
     27        reject(new Error("Invalid document type: " + documentType));
     28      }
     29    }, {once: true});
     30  });
     31 }
     32 
     33 window.didExecute = undefined;
     34 
     35 // For a script, there are three associated Documents that can
     36 // potentially different:
     37 //
     38 // [1] script's parser document
     39 //     https://html.spec.whatwg.org/C/#parser-document
     40 //
     41 // [2] script's preparation-time document
     42 //     https://html.spec.whatwg.org/C/#preparation-time-document
     43 //     == script's node document at the beginning of #prepare-a-script
     44 //
     45 // [3] script's node document at the beginning of
     46 //     #execute-the-script-block
     47 //
     48 // This helper is for tests where [1]/[2]/[3] are different.
     49 
     50 // In the spec, scripts are executed only if [1]/[2]/[3] are all the same
     51 // (or [1] is null and [2]==[3]).
     52 //
     53 // A check for [1]==[2] is in #prepare-a-script and
     54 // a check for [1]==[3] is in #execute-the-script-block,
     55 // but these are under debate: https://github.com/whatwg/html/issues/2137
     56 //
     57 // A check for [2]==[3] is in #execute-the-script-block, which is added by
     58 // https://github.com/whatwg/html/pull/2673
     59 
     60 // timing:
     61 //   "before-prepare":
     62 //     A <script> is moved during parsing before #prepare-a-script.
     63 //     [1] != [2] == [3]
     64 //
     65 //   "after-prepare":
     66 //     A <script> is moved after parsing/#prepare-a-script but
     67 //     before #execute-the-script-block.
     68 //     [1] == [2] != [3]
     69 //
     70 //     To move such scripts, #has-a-style-sheet-that-is-blocking-scripts
     71 //     is utilized to block inline scripts after #prepare-a-script.
     72 //     Note: this is a corner case in the spec which might be removed
     73 //     from the spec in the future, e.g.
     74 //     https://github.com/whatwg/html/issues/1349
     75 //     https://github.com/chrishtr/rendering/blob/master/stylesheet-loading-proposal.md
     76 //
     77 //   TODO(domfarolino): Remove the "parsing but moved back" tests, because if a
     78 //   <script> is moved before #prepare-a-script, per spec it should never make
     79 //   it to #execute-the-script-block. If an implementation does not implement
     80 //   the check in #prepare-a-script, then it will fail the "before-prepare"
     81 //   tests, so these are not necessary.
     82 //   "parsing but moved back"
     83 //     A <script> is moved before #prepare-a-script, but moved back again
     84 //     to the original Document after #prepare-a-script.
     85 //     [1] == [3] != [2]
     86 //
     87 // destType: "iframe" or "createHTMLDocument".
     88 // result: "fetch-error", "parse-error", or "success".
     89 // inlineOrExternal: "inline" or "external" or "empty-src".
     90 // type: "classic" or "module".
     91 async function runTest(timing, destType, result, inlineOrExternal, type) {
     92  const description =
     93      `Move ${result} ${inlineOrExternal} ${type} script ` +
     94      `to ${destType} ${timing}`;
     95 
     96  const t = async_test("Eval: " + description);
     97  const tScriptLoadEvent = async_test("<script> load: " + description);
     98  const tScriptErrorEvent = async_test("<script> error: " + description);
     99  const tWindowErrorEvent = async_test("window error: " + description);
    100 
    101  // If scripts should be moved after #prepare-a-script before
    102  // #execute-the-script-block, we add a style sheet that is
    103  // blocking scripts.
    104  const hasBlockingStylesheet =
    105      timing === "after-prepare" || timing === "move-back";
    106 
    107  const [sourceWindow, sourceDocument] = await createDocument(
    108      "iframe", result, inlineOrExternal, type, hasBlockingStylesheet);
    109 
    110  // Due to https://crbug.com/1034176, Chromium needs
    111  // blocking stylesheets also in the destination Documents.
    112  const [destWindow, destDocument] = await createDocument(
    113      destType, null, null, null, hasBlockingStylesheet);
    114 
    115  const scriptOnLoad =
    116    tScriptLoadEvent.unreached_func("Script load event fired unexpectedly");
    117  const scriptOnError = (event) => {
    118    // For Firefox: Prevent window.onerror is fired due to propagation
    119    // from <script>'s error event.
    120    event.stopPropagation();
    121 
    122    tScriptErrorEvent.unreached_func("Script error evennt fired unexpectedly")();
    123  };
    124 
    125  sourceWindow.didExecute = false;
    126  sourceWindow.t = t;
    127  sourceWindow.scriptOnLoad = scriptOnLoad;
    128  sourceWindow.scriptOnError = scriptOnError;
    129  sourceWindow.onerror = tWindowErrorEvent.unreached_func(
    130      "Window error event shouldn't fired on source window");
    131  sourceWindow.readyToEvaluate = false;
    132 
    133  destWindow.didExecute = false;
    134  destWindow.t = t;
    135  destWindow.scriptOnLoad = scriptOnLoad;
    136  destWindow.scriptOnError = scriptOnError;
    137  destWindow.onerror = tWindowErrorEvent.unreached_func(
    138      "Window error event shouldn't fired on destination window");
    139  destWindow.readyToEvaluate = false;
    140 
    141  // t=0 sec: Move between documents before #prepare-a-script.
    142  // At this time, the script element is not yet inserted to the DOM.
    143  if (timing === "before-prepare" || timing === "move-back") {
    144    destDocument.body.appendChild(
    145      sourceDocument.querySelector("streaming-element"));
    146  }
    147  if (timing === "before-prepare") {
    148    sourceWindow.readyToEvaluate = true;
    149    destWindow.readyToEvaluate = true;
    150  }
    151 
    152  // t=1 sec: the script element is inserted to the DOM, i.e.
    153  // #prepare-a-script is triggered (see monving-between-documents-iframe.py).
    154  // In the case of `before-prepare`, the script can be evaluated.
    155  // In other cases, the script evaluation is blocked by a style sheet.
    156  await new Promise(resolve => step_timeout(resolve, 2000));
    157 
    158  // t=2 sec: Move between documents after #prepare-a-script.
    159  if (timing === "after-prepare") {
    160    // At this point, the script hasn't been moved yet, so we'll move it for the
    161    // first time, after #prepare-a-script, but before #execute-the-script-block.
    162    destDocument.body.appendChild(
    163      sourceDocument.querySelector("streaming-element"));
    164  } else if (timing === "move-back") {
    165    // At this point the script has already been moved to the destination block
    166    // before #prepare-a-script, so we'll move it back to the source document
    167    // before #execute-the-script-block.
    168    sourceDocument.body.appendChild(
    169      destDocument.querySelector("streaming-element"));
    170  }
    171  sourceWindow.readyToEvaluate = true;
    172  destWindow.readyToEvaluate = true;
    173 
    174  // t=3 or 5 sec: Blocking stylesheet and external script are loaded,
    175  // and thus script evaulation is unblocked.
    176 
    177  // Note: scripts are expected to be loaded at t=3, because the fetch
    178  // is started by #prepare-a-script at t=1, and the script's delay is
    179  // 2 seconds. However in Chromium, due to preload scanner, the script
    180  // loading might take 4 seconds, because the first request by preload
    181  // scanner of the source Document takes 2 seconds (between t=1 and t=3)
    182  // which blocks the second request by #prepare-a-script that takes
    183  // another 2 seconds (between t=3 and t=5).
    184 
    185  // t=6 sec: After all possible script evaluation points, test whether
    186  // the script/events were evaluated/fired or not.
    187  // As we have concurrent tests, a single global step_timeout() is
    188  // used instead of multiple `t.step_timeout()` etc.,
    189  // to avoid potential race conditions between `t.step_timeout()`s.
    190  return new Promise(resolve => {
    191    step_timeout(() => {
    192      tWindowErrorEvent.done();
    193      tScriptLoadEvent.done();
    194      tScriptErrorEvent.done();
    195 
    196      t.step_func_done(() => {
    197        assert_false(sourceWindow.didExecute,
    198          "The script must not have executed in source window");
    199        assert_false(destWindow.didExecute,
    200          "The script must not have executed in destination window");
    201      })();
    202      resolve();
    203    }, 4000);
    204  });
    205 }
    206 
    207 async_test(t => {
    208  t.step_timeout(() => {
    209      assert_equals(window.didExecute, undefined,
    210        "The script must not have executed in the top-level window");
    211      t.done();
    212    },
    213    4000);
    214 }, "Sanity check around top-level Window");