3 const BASE_URL = "http://mochi.test:8888/browser/docshell/test/browser/";
5 const TEST_PAGE = BASE_URL + "file_onbeforeunload_0.html";
7 const CONTENT_PROMPT_SUBDIALOG = Services.prefs.getBoolPref(
8 "prompts.contentPromptSubDialog",
12 const { PromptTestUtils } = ChromeUtils.importESModule(
13 "resource://testing-common/PromptTestUtils.sys.mjs"
16 async function withTabModalPromptCount(expected, task) {
17 const DIALOG_TOPIC = CONTENT_PROMPT_SUBDIALOG
18 ? "common-dialog-loaded"
19 : "tabmodal-dialog-loaded";
26 Services.obs.addObserver(observer, DIALOG_TOPIC);
30 Services.obs.removeObserver(observer, DIALOG_TOPIC);
31 is(count, expected, "Should see expected number of tab modal prompts");
35 function promiseAllowUnloadPrompt(browser, allowNavigation) {
36 return PromptTestUtils.handleNextPrompt(
38 { modalType: Services.prompt.MODAL_TYPE_CONTENT, promptType: "confirmEx" },
39 { buttonNumClick: allowNavigation ? 0 : 1 }
43 // Maintain a pool of background tabs with our test document loaded so
44 // we don't have to wait for a load prior to each test step (potentially
45 // tearing down and recreating content processes in the process).
54 resolveReadyPromise: null,
57 while (this.pendingCount + this.readyTabs.length < this.poolSize) {
59 let tab = BrowserTestUtils.addTab(gBrowser, TEST_PAGE);
60 BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(() => {
61 this.readyTabs.push(tab);
64 if (this.resolveReadyPromise) {
65 this.readyPromise = null;
66 this.resolveReadyPromise();
67 this.resolveReadyPromise = null;
76 if (!this.readyPromise) {
77 this.readyPromise = new Promise(resolve => {
78 this.resolveReadyPromise = resolve;
81 return this.readyPromise;
85 while (!this.readyTabs.length) {
87 await this.getReadyPromise();
90 let tab = this.readyTabs.shift();
93 gBrowser.selectedTab = tab;
100 while (this.pendingCount) {
101 await this.getReadyPromise();
104 while (this.readyTabs.length) {
105 await BrowserTestUtils.removeTab(this.readyTabs.shift());
116 const ACTION_NAMES = new Map(Object.entries(ACTIONS).map(([k, v]) => [v, k]));
118 function* generatePermutations(depth) {
123 for (let subActions of generatePermutations(depth - 1)) {
124 for (let action of Object.values(ACTIONS)) {
125 yield [action, ...subActions];
130 const PERMUTATIONS = Array.from(generatePermutations(4));
134 { process: SpecialPowers.useRemoteSubframes ? 1 : 0 },
136 { process: SpecialPowers.useRemoteSubframes ? 1 : 0 },
139 function addListener(bc, block) {
140 return SpecialPowers.spawn(bc, [block], block => {
141 return new Promise(resolve => {
142 function onbeforeunload(event) {
144 event.preventDefault();
146 resolve({ event: "beforeunload" });
148 content.addEventListener("beforeunload", onbeforeunload, { once: true });
149 content.unlisten = () => {
150 content.removeEventListener("beforeunload", onbeforeunload);
153 content.addEventListener(
156 resolve({ event: "unload" });
164 function descendants(bc) {
166 return [bc, ...descendants(bc.children[0])];
171 async function addListeners(frames, actions, startIdx) {
172 let process = startIdx >= 0 ? FRAMES[startIdx].process : -1;
174 let roundTripPromises = [];
176 let expectNestedEventLoop = false;
178 let unloadPromises = [];
179 let beforeUnloadPromises = [];
181 for (let [i, frame] of frames.entries()) {
182 let action = actions[i];
183 if (action === ACTIONS.NONE) {
187 let block = action === ACTIONS.LISTEN_AND_BLOCK;
188 let promise = addListener(frame, block);
190 if (block || FRAMES[i].process !== process) {
191 expectNestedEventLoop = true;
193 beforeUnloadPromises.push(promise);
194 numBlockers += block;
196 unloadPromises.push(promise);
199 roundTripPromises.push(SpecialPowers.spawn(frame, [], () => {}));
202 // Wait for round trip messages to any processes with event listeners to
203 // return so we're sure that all listeners are registered and their state
204 // flags are propagated before we continue.
205 await Promise.all(roundTripPromises);
208 expectNestedEventLoop,
209 expectPrompt: !!numBlockers,
211 beforeUnloadPromises,
215 async function doTest(actions, startIdx, navigate) {
216 let tab = await TabPool.getTab();
217 let browser = tab.linkedBrowser;
219 let frames = descendants(browser.browsingContext);
220 let expected = await addListeners(frames, actions, startIdx);
222 let awaitingPrompt = false;
224 if (expected.expectPrompt) {
225 awaitingPrompt = true;
226 promptPromise = promiseAllowUnloadPrompt(browser, false).then(() => {
227 awaitingPrompt = false;
231 let promptCount = expected.expectPrompt ? 1 : 0;
232 await withTabModalPromptCount(promptCount, async () => {
233 await navigate(tab, frames).then(result => {
236 "Navigation should not complete while we're still expecting a prompt"
240 result.eventLoopSpun,
241 expected.expectNestedEventLoop,
242 "Should have nested event loop?"
246 for (let result of await Promise.all(expected.beforeUnloadPromises)) {
250 "Should have seen beforeunload event before unload"
257 SpecialPowers.spawn(frame, [], () => {
258 if (content.unlisten) {
265 await BrowserTestUtils.removeTab(tab);
268 for (let result of await Promise.all(expected.unloadPromises)) {
269 is(result.event, "unload", "Should have seen unload event");