no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / docshell / test / browser / head_browser_onbeforeunload.js
blob6bb334b793043200e950b44315f0a80a70d4da19
1 "use strict";
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",
9   false
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";
21   let count = 0;
22   function observer() {
23     count++;
24   }
26   Services.obs.addObserver(observer, DIALOG_TOPIC);
27   try {
28     return await task();
29   } finally {
30     Services.obs.removeObserver(observer, DIALOG_TOPIC);
31     is(count, expected, "Should see expected number of tab modal prompts");
32   }
35 function promiseAllowUnloadPrompt(browser, allowNavigation) {
36   return PromptTestUtils.handleNextPrompt(
37     browser,
38     { modalType: Services.prompt.MODAL_TYPE_CONTENT, promptType: "confirmEx" },
39     { buttonNumClick: allowNavigation ? 0 : 1 }
40   );
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).
46 const TabPool = {
47   poolSize: 5,
49   pendingCount: 0,
51   readyTabs: [],
53   readyPromise: null,
54   resolveReadyPromise: null,
56   spawnTabs() {
57     while (this.pendingCount + this.readyTabs.length < this.poolSize) {
58       this.pendingCount++;
59       let tab = BrowserTestUtils.addTab(gBrowser, TEST_PAGE);
60       BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(() => {
61         this.readyTabs.push(tab);
62         this.pendingCount--;
64         if (this.resolveReadyPromise) {
65           this.readyPromise = null;
66           this.resolveReadyPromise();
67           this.resolveReadyPromise = null;
68         }
70         this.spawnTabs();
71       });
72     }
73   },
75   getReadyPromise() {
76     if (!this.readyPromise) {
77       this.readyPromise = new Promise(resolve => {
78         this.resolveReadyPromise = resolve;
79       });
80     }
81     return this.readyPromise;
82   },
84   async getTab() {
85     while (!this.readyTabs.length) {
86       this.spawnTabs();
87       await this.getReadyPromise();
88     }
90     let tab = this.readyTabs.shift();
91     this.spawnTabs();
93     gBrowser.selectedTab = tab;
94     return tab;
95   },
97   async cleanup() {
98     this.poolSize = 0;
100     while (this.pendingCount) {
101       await this.getReadyPromise();
102     }
104     while (this.readyTabs.length) {
105       await BrowserTestUtils.removeTab(this.readyTabs.shift());
106     }
107   },
110 const ACTIONS = {
111   NONE: 0,
112   LISTEN_AND_ALLOW: 1,
113   LISTEN_AND_BLOCK: 2,
116 const ACTION_NAMES = new Map(Object.entries(ACTIONS).map(([k, v]) => [v, k]));
118 function* generatePermutations(depth) {
119   if (depth == 0) {
120     yield [];
121     return;
122   }
123   for (let subActions of generatePermutations(depth - 1)) {
124     for (let action of Object.values(ACTIONS)) {
125       yield [action, ...subActions];
126     }
127   }
130 const PERMUTATIONS = Array.from(generatePermutations(4));
132 const FRAMES = [
133   { process: 0 },
134   { process: SpecialPowers.useRemoteSubframes ? 1 : 0 },
135   { process: 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) {
143         if (block) {
144           event.preventDefault();
145         }
146         resolve({ event: "beforeunload" });
147       }
148       content.addEventListener("beforeunload", onbeforeunload, { once: true });
149       content.unlisten = () => {
150         content.removeEventListener("beforeunload", onbeforeunload);
151       };
153       content.addEventListener(
154         "unload",
155         () => {
156           resolve({ event: "unload" });
157         },
158         { once: true }
159       );
160     });
161   });
164 function descendants(bc) {
165   if (bc) {
166     return [bc, ...descendants(bc.children[0])];
167   }
168   return [];
171 async function addListeners(frames, actions, startIdx) {
172   let process = startIdx >= 0 ? FRAMES[startIdx].process : -1;
174   let roundTripPromises = [];
176   let expectNestedEventLoop = false;
177   let numBlockers = 0;
178   let unloadPromises = [];
179   let beforeUnloadPromises = [];
181   for (let [i, frame] of frames.entries()) {
182     let action = actions[i];
183     if (action === ACTIONS.NONE) {
184       continue;
185     }
187     let block = action === ACTIONS.LISTEN_AND_BLOCK;
188     let promise = addListener(frame, block);
189     if (startIdx <= i) {
190       if (block || FRAMES[i].process !== process) {
191         expectNestedEventLoop = true;
192       }
193       beforeUnloadPromises.push(promise);
194       numBlockers += block;
195     } else {
196       unloadPromises.push(promise);
197     }
199     roundTripPromises.push(SpecialPowers.spawn(frame, [], () => {}));
200   }
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);
207   return {
208     expectNestedEventLoop,
209     expectPrompt: !!numBlockers,
210     unloadPromises,
211     beforeUnloadPromises,
212   };
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;
223   let promptPromise;
224   if (expected.expectPrompt) {
225     awaitingPrompt = true;
226     promptPromise = promiseAllowUnloadPrompt(browser, false).then(() => {
227       awaitingPrompt = false;
228     });
229   }
231   let promptCount = expected.expectPrompt ? 1 : 0;
232   await withTabModalPromptCount(promptCount, async () => {
233     await navigate(tab, frames).then(result => {
234       ok(
235         !awaitingPrompt,
236         "Navigation should not complete while we're still expecting a prompt"
237       );
239       is(
240         result.eventLoopSpun,
241         expected.expectNestedEventLoop,
242         "Should have nested event loop?"
243       );
244     });
246     for (let result of await Promise.all(expected.beforeUnloadPromises)) {
247       is(
248         result.event,
249         "beforeunload",
250         "Should have seen beforeunload event before unload"
251       );
252     }
253     await promptPromise;
255     await Promise.all(
256       frames.map(frame =>
257         SpecialPowers.spawn(frame, [], () => {
258           if (content.unlisten) {
259             content.unlisten();
260           }
261         }).catch(() => {})
262       )
263     );
265     await BrowserTestUtils.removeTab(tab);
266   });
268   for (let result of await Promise.all(expected.unloadPromises)) {
269     is(result.event, "unload", "Should have seen unload event");
270   }