diff options
Diffstat (limited to 'browser/modules/test/browser/browser_ProcessHangNotifications.js')
-rw-r--r-- | browser/modules/test/browser/browser_ProcessHangNotifications.js | 486 |
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); +}); |