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>