summaryrefslogtreecommitdiffstats
path: root/browser/modules/test/browser/browser_ProcessHangNotifications.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/modules/test/browser/browser_ProcessHangNotifications.js')
-rw-r--r--browser/modules/test/browser/browser_ProcessHangNotifications.js486
1 files changed, 486 insertions, 0 deletions
diff --git a/browser/modules/test/browser/browser_ProcessHangNotifications.js b/browser/modules/test/browser/browser_ProcessHangNotifications.js
new file mode 100644
index 0000000000..df9011767d
--- /dev/null
+++ b/browser/modules/test/browser/browser_ProcessHangNotifications.js
@@ -0,0 +1,486 @@
+/* globals ProcessHangMonitor */
+
+const { WebExtensionPolicy } = Cu.getGlobalForObject(Services);
+
+const { UpdateUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/UpdateUtils.sys.mjs"
+);
+
+function promiseNotificationShown(aWindow, aName) {
+ 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, reject) => {
+ 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() {
+ return "chrome://browser/content/browser.js";
+ },
+};
+
+// on dev edition we add a button for js debugging of hung scripts.
+let buttonCount = AppConstants.MOZ_DEV_EDITION ? 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.closeButton.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);
+});