head_browser_onbeforeunload.js (6552B)
1 "use strict"; 2 3 const BASE_URL = "http://mochi.test:8888/browser/docshell/test/browser/"; 4 5 const TEST_PAGE = BASE_URL + "file_onbeforeunload_0.html"; 6 7 const { PromptTestUtils } = ChromeUtils.importESModule( 8 "resource://testing-common/PromptTestUtils.sys.mjs" 9 ); 10 11 async function withTabModalPromptCount(expected, task) { 12 const DIALOG_TOPIC = "common-dialog-loaded"; 13 14 let count = 0; 15 function observer() { 16 count++; 17 } 18 19 Services.obs.addObserver(observer, DIALOG_TOPIC); 20 try { 21 return await task(); 22 } finally { 23 Services.obs.removeObserver(observer, DIALOG_TOPIC); 24 is(count, expected, "Should see expected number of tab modal prompts"); 25 } 26 } 27 28 function promiseAllowUnloadPrompt(browser, allowNavigation) { 29 return PromptTestUtils.handleNextPrompt( 30 browser, 31 { modalType: Services.prompt.MODAL_TYPE_CONTENT, promptType: "confirmEx" }, 32 { buttonNumClick: allowNavigation ? 0 : 1 } 33 ); 34 } 35 36 // Maintain a pool of background tabs with our test document loaded so 37 // we don't have to wait for a load prior to each test step (potentially 38 // tearing down and recreating content processes in the process). 39 const TabPool = { 40 poolSize: 5, 41 42 pendingCount: 0, 43 44 readyTabs: [], 45 46 readyPromise: null, 47 resolveReadyPromise: null, 48 49 spawnTabs() { 50 while (this.pendingCount + this.readyTabs.length < this.poolSize) { 51 this.pendingCount++; 52 let tab = BrowserTestUtils.addTab(gBrowser, TEST_PAGE); 53 BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(() => { 54 this.readyTabs.push(tab); 55 this.pendingCount--; 56 57 if (this.resolveReadyPromise) { 58 this.readyPromise = null; 59 this.resolveReadyPromise(); 60 this.resolveReadyPromise = null; 61 } 62 63 this.spawnTabs(); 64 }); 65 } 66 }, 67 68 getReadyPromise() { 69 if (!this.readyPromise) { 70 this.readyPromise = new Promise(resolve => { 71 this.resolveReadyPromise = resolve; 72 }); 73 } 74 return this.readyPromise; 75 }, 76 77 async getTab() { 78 while (!this.readyTabs.length) { 79 this.spawnTabs(); 80 await this.getReadyPromise(); 81 } 82 83 let tab = this.readyTabs.shift(); 84 this.spawnTabs(); 85 86 gBrowser.selectedTab = tab; 87 return tab; 88 }, 89 90 async cleanup() { 91 this.poolSize = 0; 92 93 while (this.pendingCount) { 94 await this.getReadyPromise(); 95 } 96 97 while (this.readyTabs.length) { 98 await BrowserTestUtils.removeTab(this.readyTabs.shift()); 99 } 100 }, 101 }; 102 103 const ACTIONS = { 104 NONE: 0, 105 LISTEN_AND_ALLOW: 1, 106 LISTEN_AND_BLOCK: 2, 107 }; 108 109 const ACTION_NAMES = new Map(Object.entries(ACTIONS).map(([k, v]) => [v, k])); 110 111 function* generatePermutations(depth) { 112 if (depth == 0) { 113 yield []; 114 return; 115 } 116 for (let subActions of generatePermutations(depth - 1)) { 117 for (let action of Object.values(ACTIONS)) { 118 yield [action, ...subActions]; 119 } 120 } 121 } 122 123 const PERMUTATIONS = Array.from(generatePermutations(4)); 124 125 const FRAMES = [ 126 { process: 0 }, 127 { process: SpecialPowers.useRemoteSubframes ? 1 : 0 }, 128 { process: 0 }, 129 { process: SpecialPowers.useRemoteSubframes ? 1 : 0 }, 130 ]; 131 132 function addListener(bc, block) { 133 return SpecialPowers.spawn(bc, [block], block => { 134 return new Promise(resolve => { 135 function onbeforeunload(event) { 136 if (block) { 137 event.preventDefault(); 138 } 139 resolve({ event: "beforeunload" }); 140 } 141 content.addEventListener("beforeunload", onbeforeunload, { once: true }); 142 content.unlisten = () => { 143 content.removeEventListener("beforeunload", onbeforeunload); 144 }; 145 146 content.addEventListener( 147 "unload", 148 () => { 149 resolve({ event: "unload" }); 150 }, 151 { once: true } 152 ); 153 }); 154 }); 155 } 156 157 function descendants(bc) { 158 if (bc) { 159 return [bc, ...descendants(bc.children[0])]; 160 } 161 return []; 162 } 163 164 async function addListeners(frames, actions, startIdx) { 165 let process = startIdx >= 0 ? FRAMES[startIdx].process : -1; 166 167 let roundTripPromises = []; 168 169 let expectNestedEventLoop = false; 170 let numBlockers = 0; 171 let unloadPromises = []; 172 let beforeUnloadPromises = []; 173 174 for (let [i, frame] of frames.entries()) { 175 let action = actions[i]; 176 if (action === ACTIONS.NONE) { 177 continue; 178 } 179 180 let block = action === ACTIONS.LISTEN_AND_BLOCK; 181 let promise = addListener(frame, block); 182 if (startIdx <= i) { 183 if (block || FRAMES[i].process !== process) { 184 expectNestedEventLoop = true; 185 } 186 beforeUnloadPromises.push(promise); 187 numBlockers += block; 188 } else { 189 unloadPromises.push(promise); 190 } 191 192 roundTripPromises.push(SpecialPowers.spawn(frame, [], () => {})); 193 } 194 195 // Wait for round trip messages to any processes with event listeners to 196 // return so we're sure that all listeners are registered and their state 197 // flags are propagated before we continue. 198 await Promise.all(roundTripPromises); 199 200 return { 201 expectNestedEventLoop, 202 expectPrompt: !!numBlockers, 203 unloadPromises, 204 beforeUnloadPromises, 205 }; 206 } 207 208 async function doTest(actions, startIdx, navigate) { 209 let tab = await TabPool.getTab(); 210 let browser = tab.linkedBrowser; 211 212 let frames = descendants(browser.browsingContext); 213 let expected = await addListeners(frames, actions, startIdx); 214 215 let awaitingPrompt = false; 216 let promptPromise; 217 if (expected.expectPrompt) { 218 awaitingPrompt = true; 219 promptPromise = promiseAllowUnloadPrompt(browser, false).then(() => { 220 awaitingPrompt = false; 221 }); 222 } 223 224 let promptCount = expected.expectPrompt ? 1 : 0; 225 await withTabModalPromptCount(promptCount, async () => { 226 await navigate(tab, frames).then(result => { 227 ok( 228 !awaitingPrompt, 229 "Navigation should not complete while we're still expecting a prompt" 230 ); 231 232 is( 233 result.eventLoopSpun, 234 expected.expectNestedEventLoop, 235 "Should have nested event loop?" 236 ); 237 }); 238 239 for (let result of await Promise.all(expected.beforeUnloadPromises)) { 240 is( 241 result.event, 242 "beforeunload", 243 "Should have seen beforeunload event before unload" 244 ); 245 } 246 await promptPromise; 247 248 await Promise.all( 249 frames.map(frame => 250 SpecialPowers.spawn(frame, [], () => { 251 if (content.unlisten) { 252 content.unlisten(); 253 } 254 }).catch(() => {}) 255 ) 256 ); 257 258 await BrowserTestUtils.removeTab(tab); 259 }); 260 261 for (let result of await Promise.all(expected.unloadPromises)) { 262 is(result.event, "unload", "Should have seen unload event"); 263 } 264 }