Bug 1660665, hide subdialog if user chooses to print using system dialog. r=jwatt,Gijs,mstriemer

When the user chooses to print using the system dialog, we should hide the print
UI. We are choosing to hide the dialog stack instead of closing the dialog because
the print preview browser still needs to be available if the user tries to print.
We close the window if the user cancels the system dialog or once we receive the
promise from PrintUtils.printWindow.

Differential Revision: https://phabricator.services.mozilla.com/D88096
This commit is contained in:
Emma Malysz
2020-09-17 02:18:50 +00:00
parent 37deb75963
commit 7f15c06b1d
9 changed files with 254 additions and 28 deletions

View File

@@ -9045,6 +9045,10 @@ class TabDialogBox {
} }
return browser; return browser;
} }
getManager() {
return this._dialogManager;
}
} }
TabDialogBox.prototype.QueryInterface = ChromeUtils.generateQI([ TabDialogBox.prototype.QueryInterface = ChromeUtils.generateQI([

View File

@@ -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."); 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");
});
});

View File

@@ -9,7 +9,6 @@ const TEST_DIALOG_PATH = TEST_ROOT_CHROME + "subdialog.xhtml";
/** /**
* Tests that tab dialogs are focused when switching tabs. * Tests that tab dialogs are focused when switching tabs.
*/ */
add_task(async function test_tabdialogbox_tab_switch_focus() { add_task(async function test_tabdialogbox_tab_switch_focus() {
// Open 3 tabs // Open 3 tabs
let tabPromises = []; let tabPromises = [];
@@ -74,3 +73,56 @@ add_task(async function test_tabdialogbox_tab_switch_focus() {
BrowserTestUtils.removeTab(tab); 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);
});
});

View File

@@ -198,11 +198,22 @@ var PrintEventHandler = {
// This file in only used if pref print.always_print_silent is false, so // This file in only used if pref print.always_print_silent is false, so
// no need to check that here. // no need to check that here.
if (document.body.getAttribute("rendering")) {
// Disable elements of form while waiting to initialize // Disable elements of form while waiting to initialize
for (let element of document.querySelector("#print").elements) { for (let element of document.querySelector("#print").elements) {
element.disabled = true; 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. // Use our settings to prepopulate the system dialog.
// The system print dialog won't recognize our internal save-to-pdf // The system print dialog won't recognize our internal save-to-pdf
@@ -225,7 +236,7 @@ var PrintEventHandler = {
"printing.dialog_opened_via_preview_tm", "printing.dialog_opened_via_preview_tm",
1 1
); );
PRINTPROMPTSVC.showPrintDialog(window, settings); await this._showPrintDialog(PRINTPROMPTSVC, window, settings);
} catch (e) { } catch (e) {
if (e.result == Cr.NS_ERROR_ABORT) { if (e.result == Cr.NS_ERROR_ABORT) {
Services.telemetry.scalarAdd( Services.telemetry.scalarAdd(
@@ -237,7 +248,7 @@ var PrintEventHandler = {
} }
throw e; throw e;
} }
this.print(settings); await this.print(settings);
}); });
await this.refreshSettings(selectedPrinter.value); await this.refreshSettings(selectedPrinter.value);
@@ -414,10 +425,7 @@ var PrintEventHandler = {
try { try {
this.settings.showPrintProgress = true; this.settings.showPrintProgress = true;
let bc = this.previewBrowser.browsingContext; let bc = this.previewBrowser.browsingContext;
await bc.top.embedderElement.print( await this._doPrint(bc, settings);
bc.currentWindowGlobal.outerWindowId,
settings
);
} catch (e) { } catch (e) {
Cu.reportError(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 = { var PrintSettingsViewProxy = {

View File

@@ -34,3 +34,5 @@ support-files =
[browser_preview_switch_print_selected.js] [browser_preview_switch_print_selected.js]
skip-if = os == "mac" || (verify && !debug && (os == 'linux')) skip-if = os == "mac" || (verify && !debug && (os == 'linux'))
[browser_system_dialog_subdialog_hidden.js]

View File

@@ -12,11 +12,11 @@ function assertExpectedPrintPage(helper) {
add_task(async function testModalPrintDialog() { add_task(async function testModalPrintDialog() {
await PrintHelper.withTestPage(async helper => { await PrintHelper.withTestPage(async helper => {
helper.assertDialogHidden(); helper.assertDialogClosed();
await helper.startPrint(); await helper.startPrint();
helper.assertDialogVisible(); helper.assertDialogOpen();
// Check that we're printing the right page. // Check that we're printing the right page.
assertExpectedPrintPage(helper); assertExpectedPrintPage(helper);
@@ -26,26 +26,26 @@ add_task(async function testModalPrintDialog() {
EventUtils.synthesizeKey("VK_ESCAPE", {}, helper.win); EventUtils.synthesizeKey("VK_ESCAPE", {}, helper.win);
}); });
helper.assertDialogHidden(); helper.assertDialogClosed();
}); });
}); });
add_task(async function testPrintMultiple() { add_task(async function testPrintMultiple() {
await PrintHelper.withTestPage(async helper => { await PrintHelper.withTestPage(async helper => {
helper.assertDialogHidden(); helper.assertDialogClosed();
// First print as usual. // First print as usual.
await helper.startPrint(); await helper.startPrint();
helper.assertDialogVisible(); helper.assertDialogOpen();
assertExpectedPrintPage(helper); assertExpectedPrintPage(helper);
// Trigger the command a few more times, verify the overlay still exists. // Trigger the command a few more times, verify the overlay still exists.
await helper.startPrint(); await helper.startPrint();
helper.assertDialogVisible(); helper.assertDialogOpen();
await helper.startPrint(); await helper.startPrint();
helper.assertDialogVisible(); helper.assertDialogOpen();
await helper.startPrint(); await helper.startPrint();
helper.assertDialogVisible(); helper.assertDialogOpen();
// Verify it's still the correct page. // Verify it's still the correct page.
assertExpectedPrintPage(helper); assertExpectedPrintPage(helper);
@@ -57,9 +57,9 @@ add_task(async function testPrintMultiple() {
add_task(async function testCancelButton() { add_task(async function testCancelButton() {
await PrintHelper.withTestPage(async helper => { await PrintHelper.withTestPage(async helper => {
helper.assertDialogHidden(); helper.assertDialogClosed();
await helper.startPrint(); await helper.startPrint();
helper.assertDialogVisible(); helper.assertDialogOpen();
let cancelButton = helper.doc.querySelector("button[name=cancel]"); let cancelButton = helper.doc.querySelector("button[name=cancel]");
ok(cancelButton, "Got the cancel button"); ok(cancelButton, "Got the cancel button");
@@ -67,15 +67,15 @@ add_task(async function testCancelButton() {
EventUtils.synthesizeMouseAtCenter(cancelButton, {}, helper.win) EventUtils.synthesizeMouseAtCenter(cancelButton, {}, helper.win)
); );
helper.assertDialogHidden(); helper.assertDialogClosed();
}); });
}); });
add_task(async function testTabOrder() { add_task(async function testTabOrder() {
await PrintHelper.withTestPage(async helper => { await PrintHelper.withTestPage(async helper => {
helper.assertDialogHidden(); helper.assertDialogClosed();
await helper.startPrint(); await helper.startPrint();
helper.assertDialogVisible(); helper.assertDialogOpen();
const printerPicker = helper.doc.getElementById("printer-picker"); const printerPicker = helper.doc.getElementById("printer-picker");
is( is(
@@ -127,6 +127,6 @@ add_task(async function testTabOrder() {
EventUtils.synthesizeKey("VK_ESCAPE", {}); EventUtils.synthesizeKey("VK_ESCAPE", {});
}); });
helper.assertDialogHidden(); helper.assertDialogClosed();
}); });
}); });

View File

@@ -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();
});
});

View File

@@ -48,6 +48,12 @@ class PrintHelper {
async withClosingFn(closeFn) { async withClosingFn(closeFn) {
let { dialog } = this; let { dialog } = this;
await closeFn(); await closeFn();
if (this.dialog) {
await TestUtils.waitForCondition(
() => !this.dialog,
"Wait for dialog to close"
);
}
await dialog._closingPromise; await dialog._closingPromise;
} }
@@ -55,13 +61,40 @@ class PrintHelper {
await this.withClosingFn(() => this.dialog.close()); await this.withClosingFn(() => this.dialog.close());
} }
assertDialogHidden() { assertDialogClosed() {
is(this._dialogs.length, 0, "There are no print dialogs"); is(this._dialogs.length, 0, "There are no print dialogs");
} }
assertDialogVisible() { assertDialogOpen() {
is(this._dialogs.length, 1, "There is one print dialog"); 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() { get _tabDialogBox() {
@@ -70,6 +103,10 @@ class PrintHelper {
); );
} }
get _tabDialogBoxManager() {
return this._tabDialogBox.getManager();
}
get _dialogs() { get _dialogs() {
return this._tabDialogBox._dialogManager._dialogs; return this._tabDialogBox._dialogManager._dialogs;
} }

View File

@@ -921,6 +921,15 @@ class SubDialogManager {
this._topDialog.close(); 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. * Abort open dialogs.
* @param {function} [filterFn] - Function which should return true for * @param {function} [filterFn] - Function which should return true for
@@ -992,6 +1001,7 @@ class SubDialogManager {
this._topDialog._prevActiveElement?.focus(); this._topDialog._prevActiveElement?.focus();
this._topDialog._overlay.setAttribute("topmost", true); this._topDialog._overlay.setAttribute("topmost", true);
this._topDialog._addDialogEventListeners(false); this._topDialog._addDialogEventListeners(false);
this._dialogStack.hidden = false;
} else { } else {
// We have closed the last dialog, do cleanup. // We have closed the last dialog, do cleanup.
this._topLevelPrevActiveElement.focus(); this._topLevelPrevActiveElement.focus();