Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

/* globals ProcessHangMonitor */
const { WebExtensionPolicy } = Cu.getGlobalForObject(Services);
function promiseNotificationShown(aWindow) {
return new Promise(resolve => {
let notificationBox = aWindow.gNotificationBox;
notificationBox.stack.addEventListener(
"AlertActive",
function () {
is(
notificationBox.allNotifications.length,
1,
"Notification Displayed."
);
resolve(notificationBox);
},
{ once: true }
);
});
}
function pushPrefs(...aPrefs) {
return SpecialPowers.pushPrefEnv({ set: aPrefs });
}
function popPrefs() {
return SpecialPowers.popPrefEnv();
}
const TEST_ACTION_UNKNOWN = 0;
const TEST_ACTION_CANCELLED = 1;
const TEST_ACTION_TERMSCRIPT = 2;
const TEST_ACTION_TERMGLOBAL = 3;
const SLOW_SCRIPT = 1;
const ADDON_HANG = 3;
const ADDON_ID = "fake-addon";
/**
* A mock nsIHangReport that we can pass through nsIObserverService
* to trigger notifications.
*
* @param hangType
* One of SLOW_SCRIPT, ADDON_HANG.
* @param browser (optional)
* The <xul:browser> that this hang should be associated with.
* If not supplied, the hang will be associated with every browser,
* but the nsIHangReport.scriptBrowser attribute will return the
* currently selected browser in this window's gBrowser.
*/
let TestHangReport = function (
hangType = SLOW_SCRIPT,
browser = gBrowser.selectedBrowser
) {
this.promise = new Promise(resolve => {
this._resolver = resolve;
});
if (hangType == ADDON_HANG) {
// Add-on hangs need an associated add-on ID for us to blame.
this._addonId = ADDON_ID;
}
this._browser = browser;
};
TestHangReport.prototype = {
get addonId() {
return this._addonId;
},
QueryInterface: ChromeUtils.generateQI(["nsIHangReport"]),
userCanceled() {
this._resolver(TEST_ACTION_CANCELLED);
},
terminateScript() {
this._resolver(TEST_ACTION_TERMSCRIPT);
},
isReportForBrowserOrChildren(aFrameLoader) {
if (this._browser) {
return this._browser.frameLoader === aFrameLoader;
}
return true;
},
get scriptBrowser() {
return this._browser;
},
// Shut up warnings about this property missing:
get scriptFileName() {
},
};
// on dev edition we add a button for js debugging of hung scripts.
let buttonCount =
AppConstants.MOZ_DEV_EDITION || AppConstants.NIGHTLY_BUILD ? 2 : 1;
add_setup(async function () {
// Create a fake WebExtensionPolicy that we can use for
// the add-on hang notification.
const uuidGen = Services.uuid;
const uuid = uuidGen.generateUUID().number.slice(1, -1);
let policy = new WebExtensionPolicy({
name: "Scapegoat",
id: ADDON_ID,
mozExtensionHostname: uuid,
baseURL: "file:///",
allowedOrigins: new MatchPatternSet([]),
localizeCallback() {},
});
policy.active = true;
registerCleanupFunction(() => {
policy.active = false;
});
});
/**
* Test if hang reports receive a terminate script callback when the user selects
* stop in response to a script hang.
*/
add_task(async function terminateScriptTest() {
let promise = promiseNotificationShown(window, "process-hang");
let hangReport = new TestHangReport();
Services.obs.notifyObservers(hangReport, "process-hang-report");
let notification = await promise;
let buttons =
notification.currentNotification.buttonContainer.getElementsByTagName(
"button"
);
is(buttons.length, buttonCount, "proper number of buttons");
// Click the "Stop" button, we should get a terminate script callback
buttons[0].click();
let action = await hangReport.promise;
is(
action,
TEST_ACTION_TERMSCRIPT,
"Clicking 'Stop' should have terminated the script."
);
});
/**
* Test if hang reports receive user canceled callbacks after a user selects wait
* and the browser frees up from a script hang on its own.
*/
add_task(async function waitForScriptTest() {
let hangReport = new TestHangReport();
let promise = promiseNotificationShown(window, "process-hang");
Services.obs.notifyObservers(hangReport, "process-hang-report");
let notification = await promise;
let buttons =
notification.currentNotification.buttonContainer.getElementsByTagName(
"button"
);
is(buttons.length, buttonCount, "proper number of buttons");
await pushPrefs(["browser.hangNotification.waitPeriod", 1000]);
let ignoringReport = true;
hangReport.promise.then(action => {
if (ignoringReport) {
ok(
false,
"Hang report was somehow dealt with when it " +
"should have been ignored."
);
} else {
is(
action,
TEST_ACTION_CANCELLED,
"Hang report should have been cancelled."
);
}
});
// Click the "Close" button this time, we shouldn't get a callback at all.
notification.currentNotification.closeButtonEl.click();
// send another hang pulse, we should not get a notification here
Services.obs.notifyObservers(hangReport, "process-hang-report");
is(
notification.currentNotification,
null,
"no notification should be visible"
);
// Make sure that any queued Promises have run to give our report-ignoring
// then() a chance to fire.
await Promise.resolve();
ignoringReport = false;
Services.obs.notifyObservers(hangReport, "clear-hang-report");
await popPrefs();
});
/**
* Test if hang reports receive user canceled callbacks after the content
* process stops sending hang notifications.
*/
add_task(async function hangGoesAwayTest() {
await pushPrefs(["browser.hangNotification.expiration", 1000]);
let hangReport = new TestHangReport();
let promise = promiseNotificationShown(window, "process-hang");
Services.obs.notifyObservers(hangReport, "process-hang-report");
await promise;
Services.obs.notifyObservers(hangReport, "clear-hang-report");
let action = await hangReport.promise;
is(action, TEST_ACTION_CANCELLED, "Hang report should have been cancelled.");
await popPrefs();
});
/**
* Tests that if we're shutting down, any pre-existing hang reports will
* be terminated appropriately.
*/
add_task(async function terminateAtShutdown() {
let pausedHang = new TestHangReport(SLOW_SCRIPT);
Services.obs.notifyObservers(pausedHang, "process-hang-report");
ProcessHangMonitor.waitLonger(window);
ok(
ProcessHangMonitor.findPausedReport(gBrowser.selectedBrowser),
"There should be a paused report for the selected browser."
);
let scriptHang = new TestHangReport(SLOW_SCRIPT);
let addonHang = new TestHangReport(ADDON_HANG);
[scriptHang, addonHang].forEach(hangReport => {
Services.obs.notifyObservers(hangReport, "process-hang-report");
});
// Simulate the browser being told to shutdown. This should cause
// hangs to terminate scripts.
ProcessHangMonitor.onQuitApplicationGranted();
// In case this test happens to throw before it can finish, make
// sure to reset the shutting-down state.
registerCleanupFunction(() => {
ProcessHangMonitor._shuttingDown = false;
});
let pausedAction = await pausedHang.promise;
let scriptAction = await scriptHang.promise;
let addonAction = await addonHang.promise;
is(
pausedAction,
TEST_ACTION_TERMSCRIPT,
"On shutdown, should have terminated script for paused script hang."
);
is(
scriptAction,
TEST_ACTION_TERMSCRIPT,
"On shutdown, should have terminated script for script hang."
);
is(
addonAction,
TEST_ACTION_TERMSCRIPT,
"On shutdown, should have terminated script for add-on hang."
);
// ProcessHangMonitor should now be in the "shutting down" state,
// meaning that any further hangs should be handled immediately
// without user interaction.
let scriptHang2 = new TestHangReport(SLOW_SCRIPT);
let addonHang2 = new TestHangReport(ADDON_HANG);
[scriptHang2, addonHang2].forEach(hangReport => {
Services.obs.notifyObservers(hangReport, "process-hang-report");
});
let scriptAction2 = await scriptHang.promise;
let addonAction2 = await addonHang.promise;
is(
scriptAction2,
TEST_ACTION_TERMSCRIPT,
"On shutdown, should have terminated script for script hang."
);
is(
addonAction2,
TEST_ACTION_TERMSCRIPT,
"On shutdown, should have terminated script for add-on hang."
);
ProcessHangMonitor._shuttingDown = false;
});
/**
* Test that if there happens to be no open browser windows, that any
* hang reports that exist or appear while in this state will be handled
* automatically.
*/
add_task(async function terminateNoWindows() {
let testWin = await BrowserTestUtils.openNewBrowserWindow();
let pausedHang = new TestHangReport(
SLOW_SCRIPT,
testWin.gBrowser.selectedBrowser
);
Services.obs.notifyObservers(pausedHang, "process-hang-report");
ProcessHangMonitor.waitLonger(testWin);
ok(
ProcessHangMonitor.findPausedReport(testWin.gBrowser.selectedBrowser),
"There should be a paused report for the selected browser."
);
let scriptHang = new TestHangReport(SLOW_SCRIPT);
let addonHang = new TestHangReport(ADDON_HANG);
[scriptHang, addonHang].forEach(hangReport => {
Services.obs.notifyObservers(hangReport, "process-hang-report");
});
// Quick and dirty hack to trick the window mediator into thinking there
// are no browser windows without actually closing all browser windows.
document.documentElement.setAttribute(
"windowtype",
"navigator:browsertestdummy"
);
// In case this test happens to throw before it can finish, make
// sure to reset this.
registerCleanupFunction(() => {
document.documentElement.setAttribute("windowtype", "navigator:browser");
});
await BrowserTestUtils.closeWindow(testWin);
let pausedAction = await pausedHang.promise;
let scriptAction = await scriptHang.promise;
let addonAction = await addonHang.promise;
is(
pausedAction,
TEST_ACTION_TERMSCRIPT,
"With no open windows, should have terminated script for paused script hang."
);
is(
scriptAction,
TEST_ACTION_TERMSCRIPT,
"With no open windows, should have terminated script for script hang."
);
is(
addonAction,
TEST_ACTION_TERMSCRIPT,
"With no open windows, should have terminated script for add-on hang."
);
// ProcessHangMonitor should notice we're in the "no windows" state,
// so any further hangs should be handled immediately without user
// interaction.
let scriptHang2 = new TestHangReport(SLOW_SCRIPT);
let addonHang2 = new TestHangReport(ADDON_HANG);
[scriptHang2, addonHang2].forEach(hangReport => {
Services.obs.notifyObservers(hangReport, "process-hang-report");
});
let scriptAction2 = await scriptHang.promise;
let addonAction2 = await addonHang.promise;
is(
scriptAction2,
TEST_ACTION_TERMSCRIPT,
"With no open windows, should have terminated script for script hang."
);
is(
addonAction2,
TEST_ACTION_TERMSCRIPT,
"With no open windows, should have terminated script for add-on hang."
);
document.documentElement.setAttribute("windowtype", "navigator:browser");
});
/**
* Test that if a script hang occurs in one browser window, and that
* browser window goes away, that we clear the hang. For plug-in hangs,
* we do the conservative thing and terminate any plug-in hangs when a
* window closes, even though we don't exactly know which window it
* belongs to.
*/
add_task(async function terminateClosedWindow() {
let testWin = await BrowserTestUtils.openNewBrowserWindow();
let testBrowser = testWin.gBrowser.selectedBrowser;
let pausedHang = new TestHangReport(SLOW_SCRIPT, testBrowser);
Services.obs.notifyObservers(pausedHang, "process-hang-report");
ProcessHangMonitor.waitLonger(testWin);
ok(
ProcessHangMonitor.findPausedReport(testWin.gBrowser.selectedBrowser),
"There should be a paused report for the selected browser."
);
let scriptHang = new TestHangReport(SLOW_SCRIPT, testBrowser);
let addonHang = new TestHangReport(ADDON_HANG, testBrowser);
[scriptHang, addonHang].forEach(hangReport => {
Services.obs.notifyObservers(hangReport, "process-hang-report");
});
await BrowserTestUtils.closeWindow(testWin);
let pausedAction = await pausedHang.promise;
let scriptAction = await scriptHang.promise;
let addonAction = await addonHang.promise;
is(
pausedAction,
TEST_ACTION_TERMSCRIPT,
"When closing window, should have terminated script for a paused script hang."
);
is(
scriptAction,
TEST_ACTION_TERMSCRIPT,
"When closing window, should have terminated script for script hang."
);
is(
addonAction,
TEST_ACTION_TERMSCRIPT,
"When closing window, should have terminated script for add-on hang."
);
});
/**
* Test that permitUnload (used for closing or discarding tabs) does not
* try to talk to the hung child
*/
add_task(async function permitUnload() {
let testWin = await BrowserTestUtils.openNewBrowserWindow();
let testTab = testWin.gBrowser.selectedTab;
// Ensure we don't close the window:
BrowserTestUtils.addTab(testWin.gBrowser, "about:blank");
// Set up the test tab and another tab so we can check what happens when
// they are closed:
let otherTab = BrowserTestUtils.addTab(testWin.gBrowser, "about:blank");
let permitUnloadCount = 0;
for (let tab of [testTab, otherTab]) {
let browser = tab.linkedBrowser;
// Fake before unload state:
Object.defineProperty(browser, "hasBeforeUnload", { value: true });
// Increment permitUnloadCount if we ask for unload permission:
browser.asyncPermitUnload = () => {
permitUnloadCount++;
return Promise.resolve({ permitUnload: true });
};
}
// Set up a hang for the selected tab:
let testBrowser = testTab.linkedBrowser;
let pausedHang = new TestHangReport(SLOW_SCRIPT, testBrowser);
Services.obs.notifyObservers(pausedHang, "process-hang-report");
ProcessHangMonitor.waitLonger(testWin);
ok(
ProcessHangMonitor.findPausedReport(testWin.gBrowser.selectedBrowser),
"There should be a paused report for the browser we're about to remove."
);
BrowserTestUtils.removeTab(otherTab);
BrowserTestUtils.removeTab(testWin.gBrowser.getTabForBrowser(testBrowser));
is(
permitUnloadCount,
1,
"Should have called asyncPermitUnload once (not for the hung tab)."
);
await BrowserTestUtils.closeWindow(testWin);
});