Source code

Revision control

Copy as Markdown

Other Tools

"use strict";
const TEST_PAGE = BASE_URL + "file_onbeforeunload_0.html";
const { PromptTestUtils } = ChromeUtils.importESModule(
);
async function withTabModalPromptCount(expected, task) {
const DIALOG_TOPIC = "common-dialog-loaded";
let count = 0;
function observer() {
count++;
}
Services.obs.addObserver(observer, DIALOG_TOPIC);
try {
return await task();
} finally {
Services.obs.removeObserver(observer, DIALOG_TOPIC);
is(count, expected, "Should see expected number of tab modal prompts");
}
}
function promiseAllowUnloadPrompt(browser, allowNavigation) {
return PromptTestUtils.handleNextPrompt(
browser,
{ modalType: Services.prompt.MODAL_TYPE_CONTENT, promptType: "confirmEx" },
{ buttonNumClick: allowNavigation ? 0 : 1 }
);
}
// Maintain a pool of background tabs with our test document loaded so
// we don't have to wait for a load prior to each test step (potentially
// tearing down and recreating content processes in the process).
const TabPool = {
poolSize: 5,
pendingCount: 0,
readyTabs: [],
readyPromise: null,
resolveReadyPromise: null,
spawnTabs() {
while (this.pendingCount + this.readyTabs.length < this.poolSize) {
this.pendingCount++;
let tab = BrowserTestUtils.addTab(gBrowser, TEST_PAGE);
BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(() => {
this.readyTabs.push(tab);
this.pendingCount--;
if (this.resolveReadyPromise) {
this.readyPromise = null;
this.resolveReadyPromise();
this.resolveReadyPromise = null;
}
this.spawnTabs();
});
}
},
getReadyPromise() {
if (!this.readyPromise) {
this.readyPromise = new Promise(resolve => {
this.resolveReadyPromise = resolve;
});
}
return this.readyPromise;
},
async getTab() {
while (!this.readyTabs.length) {
this.spawnTabs();
await this.getReadyPromise();
}
let tab = this.readyTabs.shift();
this.spawnTabs();
gBrowser.selectedTab = tab;
return tab;
},
async cleanup() {
this.poolSize = 0;
while (this.pendingCount) {
await this.getReadyPromise();
}
while (this.readyTabs.length) {
await BrowserTestUtils.removeTab(this.readyTabs.shift());
}
},
};
const ACTIONS = {
NONE: 0,
LISTEN_AND_ALLOW: 1,
LISTEN_AND_BLOCK: 2,
};
const ACTION_NAMES = new Map(Object.entries(ACTIONS).map(([k, v]) => [v, k]));
function* generatePermutations(depth) {
if (depth == 0) {
yield [];
return;
}
for (let subActions of generatePermutations(depth - 1)) {
for (let action of Object.values(ACTIONS)) {
yield [action, ...subActions];
}
}
}
const PERMUTATIONS = Array.from(generatePermutations(4));
const FRAMES = [
{ process: 0 },
{ process: SpecialPowers.useRemoteSubframes ? 1 : 0 },
{ process: 0 },
{ process: SpecialPowers.useRemoteSubframes ? 1 : 0 },
];
function addListener(bc, block) {
return SpecialPowers.spawn(bc, [block], block => {
return new Promise(resolve => {
function onbeforeunload(event) {
if (block) {
event.preventDefault();
}
resolve({ event: "beforeunload" });
}
content.addEventListener("beforeunload", onbeforeunload, { once: true });
content.unlisten = () => {
content.removeEventListener("beforeunload", onbeforeunload);
};
content.addEventListener(
"unload",
() => {
resolve({ event: "unload" });
},
{ once: true }
);
});
});
}
function descendants(bc) {
if (bc) {
return [bc, ...descendants(bc.children[0])];
}
return [];
}
async function addListeners(frames, actions, startIdx) {
let process = startIdx >= 0 ? FRAMES[startIdx].process : -1;
let roundTripPromises = [];
let expectNestedEventLoop = false;
let numBlockers = 0;
let unloadPromises = [];
let beforeUnloadPromises = [];
for (let [i, frame] of frames.entries()) {
let action = actions[i];
if (action === ACTIONS.NONE) {
continue;
}
let block = action === ACTIONS.LISTEN_AND_BLOCK;
let promise = addListener(frame, block);
if (startIdx <= i) {
if (block || FRAMES[i].process !== process) {
expectNestedEventLoop = true;
}
beforeUnloadPromises.push(promise);
numBlockers += block;
} else {
unloadPromises.push(promise);
}
roundTripPromises.push(SpecialPowers.spawn(frame, [], () => {}));
}
// Wait for round trip messages to any processes with event listeners to
// return so we're sure that all listeners are registered and their state
// flags are propagated before we continue.
await Promise.all(roundTripPromises);
return {
expectNestedEventLoop,
expectPrompt: !!numBlockers,
unloadPromises,
beforeUnloadPromises,
};
}
async function doTest(actions, startIdx, navigate) {
let tab = await TabPool.getTab();
let browser = tab.linkedBrowser;
let frames = descendants(browser.browsingContext);
let expected = await addListeners(frames, actions, startIdx);
let awaitingPrompt = false;
let promptPromise;
if (expected.expectPrompt) {
awaitingPrompt = true;
promptPromise = promiseAllowUnloadPrompt(browser, false).then(() => {
awaitingPrompt = false;
});
}
let promptCount = expected.expectPrompt ? 1 : 0;
await withTabModalPromptCount(promptCount, async () => {
await navigate(tab, frames).then(result => {
ok(
!awaitingPrompt,
"Navigation should not complete while we're still expecting a prompt"
);
is(
result.eventLoopSpun,
expected.expectNestedEventLoop,
"Should have nested event loop?"
);
});
for (let result of await Promise.all(expected.beforeUnloadPromises)) {
is(
result.event,
"beforeunload",
"Should have seen beforeunload event before unload"
);
}
await promptPromise;
await Promise.all(
frames.map(frame =>
SpecialPowers.spawn(frame, [], () => {
if (content.unlisten) {
content.unlisten();
}
}).catch(() => {})
)
);
await BrowserTestUtils.removeTab(tab);
});
for (let result of await Promise.all(expected.unloadPromises)) {
is(result.event, "unload", "Should have seen unload event");
}
}