racing-soft-navigations.html (8764B)
1 <!doctype html> 2 <html> 3 <head> 4 <meta charset="utf-8" /> 5 <title>Two soft navigations racing each other.</title> 6 <script src="/resources/testharness.js"></script> 7 <script src="/resources/testharnessreport.js"></script> 8 <script src="/resources/testdriver.js"></script> 9 <script src="/resources/testdriver-vendor.js"></script> 10 <script></script> 11 </head> 12 <body> 13 <div id="first_interaction">Click here!</div> 14 <div id="second_interaction">Click here!</div> 15 16 <script> 17 const FIRST_URL = "first-url"; 18 const SECOND_URL = "second-url"; 19 20 const button1 = document.getElementById("first_interaction"); 21 const button2 = document.getElementById("second_interaction"); 22 23 async function updateUI() { 24 const greeting = document.createElement("div"); 25 greeting.textContent = "Hello, World."; 26 document.body.appendChild(greeting); 27 } 28 29 function updateUrl(t, url) { 30 t.state.numPushStateCalls++; 31 const actual_url = t.state.urlPrefix + url; 32 history.pushState({}, "", actual_url); 33 } 34 35 async function waitForSoftNavEntry(t, count = 1) { 36 return t.step_wait(() => t.state.softNavEntries.length >= count); 37 } 38 39 async function create_test(urlPrefix, callback) { 40 return promise_test(async (t) => { 41 const currentUrl = location.pathname.replace(/.*\//, ""); 42 assert_equals(currentUrl, "racing-soft-navigations.html"); 43 44 t.state = {}; 45 t.state.urlPrefix = urlPrefix; 46 t.state.numPushStateCalls = 0; 47 t.state.softNavEntries = []; 48 const observer = new PerformanceObserver((list, observer) => { 49 // If we get two soft-navs in one observer callback... 50 // that is a sign that we emitted multiple for a single effect 51 const entries = list.getEntries(); 52 assert_equals(entries.length, 1, "Expecting a single soft navigation"); 53 t.state.softNavEntries.push(entries[0]); 54 }); 55 observer.observe({ type: 'soft-navigation' }); 56 57 // We have multiple test cases with side effects, so add some cleanup. 58 t.add_cleanup(async () => { 59 observer.disconnect(); 60 61 // Go back to the original URL 62 for (let i = 0; i < t.state.numPushStateCalls; i++) { 63 history.back(); 64 await new Promise(resolve => { 65 addEventListener('popstate', resolve, {once: true}); 66 }); 67 } 68 }); 69 70 return callback(t); 71 }, "Racing multiple overlapping interactions and soft navs: " + urlPrefix); 72 } 73 74 async function expectationsMultipleInteractionTest(t, expected = [FIRST_URL, SECOND_URL]) { 75 const count = expected.length; 76 await t.step_wait(() => t.state.softNavEntries.length >= count, `Wait for ${count} soft navigation entries`); 77 78 // Although we await at least `count` (above), we also assert exactly `count` (here) 79 assert_equals(t.state.softNavEntries.length, count, `Expected ${count} soft navigation entries`); 80 81 for (let i = 0; i < count; i++) { 82 const entry = t.state.softNavEntries[i]; 83 const actual_expected_url = t.state.urlPrefix + expected[i]; 84 assert_equals( 85 entry.name.replace(/.*\//, ""), 86 actual_expected_url, 87 "Expect to observe the first URL change.", 88 ); 89 } 90 } 91 92 // The following tests will trigger two interaction back to back, and each 93 // interaction will do a sequence of the following: 94 // - Triggers event listener, which schedules async work 95 // - updates URL 96 // - updates UI 97 // - yield, or timeout of some kind 98 // - Emit a soft nav entry 99 // 100 // Because there are two interactions per test, we manipulate the 101 // sequence of operations in various ways. 102 103 // Baseline, non overlapping interactions. 104 create_test("click1,url1,ui1,sn1,yield,click2,url2,ui2,sn2", async (t) => { 105 button1.addEventListener('click', async () => { 106 updateUrl(t, FIRST_URL); 107 updateUI(); 108 }, { once: true }); 109 110 button2.addEventListener('click', async () => { 111 updateUrl(t, SECOND_URL); 112 updateUI(); 113 }, { once: true }); 114 115 if (test_driver) { 116 test_driver.click(button1); 117 } 118 119 await waitForSoftNavEntry(t); 120 121 if(test_driver) { 122 test_driver.click(button2); 123 } 124 125 await expectationsMultipleInteractionTest(t); 126 }); 127 128 // Both interactions start and yield (simulate network), then finish all 129 // required effects and emit soft nav without overlap. First interaction 130 // wins the "network" race. 131 create_test("click1,yield,click2,yield,url1,ui1,sn1,yield,url2,ui2,sn2", async (t) => { 132 button1.addEventListener('click', async () => { 133 t.step_timeout(() => { 134 updateUrl(t, FIRST_URL); 135 updateUI(); 136 }, 0); 137 }, { once: true }); 138 139 button2.addEventListener('click', async () => { 140 await waitForSoftNavEntry(t); 141 142 updateUrl(t, SECOND_URL); 143 updateUI(); 144 }, { once: true }); 145 146 // Start both soft navigations in rapid succession. 147 if (test_driver) { 148 test_driver.click(button1); 149 test_driver.click(button2); 150 } 151 152 await expectationsMultipleInteractionTest(t); 153 154 }); 155 156 // Both interactions start and yield (simulate network), then finish all 157 // required effects and emit soft nav without overlap. Second interaction 158 // wins the "network" race. 159 create_test("click1,yield,click2,yield,url2,ui2,sn2,yield,url1,ui1,sn1", async (t) => { 160 button1.addEventListener('click', async () => { 161 await waitForSoftNavEntry(t); 162 // In this test, the first interaction sets the second URL 163 updateUrl(t, FIRST_URL); 164 updateUI(); 165 }, { once: true }); 166 167 button2.addEventListener('click', async () => { 168 t.step_timeout(() => { 169 updateUrl(t, SECOND_URL); 170 updateUI(); 171 }, 0); 172 }, { once: true }); 173 174 // Start both soft navigations in rapid succession. 175 if (test_driver) { 176 test_driver.click(button1); 177 test_driver.click(button2); 178 } 179 180 await expectationsMultipleInteractionTest(t, [SECOND_URL, FIRST_URL]); 181 }); 182 183 // Both interactions start, immediately update URL and yield (simulate 184 // navigate interception), then finish all required effects later. 185 // Only the second URL update emits a soft nav entry. 186 create_test("click1,url1,yield,click2,url2,ui1,yield,ui2,sn2", async (t) => { 187 let first_interaction_did_finish_paint = false; 188 let second_interaction_did_run = false; 189 190 button1.addEventListener('click', async () => { 191 updateUrl(t, FIRST_URL); 192 193 await t.step_wait(() => second_interaction_did_run); 194 195 updateUI(); 196 197 await new Promise(r => requestAnimationFrame(r)); 198 await new Promise(r => t.step_timeout(r, 0)); 199 first_interaction_did_finish_paint = true; 200 }, { once: true }); 201 202 button2.addEventListener('click', async () => { 203 updateUrl(t, SECOND_URL); 204 second_interaction_did_run = true; 205 206 await t.step_wait(() => first_interaction_did_finish_paint); 207 208 updateUI(); 209 }, { once: true }); 210 211 // Start both soft navigations in rapid succession. 212 if (test_driver) { 213 test_driver.click(button1); 214 test_driver.click(button2); 215 } 216 217 await expectationsMultipleInteractionTest(t,[SECOND_URL]); 218 }); 219 220 // Both interactions start, immediately update URL and yield (simulate 221 // navigate interception), then finish all required effects later. 222 // Only the second URL update emits a soft nav entry. 223 create_test("click1,url1,yield,click2,url2,yield,ui2,sn2,yield,ui1", async (t) => { 224 button1.addEventListener('click', async () => { 225 updateUrl(t, FIRST_URL); 226 await waitForSoftNavEntry(t); 227 updateUI(); 228 }, { once: true }); 229 230 button2.addEventListener('click', async () => { 231 updateUrl(t, SECOND_URL); 232 233 t.step_timeout(async () => { 234 updateUI(); 235 }, 100); 236 }, { once: true }); 237 238 // Start both soft navigations in rapid succession. 239 if (test_driver) { 240 test_driver.click(button1); 241 test_driver.click(button2); 242 } 243 244 await expectationsMultipleInteractionTest(t,[SECOND_URL]); 245 }); 246 247 </script> 248 </body> 249 </html>