diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index dd5bc0be54cc..eab6b6da7ac4 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -9045,6 +9045,10 @@ class TabDialogBox { } return browser; } + + getManager() { + return this._dialogManager; + } } TabDialogBox.prototype.QueryInterface = ChromeUtils.generateQI([ diff --git a/browser/base/content/test/tabdialogs/browser_tabdialogbox_navigation.js b/browser/base/content/test/tabdialogs/browser_tabdialogbox_navigation.js index 4d5186e8263d..52f9ff348fea 100644 --- a/browser/base/content/test/tabdialogs/browser_tabdialogbox_navigation.js +++ b/browser/base/content/test/tabdialogs/browser_tabdialogbox_navigation.js @@ -105,3 +105,52 @@ add_task(async function test_tabdialogbox_close_on_content_nav() { ok(true, "Dialog should close for cross origin navigation by the content."); }); }); + +/** + * Hides a dialog stack and tests that behavior doesn't change. Ensures + * navigation triggered by web content still closes all dialogs. + */ +add_task(async function test_tabdialogbox_hide() { + await BrowserTestUtils.withNewTab("https://example.com", async function( + browser + ) { + // Open a dialog and wait for it to be ready + let dialogBox = gBrowser.getTabDialogBox(browser); + let dialogBoxManager = dialogBox.getManager(); + let closedPromises = [ + dialogBox.open(TEST_DIALOG_PATH), + dialogBox.open(TEST_DIALOG_PATH), + ]; + + let dialogs = dialogBox._dialogManager._dialogs; + + is( + dialogBox._dialogManager._dialogs.length, + 2, + "Dialog manager has two dialogs." + ); + + info("Waiting for dialogs to open."); + await Promise.all(dialogs.map(dialog => dialog._dialogReady)); + + is(dialogBoxManager._dialogStack.hidden, false, "Dialog stack is showing"); + + dialogBoxManager.hideDialog(browser); + + is( + dialogBoxManager._dialogs.length, + 2, + "Dialog manager still has two dialogs." + ); + + is(dialogBoxManager._dialogStack.hidden, true, "Dialog stack is hidden"); + + // Navigate to a different page + BrowserTestUtils.loadURI(browser, "https://example.org"); + + info("Waiting for dialogs to close."); + await closedPromises; + + ok(true, "All open dialogs should still close on navigation"); + }); +}); diff --git a/browser/base/content/test/tabdialogs/browser_tabdialogbox_tab_switch_focus.js b/browser/base/content/test/tabdialogs/browser_tabdialogbox_tab_switch_focus.js index 6bd08e4cc097..e602ac393358 100644 --- a/browser/base/content/test/tabdialogs/browser_tabdialogbox_tab_switch_focus.js +++ b/browser/base/content/test/tabdialogs/browser_tabdialogbox_tab_switch_focus.js @@ -9,7 +9,6 @@ const TEST_DIALOG_PATH = TEST_ROOT_CHROME + "subdialog.xhtml"; /** * Tests that tab dialogs are focused when switching tabs. */ - add_task(async function test_tabdialogbox_tab_switch_focus() { // Open 3 tabs let tabPromises = []; @@ -74,3 +73,56 @@ add_task(async function test_tabdialogbox_tab_switch_focus() { BrowserTestUtils.removeTab(tab); }); }); + +/** + * Tests that other dialogs are still visible if one dialog is hidden. + */ +add_task(async function test_tabdialogbox_tab_switch_hidden() { + // Open 2 tabs + let tabPromises = []; + for (let i = 0; i < 2; i += 1) { + tabPromises.push( + BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://example.com", + true + ) + ); + } + + // Wait for tabs to be ready + let tabs = await Promise.all(tabPromises); + + // Open subdialog in tabs + let dialogs = []; + let dialogBox, dialogBoxManager, browser; + for (let i = 0; i < 2; i += 1) { + dialogBox = gBrowser.getTabDialogBox(tabs[i].linkedBrowser); + browser = tabs[i].linkedBrowser; + dialogBox.open(TEST_DIALOG_PATH); + dialogBoxManager = dialogBox.getManager(); + dialogs.push(dialogBoxManager._topDialog); + } + + // Wait for dialogs to be ready + await Promise.all([dialogs[0]._dialogReady, dialogs[1]._dialogReady]); + + // Hide the top dialog + dialogBoxManager.hideDialog(browser); + + is(dialogBoxManager._dialogStack.hidden, true, "Dialog stack is hidden"); + + // Switch to first tab + await BrowserTestUtils.switchTab(gBrowser, tabs[0]); + + // Check the dialog stack is showing in first tab + dialogBoxManager = gBrowser + .getTabDialogBox(tabs[0].linkedBrowser) + .getManager(); + is(dialogBoxManager._dialogStack.hidden, false, "Dialog stack is showing"); + + // Cleanup + tabs.forEach(tab => { + BrowserTestUtils.removeTab(tab); + }); +}); diff --git a/toolkit/components/printing/content/print.js b/toolkit/components/printing/content/print.js index 7f09fa390fac..04bebe536a20 100644 --- a/toolkit/components/printing/content/print.js +++ b/toolkit/components/printing/content/print.js @@ -198,11 +198,22 @@ var PrintEventHandler = { // This file in only used if pref print.always_print_silent is false, so // no need to check that here. - // Disable elements of form while waiting to initialize - for (let element of document.querySelector("#print").elements) { - element.disabled = true; + if (document.body.getAttribute("rendering")) { + // Disable elements of form while waiting to initialize + for (let element of document.querySelector("#print").elements) { + element.disabled = true; + } + await window._initialized; } - await window._initialized; + + // Hide the dialog box before opening system dialog + // We cannot close the window yet because the browsing context for the + // print preview browser is needed to print the page. + let sourceBrowser = this.getSourceBrowsingContext().top.embedderElement; + let dialogBoxManager = gBrowser + .getTabDialogBox(sourceBrowser) + .getManager(); + dialogBoxManager.hideDialog(sourceBrowser); // Use our settings to prepopulate the system dialog. // The system print dialog won't recognize our internal save-to-pdf @@ -225,7 +236,7 @@ var PrintEventHandler = { "printing.dialog_opened_via_preview_tm", 1 ); - PRINTPROMPTSVC.showPrintDialog(window, settings); + await this._showPrintDialog(PRINTPROMPTSVC, window, settings); } catch (e) { if (e.result == Cr.NS_ERROR_ABORT) { Services.telemetry.scalarAdd( @@ -237,7 +248,7 @@ var PrintEventHandler = { } throw e; } - this.print(settings); + await this.print(settings); }); await this.refreshSettings(selectedPrinter.value); @@ -414,10 +425,7 @@ var PrintEventHandler = { try { this.settings.showPrintProgress = true; let bc = this.previewBrowser.browsingContext; - await bc.top.embedderElement.print( - bc.currentWindowGlobal.outerWindowId, - settings - ); + await this._doPrint(bc, settings); } catch (e) { Cu.reportError(e); } @@ -706,6 +714,26 @@ var PrintEventHandler = { } } }, + + /** + * Prints the window. This method has been abstracted into a helper for + * testing purposes. + */ + _doPrint(aBrowsingContext, aSettings) { + return aBrowsingContext.top.embedderElement.print( + aBrowsingContext.currentWindowGlobal.outerWindowId, + aSettings + ); + }, + + /** + * Shows the system dialog. This method has been abstracted into a helper for + * testing purposes. The showPrintDialog() call blocks until the dialog is + * closed, so we mark it as async to allow us to reject from the test. + */ + async _showPrintDialog(aPrintingPromptService, aWindow, aSettings) { + return aPrintingPromptService.showPrintDialog(aWindow, aSettings); + }, }; var PrintSettingsViewProxy = { diff --git a/toolkit/components/printing/tests/browser.ini b/toolkit/components/printing/tests/browser.ini index 0508a36b52fd..63545afc729b 100644 --- a/toolkit/components/printing/tests/browser.ini +++ b/toolkit/components/printing/tests/browser.ini @@ -34,3 +34,5 @@ support-files = [browser_preview_switch_print_selected.js] skip-if = os == "mac" || (verify && !debug && (os == 'linux')) + +[browser_system_dialog_subdialog_hidden.js] diff --git a/toolkit/components/printing/tests/browser_modal_print.js b/toolkit/components/printing/tests/browser_modal_print.js index 06e6af167828..27b376e76316 100644 --- a/toolkit/components/printing/tests/browser_modal_print.js +++ b/toolkit/components/printing/tests/browser_modal_print.js @@ -12,11 +12,11 @@ function assertExpectedPrintPage(helper) { add_task(async function testModalPrintDialog() { await PrintHelper.withTestPage(async helper => { - helper.assertDialogHidden(); + helper.assertDialogClosed(); await helper.startPrint(); - helper.assertDialogVisible(); + helper.assertDialogOpen(); // Check that we're printing the right page. assertExpectedPrintPage(helper); @@ -26,26 +26,26 @@ add_task(async function testModalPrintDialog() { EventUtils.synthesizeKey("VK_ESCAPE", {}, helper.win); }); - helper.assertDialogHidden(); + helper.assertDialogClosed(); }); }); add_task(async function testPrintMultiple() { await PrintHelper.withTestPage(async helper => { - helper.assertDialogHidden(); + helper.assertDialogClosed(); // First print as usual. await helper.startPrint(); - helper.assertDialogVisible(); + helper.assertDialogOpen(); assertExpectedPrintPage(helper); // Trigger the command a few more times, verify the overlay still exists. await helper.startPrint(); - helper.assertDialogVisible(); + helper.assertDialogOpen(); await helper.startPrint(); - helper.assertDialogVisible(); + helper.assertDialogOpen(); await helper.startPrint(); - helper.assertDialogVisible(); + helper.assertDialogOpen(); // Verify it's still the correct page. assertExpectedPrintPage(helper); @@ -57,9 +57,9 @@ add_task(async function testPrintMultiple() { add_task(async function testCancelButton() { await PrintHelper.withTestPage(async helper => { - helper.assertDialogHidden(); + helper.assertDialogClosed(); await helper.startPrint(); - helper.assertDialogVisible(); + helper.assertDialogOpen(); let cancelButton = helper.doc.querySelector("button[name=cancel]"); ok(cancelButton, "Got the cancel button"); @@ -67,15 +67,15 @@ add_task(async function testCancelButton() { EventUtils.synthesizeMouseAtCenter(cancelButton, {}, helper.win) ); - helper.assertDialogHidden(); + helper.assertDialogClosed(); }); }); add_task(async function testTabOrder() { await PrintHelper.withTestPage(async helper => { - helper.assertDialogHidden(); + helper.assertDialogClosed(); await helper.startPrint(); - helper.assertDialogVisible(); + helper.assertDialogOpen(); const printerPicker = helper.doc.getElementById("printer-picker"); is( @@ -127,6 +127,6 @@ add_task(async function testTabOrder() { EventUtils.synthesizeKey("VK_ESCAPE", {}); }); - helper.assertDialogHidden(); + helper.assertDialogClosed(); }); }); diff --git a/toolkit/components/printing/tests/browser_system_dialog_subdialog_hidden.js b/toolkit/components/printing/tests/browser_system_dialog_subdialog_hidden.js new file mode 100644 index 000000000000..0533ca9d9d76 --- /dev/null +++ b/toolkit/components/printing/tests/browser_system_dialog_subdialog_hidden.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testModalPrintDialog() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + + helper.assertDialogOpen(); + + await helper.setupMockPrint(); + + helper.doc.querySelector("#open-dialog-link").click(); + + helper.assertDialogHidden(); + + await helper.withClosingFn(() => { + helper.resolveShowSystemDialog(); + helper.resolvePrint(); + }); + + helper.assertDialogClosed(); + }); +}); + +add_task(async function testModalPrintDialogCancelled() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + + helper.assertDialogOpen(); + + await helper.setupMockPrint(); + + helper.doc.querySelector("#open-dialog-link").click(); + + helper.assertDialogHidden(); + + await helper.withClosingFn(() => { + helper.rejectShowSystemDialog(); + }); + + helper.assertDialogClosed(); + }); +}); diff --git a/toolkit/components/printing/tests/head.js b/toolkit/components/printing/tests/head.js index 5d11bf058e9e..69667d419970 100644 --- a/toolkit/components/printing/tests/head.js +++ b/toolkit/components/printing/tests/head.js @@ -48,6 +48,12 @@ class PrintHelper { async withClosingFn(closeFn) { let { dialog } = this; await closeFn(); + if (this.dialog) { + await TestUtils.waitForCondition( + () => !this.dialog, + "Wait for dialog to close" + ); + } await dialog._closingPromise; } @@ -55,13 +61,40 @@ class PrintHelper { await this.withClosingFn(() => this.dialog.close()); } - assertDialogHidden() { + assertDialogClosed() { is(this._dialogs.length, 0, "There are no print dialogs"); } - assertDialogVisible() { + assertDialogOpen() { is(this._dialogs.length, 1, "There is one print dialog"); - BrowserTestUtils.is_visible(this.dialog._box, "The dialog is visible"); + ok(BrowserTestUtils.is_visible(this.dialog._box), "The dialog is visible"); + } + + assertDialogHidden() { + is(this._dialogs.length, 1, "There is one print dialog"); + ok(BrowserTestUtils.is_hidden(this.dialog._box), "The dialog is hidden"); + } + + async setupMockPrint() { + if (this.resolveShowSystemDialog) { + throw new Error("Print already mocked"); + } + + // Create some Promises that we can resolve/reject from the test. + let showSystemDialogPromise = new Promise((resolve, reject) => { + this.resolveShowSystemDialog = resolve; + this.rejectShowSystemDialog = () => { + reject(Components.Exception("", Cr.NS_ERROR_ABORT)); + }; + }); + let printPromise = new Promise((resolve, reject) => { + this.resolvePrint = resolve; + this.rejectPrint = reject; + }); + + // Mock PrintEventHandler with our Promises. + this.win.PrintEventHandler._showPrintDialog = () => showSystemDialogPromise; + this.win.PrintEventHandler._doPrint = () => printPromise; } get _tabDialogBox() { @@ -70,6 +103,10 @@ class PrintHelper { ); } + get _tabDialogBoxManager() { + return this._tabDialogBox.getManager(); + } + get _dialogs() { return this._tabDialogBox._dialogManager._dialogs; } diff --git a/toolkit/modules/SubDialog.jsm b/toolkit/modules/SubDialog.jsm index 88250ded4cfc..ca9cc63a693a 100644 --- a/toolkit/modules/SubDialog.jsm +++ b/toolkit/modules/SubDialog.jsm @@ -921,6 +921,15 @@ class SubDialogManager { this._topDialog.close(); } + /** + * Hides the dialog stack for a specific browser. + * @param aBrowser - The browser associated with the tab dialog. + */ + hideDialog(aBrowser) { + aBrowser.removeAttribute("tabDialogShowing"); + this._dialogStack.hidden = true; + } + /** * Abort open dialogs. * @param {function} [filterFn] - Function which should return true for @@ -992,6 +1001,7 @@ class SubDialogManager { this._topDialog._prevActiveElement?.focus(); this._topDialog._overlay.setAttribute("topmost", true); this._topDialog._addDialogEventListeners(false); + this._dialogStack.hidden = false; } else { // We have closed the last dialog, do cleanup. this._topLevelPrevActiveElement.focus();