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");