diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /toolkit/components/printing | |
parent | Initial commit. (diff) | |
download | firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
80 files changed, 12791 insertions, 0 deletions
diff --git a/toolkit/components/printing/content/print.css b/toolkit/components/printing/content/print.css new file mode 100644 index 0000000000..2788312576 --- /dev/null +++ b/toolkit/components/printing/content/print.css @@ -0,0 +1,299 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +html, body { + height: 100vh; +} + +body { + display: flex; + flex-direction: column; + justify-content: flex-start; + overflow: hidden; +} + +body[loading] #print { + visibility: hidden; +} + +*[hidden] { + display: none !important; +} + +.section-block { + margin: 16px; +} + +.section-block .block-label { + display: block; + margin-bottom: 8px; +} + +.row { + display: flex; + flex-direction: column; + width: 100%; + min-height: 1.8em; + margin-block: 2px; +} + +.row.cols-2 { + flex-direction: row; + align-items: center; +} + +#scale .row.cols-2, +#more-settings-options > .row.cols-2 { + /* The margin-block of the checkboxes/radiobuttons + already provide the needed vertical spacing. */ + margin-block: 0; +} + +hr { + margin-inline: 8px; + margin-block: 0; +} + +.header-container { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + flex: 0 1 auto; + padding: 8px 18px; +} +.header-container > h2 { + margin: 0; + font-size: 17px; + line-height: 1; +} + +#sheet-count { + font-size: 11px; + padding: 3px 8px; + margin: 0; +} + +#sheet-count[loading], +body[invalid] #sheet-count { + visibility: hidden; +} + +form#print { + display: flex; + flex: 1 1 auto; + flex-direction: column; + justify-content: flex-start; + overflow: hidden; + font: menu; +} + +select { + margin: 0; +} + +select:not([size], [multiple])[iconic] { + padding-inline-start: calc(8px + 16px + 4px); /* spacing before image + image width + spacing after image */ + background-size: auto 12px, 16px; + background-position: right 19px center, left 8px center; +} + +select:not([size], [multiple])[iconic]:dir(rtl) { + background-position-x: left 19px, right 8px; +} + +#printer-picker { + background-image: url("chrome://global/skin/icons/arrow-down-12.svg"), url("chrome://global/skin/icons/print.svg"); + width: 100%; +} + +#printer-picker[output="pdf"] { + background-image: url("chrome://global/skin/icons/arrow-down-12.svg"), url("chrome://global/skin/icons/page-portrait.svg"); +} + +input[type="number"], +input[type="text"] { + padding: 2px; + padding-inline-start: 8px; +} + +.toggle-group-label { + padding: 4px 8px; +} + +.body-container { + flex: 1 1 auto; + overflow: auto; +} + +.twisty > summary { + list-style: none; + display: flex; + cursor: pointer; + align-items: center; +} + +#more-settings > summary > .twisty { + background-image: url("chrome://global/skin/icons/arrow-down-12.svg"); + background-repeat: no-repeat; + background-position: center; + -moz-context-properties: fill; + fill: currentColor; + width: 12px; + height: 12px; +} + +#more-settings > summary > .label { + flex-grow: 1; +} + +#more-settings[open] > summary > .twisty { + background-image: url("chrome://global/skin/icons/arrow-up-12.svg"); +} + +#open-dialog-link { + display: flex; + justify-content: space-between; + align-items: center; +} + +#open-dialog-link::after { + display: block; + flex-shrink: 0; + content: ""; + background: url(chrome://global/skin/icons/open-in-new.svg) no-repeat center; + background-size: 12px; + -moz-context-properties: fill; + fill: currentColor; + width: 12px; + height: 12px; +} + +#open-dialog-link:dir(rtl)::after { + scale: -1 1; +} + +.footer-container { + flex: 0 1 auto; +} + +#print-progress { + background-image: url("chrome://global/skin/icons/loading.png"); + background-repeat: no-repeat; + background-size: 16px; + background-position: left center; + padding-inline-start: 20px; +} + +@media (min-resolution: 1.1dppx) { + #print-progress { + background-image: url("chrome://global/skin/icons/loading@2x.png"); + } +} + +#print-progress:dir(rtl) { + background-position-x: right; +} + +#custom-range { + margin-top: 4px; + margin-inline: 0; +} + +.vertical-margins, +.horizontal-margins { + display: flex; + gap: 20px; + margin-block: 5px; +} + +.margin-pair { + width: calc(50% - 10px); /* Half minus the gap */ +} + +.margin-input { + /* FIXME: Important is needed to override the photon-number styles below, + * probably should be refactored */ + width: 100% !important; + box-sizing: border-box; +} + +.margin-descriptor { + text-align: center; + display: block; + margin-top: 2px; +} + +.toggle-group #landscape + .toggle-group-label::before { + content: url("chrome://global/skin/icons/page-landscape.svg"); +} +.toggle-group #portrait + .toggle-group-label::before { + content: url("chrome://global/skin/icons/page-portrait.svg"); +} + +.error-message { + font-size: 12px; + color: var(--in-content-error-text-color, color-mix(in srgb, currentColor 40%, #C50042)); + margin-top: 4px; +} + +#percent-scale { + margin-inline-start: 4px; +} + +#percent-scale:disabled { + /* Let clicks on the disabled input select the radio button */ + pointer-events: none; +} + +input[type="number"].photon-number { + padding: 0; + padding-inline-start: 8px; + margin: 0; + height: 1.5em; + width: 4em; +} + +input[type="number"].photon-number::-moz-number-spin-box { + height: 100%; + max-height: 100%; + border-inline-start: 1px solid var(--in-content-box-border-color); + width: 1em; + border-start-end-radius: 4px; + border-end-end-radius: 4px; +} + +input[type="number"].photon-number:hover::-moz-number-spin-box { + border-color: var(--in-content-border-hover); +} + +input[type="number"].photon-number::-moz-number-spin-up, +input[type="number"].photon-number::-moz-number-spin-down { + border: 0; + border-radius: 0; + background-color: var(--in-content-button-background); + background-image: url("chrome://global/skin/icons/arrow-down.svg"); + background-repeat: no-repeat; + background-size: 8px; + background-position: center; + -moz-context-properties: fill; + fill: currentColor; + appearance: none; +} + +input[type="number"].photon-number::-moz-number-spin-up { + background-image: url("chrome://global/skin/icons/arrow-up.svg"); +} + +input[type="number"].photon-number::-moz-number-spin-up:hover, +input[type="number"].photon-number::-moz-number-spin-down:hover { + background-color: var(--in-content-button-background-hover); + color: var(--in-content-button-text-color-hover); +} + +input[type="checkbox"]:disabled + label, +input[type="radio"]:disabled + label, +input[type="radio"]:disabled + span > label { + opacity: 0.5; +} diff --git a/toolkit/components/printing/content/print.html b/toolkit/components/printing/content/print.html new file mode 100644 index 0000000000..62d214608c --- /dev/null +++ b/toolkit/components/printing/content/print.html @@ -0,0 +1,587 @@ +<!DOCTYPE html> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. --> +<html> + <head> + <meta charset="utf-8" /> + <title data-l10n-id="printui-title"></title> + <meta + http-equiv="Content-Security-Policy" + content="default-src chrome:;img-src data:; object-src 'none'" + /> + + <link rel="localization" href="toolkit/printing/printUI.ftl" /> + + <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" /> + <link rel="stylesheet" href="chrome://global/content/toggle-group.css" /> + <link rel="stylesheet" href="chrome://global/content/print.css" /> + <script defer src="chrome://global/content/print.js"></script> + <script + type="module" + src="chrome://global/content/elements/moz-button-group.mjs" + ></script> + </head> + + <body loading rendering> + <template id="page-range-template"> + <select + id="range-picker" + name="page-range-type" + data-l10n-id="printui-page-range-picker" + is="setting-select" + > + <option + value="all" + selected + data-l10n-id="printui-page-range-all" + ></option> + <option + value="current" + data-l10n-id="printui-page-range-current" + ></option> + <option value="odd" data-l10n-id="printui-page-range-odd"></option> + <option + value="even" + name="even" + data-l10n-id="printui-page-range-even" + ></option> + <option + value="custom" + data-l10n-id="printui-page-range-custom" + ></option> + </select> + <input + id="custom-range" + type="text" + disabled + hidden + data-l10n-id="printui-page-custom-range-input" + aria-errormessage="error-invalid-range error-invalid-start-range-overflow" + /> + <p + id="error-invalid-range" + hidden + data-l10n-id="printui-error-invalid-range" + class="error-message" + role="alert" + data-l10n-args='{ "numPages": 1 }' + ></p> + <p + id="error-invalid-start-range-overflow" + hidden + data-l10n-id="printui-error-invalid-start-overflow" + class="error-message" + role="alert" + ></p> + </template> + + <template id="orientation-template"> + <input + type="radio" + name="orientation" + id="portrait" + value="0" + checked + class="toggle-group-input" + /> + <label + for="portrait" + data-l10n-id="printui-portrait" + class="toggle-group-label toggle-group-label-iconic" + ></label> + <input + type="radio" + name="orientation" + id="landscape" + value="1" + class="toggle-group-input" + /> + <label + for="landscape" + data-l10n-id="printui-landscape" + class="toggle-group-label toggle-group-label-iconic" + ></label> + </template> + + <template id="twisty-summary-template"> + <span class="label"></span> + <span class="twisty"></span> + </template> + + <template id="scale-template"> + <div role="radiogroup" aria-labelledby="scale-label"> + <label class="row cols-2"> + <input + type="radio" + name="scale-choice" + id="fit-choice" + value="fit" + checked + /> + <span + data-l10n-id="printui-scale-fit-to-page-width" + class="col" + ></span> + </label> + <label class="row cols-2" for="percent-scale-choice"> + <input type="radio" name="scale-choice" id="percent-scale-choice" /> + <span class="col"> + <span + id="percent-scale-label" + data-l10n-id="printui-scale-pcent" + ></span> + <!-- Note that here and elsewhere, we're setting aria-errormessage + attributes to a list of all possible errors. The a11y APIs + will filter this down to visible items only. --> + <input + id="percent-scale" + class="photon-number" + is="setting-number" + min="10" + max="200" + step="1" + size="6" + aria-labelledby="percent-scale-label" + aria-errormessage="error-invalid-scale" + disabled + required + /> + </span> + </label> + <p + id="error-invalid-scale" + hidden + data-l10n-id="printui-error-invalid-scale" + class="error-message" + role="alert" + ></p> + </div> + </template> + + <template id="copy-template"> + <input + id="copies-count" + is="setting-number" + data-setting-name="numCopies" + min="1" + max="10000" + class="copy-count-input photon-number" + aria-errormessage="error-invalid-copies" + required + /> + <p + id="error-invalid-copies" + hidden + data-l10n-id="printui-error-invalid-copies" + class="error-message" + role="alert" + ></p> + </template> + + <template id="margins-template"> + <label + for="margins-picker" + class="block-label" + data-l10n-id="printui-margins" + ></label> + <select + is="margins-select" + id="margins-picker" + name="margin-type" + class="row" + data-setting-name="margins" + > + <option value="default" data-l10n-id="printui-margins-default"></option> + <option value="minimum" data-l10n-id="printui-margins-min"></option> + <option value="none" data-l10n-id="printui-margins-none"></option> + <option + value="custom" + data-unit-prefix-l10n-id="printui-margins-custom-" + ></option> + </select> + <div id="custom-margins" class="margin-group" role="group" hidden> + <div class="vertical-margins"> + <div class="margin-pair"> + <input + is="setting-number" + id="custom-margin-top" + class="margin-input photon-number" + aria-describedby="margins-custom-margin-top-desc" + min="0" + step="0.01" + required + /> + <label + for="custom-margin-top" + class="margin-descriptor" + data-l10n-id="printui-margins-custom-top" + ></label> + <label + hidden + id="margins-custom-margin-top-desc" + data-unit-prefix-l10n-id="printui-margins-custom-top-" + ></label> + </div> + <div class="margin-pair"> + <input + is="setting-number" + id="custom-margin-bottom" + class="margin-input photon-number" + aria-describedby="margins-custom-margin-bottom-desc" + min="0" + step="0.01" + required + /> + <label + for="custom-margin-bottom" + class="margin-descriptor" + data-l10n-id="printui-margins-custom-bottom" + ></label> + <label + hidden + id="margins-custom-margin-bottom-desc" + data-unit-prefix-l10n-id="printui-margins-custom-bottom-" + ></label> + </div> + </div> + <div class="horizontal-margins"> + <div class="margin-pair"> + <input + is="setting-number" + id="custom-margin-left" + class="margin-input photon-number" + aria-describedby="margins-custom-margin-left-desc" + min="0" + step="0.01" + required + /> + <label + for="custom-margin-left" + class="margin-descriptor" + data-l10n-id="printui-margins-custom-left" + ></label> + <label + hidden + id="margins-custom-margin-left-desc" + data-unit-prefix-l10n-id="printui-margins-custom-left-" + ></label> + </div> + <div class="margin-pair"> + <input + is="setting-number" + id="custom-margin-right" + class="margin-input photon-number" + aria-describedby="margins-custom-margin-right-desc" + min="0" + step="0.01" + required + /> + <label + for="custom-margin-right" + class="margin-descriptor" + data-l10n-id="printui-margins-custom-right" + ></label> + <label + hidden + id="margins-custom-margin-right-desc" + data-unit-prefix-l10n-id="printui-margins-custom-right-" + ></label> + </div> + </div> + <p + id="error-invalid-margin" + hidden + data-l10n-id="printui-error-invalid-margin" + class="error-message" + role="alert" + ></p> + </div> + </template> + + <header class="header-container" role="none"> + <h2 data-l10n-id="printui-title"></h2> + <div aria-live="off"> + <p + id="sheet-count" + is="page-count" + data-l10n-id="printui-sheets-count" + data-l10n-args='{ "sheetCount": 0 }' + loading + ></p> + </div> + </header> + + <hr /> + + <form id="print" is="print-form" aria-labelledby="page-header"> + <section class="body-container"> + <section id="destination" class="section-block"> + <label + for="printer-picker" + class="block-label" + data-l10n-id="printui-destination-label" + ></label> + <div class="printer-picker-wrapper"> + <select + is="destination-picker" + id="printer-picker" + data-setting-name="printerName" + iconic + ></select> + </div> + </section> + <section id="settings"> + <section id="copies" class="section-block"> + <label + for="copies-count" + class="block-label" + data-l10n-id="printui-copies-label" + ></label> + <copy-count-input></copy-count-input> + </section> + + <section id="orientation" class="section-block"> + <label + id="orientation-label" + class="block-label" + data-l10n-id="printui-orientation" + ></label> + <div + is="orientation-input" + class="toggle-group" + role="radiogroup" + aria-labelledby="orientation-label" + ></div> + </section> + + <section id="pages" class="section-block"> + <label + for="page-range-input" + class="block-label" + data-l10n-id="printui-page-range-label" + ></label> + <div + id="page-range-input" + is="page-range-input" + class="page-range-input row" + ></div> + </section> + + <section id="color-mode" class="section-block"> + <label + for="color-mode-picker" + class="block-label" + data-l10n-id="printui-color-mode-label" + ></label> + <select + is="color-mode-select" + id="color-mode-picker" + class="row" + data-setting-name="printInColor" + > + <option + value="color" + selected + data-l10n-id="printui-color-mode-color" + ></option> + <option value="bw" data-l10n-id="printui-color-mode-bw"></option> + </select> + </section> + + <hr /> + + <details id="more-settings" class="twisty"> + <summary + class="block-label section-block" + is="twisty-summary" + data-open-l10n-id="printui-less-settings" + data-closed-l10n-id="printui-more-settings" + ></summary> + + <section id="paper-size" class="section-block"> + <label + for="paper-size-picker" + class="block-label" + data-l10n-id="printui-paper-size-label" + ></label> + <select + is="paper-size-select" + id="paper-size-picker" + class="row" + data-setting-name="paperId" + ></select> + </section> + + <section id="scale" class="section-block"> + <label + id="scale-label" + class="block-label" + data-l10n-id="printui-scale" + ></label> + <scale-input></scale-input> + </section> + + <section id="pages-per-sheet" class="section-block"> + <label + id="pages-per-sheet-label" + for="pages-per-sheet-picker" + class="block-label" + data-l10n-id="printui-pages-per-sheet" + ></label> + <select + is="setting-select" + id="pages-per-sheet-picker" + class="row" + data-setting-name="numPagesPerSheet" + > + <option value="1">1</option> + <option value="2">2</option> + <option value="4">4</option> + <option value="6">6</option> + <option value="9">9</option> + <option value="16">16</option> + </select> + </section> + + <section id="margins" class="section-block"> + <div + id="margins-select" + is="margins-select" + class="margins-select row" + ></div> + </section> + + <section id="two-sided-printing" class="section-block"> + <label + class="block-label" + for="duplex-select" + data-l10n-id="printui-two-sided-printing" + ></label> + <select + is="setting-select" + id="duplex-select" + name="duplex-type" + class="row" + data-setting-name="printDuplex" + > + <option + value="off" + data-l10n-id="printui-two-sided-printing-off" + selected + ></option> + <option + value="long-edge" + data-l10n-id="printui-two-sided-printing-long-edge" + ></option> + <option + value="short-edge" + data-l10n-id="printui-two-sided-printing-short-edge" + ></option> + </select> + </section> + + <section id="source-version-section" class="section-block"> + <label + id="source-version-label" + data-l10n-id="printui-source-label" + ></label> + <div role="radiogroup" aria-labelledby="source-version-label"> + <label id="source-version-source" class="row cols-2"> + <input + is="setting-radio" + name="source-version" + value="source" + id="source-version-source-radio" + data-setting-name="sourceVersion" + checked + /> + <span data-l10n-id="printui-source-radio"></span> + </label> + <label id="source-version-selection" class="row cols-2"> + <input + is="setting-radio" + name="source-version" + value="selection" + id="source-version-selection-radio" + data-setting-name="sourceVersion" + /> + <span data-l10n-id="printui-selection-radio"></span> + </label> + <label id="source-version-simplified" class="row cols-2"> + <input + is="setting-radio" + name="source-version" + value="simplified" + id="source-version-simplified-radio" + data-setting-name="sourceVersion" + /> + <span data-l10n-id="printui-simplify-page-radio"></span> + </label> + </div> + </section> + + <section id="more-settings-options" class="section-block"> + <label class="block-label" data-l10n-id="printui-options"></label> + <label id="headers-footers" class="row cols-2"> + <input + is="setting-checkbox" + id="headers-footers-enabled" + data-setting-name="printFootersHeaders" + /> + <span data-l10n-id="printui-headers-footers-checkbox"></span> + </label> + <label id="backgrounds" class="row cols-2"> + <input + is="print-backgrounds" + id="backgrounds-enabled" + data-setting-name="printBackgrounds" + /> + <span data-l10n-id="printui-backgrounds-checkbox"></span> + </label> + </section> + </details> + </section> + + <section id="system-print" class="section-block"> + <a + href="#" + id="open-dialog-link" + data-l10n-id="printui-system-dialog-link" + ></a> + </section> + </section> + + <hr /> + + <footer class="footer-container" id="print-footer" role="none"> + <p + id="print-progress" + class="section-block" + data-l10n-id="printui-print-progress-indicator" + hidden + ></p> + <moz-button-group id="button-container" class="section-block"> + <button + id="print-button" + class="primary" + showfocus + name="print" + data-l10n-id="printui-primary-button" + is="print-button" + type="submit" + ></button> + <button + id="cancel-button" + name="cancel" + data-l10n-id="printui-cancel-button" + data-close-l10n-id="printui-close-button" + data-cancel-l10n-id="printui-cancel-button" + type="button" + is="cancel-button" + ></button> + </moz-button-group> + </footer> + </form> + </body> +</html> diff --git a/toolkit/components/printing/content/print.js b/toolkit/components/printing/content/print.js new file mode 100644 index 0000000000..45938431b5 --- /dev/null +++ b/toolkit/components/printing/content/print.js @@ -0,0 +1,2847 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { PrintUtils, Services, AppConstants } = + window.docShell.chromeEventHandler.ownerGlobal; + +ChromeUtils.defineESModuleGetters(this, { + DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", + DownloadPaths: "resource://gre/modules/DownloadPaths.sys.mjs", +}); + +const PDF_JS_URI = "resource://pdf.js/web/viewer.html"; +const INPUT_DELAY_MS = Cu.isInAutomation ? 100 : 500; +const MM_PER_POINT = 25.4 / 72; +const INCHES_PER_POINT = 1 / 72; +const INCHES_PER_MM = 1 / 25.4; +const ourBrowser = window.docShell.chromeEventHandler; +const PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService( + Ci.nsIPrintSettingsService +); + +var logger = (function () { + const getMaxLogLevel = () => + Services.prefs.getBoolPref("print.debug", false) ? "all" : "warn"; + + let { ConsoleAPI } = ChromeUtils.importESModule( + "resource://gre/modules/Console.sys.mjs" + ); + // Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref. + let _logger = new ConsoleAPI({ + prefix: "printUI", + maxLogLevel: getMaxLogLevel(), + }); + + function onPrefChange() { + if (_logger) { + _logger.maxLogLevel = getMaxLogLevel(); + } + } + // Watch for pref changes and the maxLogLevel for the logger + Services.prefs.addObserver("print.debug", onPrefChange); + window.addEventListener("unload", () => { + Services.prefs.removeObserver("print.debug", onPrefChange); + }); + return _logger; +})(); + +function serializeSettings(settings, logPrefix) { + let re = /^(k[A-Z]|resolution)/; // accessing settings.resolution throws an exception? + let types = new Set(["string", "boolean", "number", "undefined"]); + let nameValues = {}; + for (let key in settings) { + try { + if (!re.test(key) && types.has(typeof settings[key])) { + nameValues[key] = settings[key]; + } + } catch (e) { + logger.warn("Exception accessing setting: ", key, e); + } + } + return JSON.stringify(nameValues, null, 2); +} + +let printPending = false; +let deferredTasks = []; +function createDeferredTask(fn, timeout) { + let task = new DeferredTask(fn, timeout); + deferredTasks.push(task); + return task; +} + +function cancelDeferredTasks() { + for (let task of deferredTasks) { + task.disarm(); + } + PrintEventHandler._updatePrintPreviewTask?.disarm(); + deferredTasks = []; +} + +document.addEventListener( + "DOMContentLoaded", + e => { + window._initialized = PrintEventHandler.init().catch(e => console.error(e)); + ourBrowser.setAttribute("flex", "0"); + ourBrowser.setAttribute("constrainpopups", "false"); + ourBrowser.classList.add("printSettingsBrowser"); + ourBrowser.closest(".dialogBox")?.classList.add("printDialogBox"); + }, + { once: true } +); + +window.addEventListener("dialogclosing", () => { + cancelDeferredTasks(); +}); + +window.addEventListener( + "unload", + e => { + document.textContent = ""; + }, + { once: true } +); + +var PrintEventHandler = { + settings: null, + defaultSettings: null, + allPaperSizes: {}, + previewIsEmpty: false, + _delayedChanges: {}, + _userChangedSettings: {}, + settingFlags: { + margins: Ci.nsIPrintSettings.kInitSaveMargins, + customMargins: Ci.nsIPrintSettings.kInitSaveMargins, + orientation: Ci.nsIPrintSettings.kInitSaveOrientation, + paperId: + Ci.nsIPrintSettings.kInitSavePaperSize | + Ci.nsIPrintSettings.kInitSaveUnwriteableMargins, + printInColor: Ci.nsIPrintSettings.kInitSaveInColor, + scaling: Ci.nsIPrintSettings.kInitSaveScaling, + shrinkToFit: Ci.nsIPrintSettings.kInitSaveShrinkToFit, + printDuplex: Ci.nsIPrintSettings.kInitSaveDuplex, + printFootersHeaders: + Ci.nsIPrintSettings.kInitSaveHeaderLeft | + Ci.nsIPrintSettings.kInitSaveHeaderCenter | + Ci.nsIPrintSettings.kInitSaveHeaderRight | + Ci.nsIPrintSettings.kInitSaveFooterLeft | + Ci.nsIPrintSettings.kInitSaveFooterCenter | + Ci.nsIPrintSettings.kInitSaveFooterRight, + printBackgrounds: + Ci.nsIPrintSettings.kInitSaveBGColors | + Ci.nsIPrintSettings.kInitSaveBGImages, + }, + + topContentTitle: null, + topCurrentURI: null, + activeContentTitle: null, + activeCurrentURI: null, + + get activeURI() { + return this.viewSettings.sourceVersion == "selection" + ? this.activeCurrentURI + : this.topCurrentURI; + }, + get activeTitle() { + return this.viewSettings.sourceVersion == "selection" + ? this.activeContentTitle + : this.topContentTitle; + }, + + // These settings do not have an associated pref value or flag, but + // changing them requires us to update the print preview. + _nonFlaggedUpdatePreviewSettings: new Set([ + "pageRanges", + "numPagesPerSheet", + "sourceVersion", + ]), + _noPreviewUpdateSettings: new Set(["numCopies", "printDuplex"]), + + async init() { + Services.telemetry.scalarAdd("printing.preview_opened_tm", 1); + + this.printPreviewEl = + ourBrowser.parentElement.querySelector("print-preview"); + + // Do not keep a reference to source browser, it may mutate after printing + // is initiated and the print preview clone must be a snapshot from the + // time that the print was started. + let sourceBrowsingContext = this.printPreviewEl.getSourceBrowsingContext(); + + let args = window.arguments[0]; + this.printFrameOnly = args.getProperty("printFrameOnly"); + this.printSelectionOnly = args.getProperty("printSelectionOnly"); + this.isArticle = args.getProperty("isArticle"); + this.hasSelection = await PrintUtils.checkForSelection( + sourceBrowsingContext + ); + + let sourcePrincipal = + sourceBrowsingContext.currentWindowGlobal.documentPrincipal; + let sourceIsPdf = + !sourcePrincipal.isNullPrincipal && sourcePrincipal.spec == PDF_JS_URI; + this.activeContentTitle = + sourceBrowsingContext.currentWindowContext.documentTitle; + this.activeCurrentURI = + sourceBrowsingContext.currentWindowContext.documentURI.spec; + let topWindowContext = sourceBrowsingContext.top.currentWindowContext; + this.topContentTitle = topWindowContext.documentTitle; + this.topCurrentURI = topWindowContext.documentURI.spec; + this.isReader = this.topCurrentURI.startsWith("about:reader"); + + let canSimplify = !this.isReader && this.isArticle; + if (!this.hasSelection && !canSimplify) { + document.getElementById("source-version-section").hidden = true; + } else { + document.getElementById("source-version-selection").hidden = + !this.hasSelection; + document.getElementById("source-version-simplified").hidden = + !canSimplify; + } + + // We don't need the sourceBrowsingContext anymore, get rid of it. + sourceBrowsingContext = undefined; + + this.printProgressIndicator = document.getElementById("print-progress"); + this.printForm = document.getElementById("print"); + if (sourceIsPdf) { + this.printForm.removeNonPdfSettings(); + } + + // Let the dialog appear before doing any potential main thread work. + await ourBrowser._dialogReady; + + // First check the available destinations to ensure we get settings for an + // accessible printer. + let destinations, + defaultSystemPrinter, + fallbackPaperList, + selectedPrinter, + printersByName; + try { + ({ + destinations, + defaultSystemPrinter, + fallbackPaperList, + selectedPrinter, + printersByName, + } = await this.getPrintDestinations()); + } catch (e) { + this.reportPrintingError("PRINT_DESTINATIONS"); + throw e; + } + PrintSettingsViewProxy.availablePrinters = printersByName; + PrintSettingsViewProxy.fallbackPaperList = fallbackPaperList; + PrintSettingsViewProxy.defaultSystemPrinter = defaultSystemPrinter; + PrintSettingsViewProxy._sourceVersion = + this.hasSelection && this.printSelectionOnly ? "selection" : "source"; + + logger.debug("availablePrinters: ", Object.keys(printersByName)); + logger.debug("defaultSystemPrinter: ", defaultSystemPrinter); + + document.addEventListener("print", async () => { + let cancelButton = document.getElementById("cancel-button"); + document.l10n.setAttributes( + cancelButton, + cancelButton.dataset.closeL10nId + ); + let didPrint = await this.print(); + if (!didPrint) { + // Re-enable elements of the form if the user cancels saving or + // if a deferred task rendered the page invalid. + this.printForm.enable(); + } + // Reset the cancel button regardless of the outcome. + document.l10n.setAttributes( + cancelButton, + cancelButton.dataset.cancelL10nId + ); + }); + this._createDelayedSettingsChangeTask(); + document.addEventListener("update-print-settings", e => { + this.handleSettingsChange(e.detail); + }); + document.addEventListener("cancel-print-settings", e => { + this._delayedSettingsChangeTask.disarm(); + for (let setting of Object.keys(e.detail)) { + delete this._delayedChanges[setting]; + } + }); + document.addEventListener("cancel-print", () => this.cancelPrint()); + document.addEventListener("open-system-dialog", async () => { + // This file in only used if pref print.always_print_silent is false, so + // no need to check that here. + + // 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.printPreviewEl.getSourceBrowsingContext().top.embedderElement; + let dialogBoxManager = + PrintUtils.getTabDialogBox(sourceBrowser).getTabDialogManager(); + dialogBoxManager.hideDialog(sourceBrowser); + + // Use our settings to prepopulate the system dialog. + // The system print dialog won't recognize our internal save-to-pdf + // pseudo-printer. We need to pass it a settings object from any + // system recognized printer. + let settings = + this.settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER + ? PrintUtils.getPrintSettings(this.viewSettings.defaultSystemPrinter) + : this.settings.clone(); + // We set the title so that if the user chooses save-to-PDF from the + // system dialog the title will be used to generate the prepopulated + // filename in the file picker. + settings.title = this.activeTitle; + Services.telemetry.scalarAdd("printing.dialog_opened_via_preview_tm", 1); + const doPrint = await this._showPrintDialog( + window, + this.hasSelection, + settings + ); + if (!doPrint) { + Services.telemetry.scalarAdd( + "printing.dialog_via_preview_cancelled_tm", + 1 + ); + window.close(); + return; + } + await this.print(settings); + }); + + let originalError; + const printersByPriority = [ + selectedPrinter.value, + ...Object.getOwnPropertyNames(printersByName).filter( + name => name != selectedPrinter.value + ), + ]; + + // Try to update settings, falling back to any available printer + for (const printerName of printersByPriority) { + try { + let settingsToChange = await this.refreshSettings(printerName); + await this.updateSettings(settingsToChange, true); + originalError = null; + break; + } catch (e) { + if (!originalError) { + originalError = e; + // Report on how often fetching the last used printer settings fails. + this.reportPrintingError("PRINTER_SETTINGS_LAST_USED"); + } + } + } + + // Only throw original error if no fallback was possible + if (originalError) { + this.reportPrintingError("PRINTER_SETTINGS"); + throw originalError; + } + + let initialPreviewDone = this._updatePrintPreview(); + + // Use a DeferredTask for updating the preview. This will ensure that we + // only have one update running at a time. + this._createUpdatePrintPreviewTask(initialPreviewDone); + + document.dispatchEvent( + new CustomEvent("available-destinations", { + detail: destinations, + }) + ); + + document.dispatchEvent( + new CustomEvent("print-settings", { + detail: this.viewSettings, + }) + ); + + document.body.removeAttribute("loading"); + + await new Promise(resolve => window.requestAnimationFrame(resolve)); + + // Now that we're showing the form, select the destination select. + document.getElementById("printer-picker").focus({ focusVisible: true }); + + await initialPreviewDone; + }, + + async print(systemDialogSettings) { + // Disable the form when a print is in progress + this.printForm.disable(); + + if (Object.keys(this._delayedChanges).length) { + // Make sure any pending changes get saved. + let task = this._delayedSettingsChangeTask; + this._createDelayedSettingsChangeTask(); + await task.finalize(); + } + + if (this.settings.pageRanges.length) { + // Finish any running previews to verify the range is still valid. + let task = this._updatePrintPreviewTask; + this._createUpdatePrintPreviewTask(); + await task.finalize(); + } + + if (!this.printForm.checkValidity() || this.previewIsEmpty) { + return false; + } + + let settings = systemDialogSettings || this.settings; + + if (settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER) { + try { + settings.toFileName = await pickFileName( + this.activeTitle, + this.activeURI + ); + } catch (e) { + return false; + } + } + + await window._initialized; + + // This seems like it should be handled automatically but it isn't. + PSSVC.maybeSaveLastUsedPrinterNameToPrefs(settings.printerName); + + try { + // We'll provide our own progress indicator. + let l10nId = + settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER + ? "printui-print-progress-indicator-saving" + : "printui-print-progress-indicator"; + document.l10n.setAttributes(this.printProgressIndicator, l10nId); + this.printProgressIndicator.hidden = false; + let bc = this.printPreviewEl.currentBrowsingContext; + await this._doPrint(bc, settings); + } catch (e) { + console.error(e); + } + + if (settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER) { + // Clear the file name from the preference value since it may potentially + // contain sensitive information from the page title (Bug 1675965) + let prefName = + "print.printer_" + + settings.printerName.replace(/ /g, "_") + + ".print_to_filename"; + Services.prefs.clearUserPref(prefName); + } + + window.close(); + return true; + }, + + /** + * Prints the window. This method has been abstracted into a helper for + * testing purposes. + */ + _doPrint(aBrowsingContext, aSettings) { + return aBrowsingContext.print(aSettings); + }, + + cancelPrint() { + Services.telemetry.scalarAdd("printing.preview_cancelled_tm", 1); + window.close(); + }, + + async refreshSettings(printerName) { + this.currentPrinterName = printerName; + let currentPrinter; + try { + currentPrinter = await PrintSettingsViewProxy.resolvePropertiesForPrinter( + printerName + ); + } catch (e) { + this.reportPrintingError("PRINTER_PROPERTIES"); + throw e; + } + if (this.currentPrinterName != printerName) { + // Refresh settings could take a while, if the destination has changed + // then we don't want to update the settings after all. + return {}; + } + + this.settings = currentPrinter.settings; + this.defaultSettings = currentPrinter.defaultSettings; + + this.settings.printSelectionOnly = this.printSelectionOnly; + + logger.debug("currentPrinter name: ", printerName); + logger.debug("settings:", serializeSettings(this.settings)); + + // Some settings are only used by the UI + // assigning new values should update the underlying settings + this.viewSettings = new Proxy(this.settings, PrintSettingsViewProxy); + return this.getSettingsToUpdate(); + }, + + getSettingsToUpdate() { + // Get the previously-changed settings we want to try to use on this printer + let settingsToUpdate = Object.assign({}, this._userChangedSettings); + + // Ensure the color option is correct, if either of the supportsX flags are + // false then the user cannot change the value through the UI. + if (!this.viewSettings.supportsColor) { + settingsToUpdate.printInColor = false; + } else if (!this.viewSettings.supportsMonochrome) { + settingsToUpdate.printInColor = true; + } + + if (settingsToUpdate.sourceVersion == "simplified") { + if (this.viewSettings.printBackgrounds) { + // Remember that this was true before so it gets restored if the + // format is changed to something else. + this._userChangedSettings.printBackgrounds = true; + } + // Backgrounds are removed in simplified mode and this setting changes + // the output subtly to be less legible. + settingsToUpdate.printBackgrounds = false; + } + + if ( + settingsToUpdate.printInColor != this._userChangedSettings.printInColor + ) { + delete this._userChangedSettings.printInColor; + } + + // See if the paperId needs to change. + let paperId = settingsToUpdate.paperId || this.viewSettings.paperId; + + logger.debug("Using paperId: ", paperId); + logger.debug( + "Available paper sizes: ", + PrintSettingsViewProxy.availablePaperSizes + ); + let matchedPaper = + paperId && PrintSettingsViewProxy.availablePaperSizes[paperId]; + if (!matchedPaper) { + let paperWidth, paperHeight, paperSizeUnit; + if (settingsToUpdate.paperId) { + // The user changed paperId in this instance and session, + // We should have details on the paper size from the previous printer + paperId = settingsToUpdate.paperId; + let cachedPaperWrapper = this.allPaperSizes[paperId]; + // for the purposes of finding a best-size match, we'll use mm + paperWidth = cachedPaperWrapper.paper.width * MM_PER_POINT; + paperHeight = cachedPaperWrapper.paper.height * MM_PER_POINT; + paperSizeUnit = PrintEventHandler.settings.kPaperSizeMillimeters; + } else { + paperId = this.viewSettings.paperId; + logger.debug( + "No paperId or matchedPaper, get a new default from viewSettings:", + paperId + ); + paperWidth = this.viewSettings.paperWidth; + paperHeight = this.viewSettings.paperHeight; + paperSizeUnit = this.viewSettings.paperSizeUnit; + } + matchedPaper = PrintSettingsViewProxy.getBestPaperMatch( + paperWidth, + paperHeight, + paperSizeUnit + ); + } + if (!matchedPaper) { + // We didn't find a good match. Take the first paper size + matchedPaper = Object.values( + PrintSettingsViewProxy.availablePaperSizes + )[0]; + delete this._userChangedSettings.paperId; + } + if (matchedPaper.id !== paperId) { + // The exact paper id doesn't exist for this printer + logger.log( + `Requested paperId: "${paperId}" missing on this printer, using: ${matchedPaper.id} instead` + ); + delete this._userChangedSettings.paperId; + } + // Always write paper details back to settings + settingsToUpdate.paperId = matchedPaper.id; + + return settingsToUpdate; + }, + + _createDelayedSettingsChangeTask() { + this._delayedSettingsChangeTask = createDeferredTask(async () => { + if (Object.keys(this._delayedChanges).length) { + let changes = this._delayedChanges; + this._delayedChanges = {}; + await this.onUserSettingsChange(changes); + } + }, INPUT_DELAY_MS); + }, + + _createUpdatePrintPreviewTask(initialPreviewDone = null) { + this._updatePrintPreviewTask = new DeferredTask(async () => { + await initialPreviewDone; + await this._updatePrintPreview(); + document.dispatchEvent(new CustomEvent("preview-updated")); + }, 0); + }, + + _scheduleDelayedSettingsChange(changes) { + Object.assign(this._delayedChanges, changes); + this._delayedSettingsChangeTask.disarm(); + this._delayedSettingsChangeTask.arm(); + }, + + handleSettingsChange(changedSettings = {}) { + let delayedChanges = {}; + let instantChanges = {}; + for (let [setting, value] of Object.entries(changedSettings)) { + switch (setting) { + case "pageRanges": + case "scaling": + delayedChanges[setting] = value; + break; + case "customMargins": + delete this._delayedChanges.margins; + changedSettings.margins == "custom" + ? (delayedChanges[setting] = value) + : (instantChanges[setting] = value); + break; + default: + instantChanges[setting] = value; + break; + } + } + if (Object.keys(delayedChanges).length) { + this._scheduleDelayedSettingsChange(delayedChanges); + } + if (Object.keys(instantChanges).length) { + this.onUserSettingsChange(instantChanges); + } + }, + + async onUserSettingsChange(changedSettings = {}) { + let previewableChange = false; + for (let [setting, value] of Object.entries(changedSettings)) { + Services.telemetry.keyedScalarAdd( + "printing.settings_changed", + setting, + 1 + ); + // Update the list of user-changed settings, which we attempt to maintain + // across printer changes. + this._userChangedSettings[setting] = value; + if (!this._noPreviewUpdateSettings.has(setting)) { + previewableChange = true; + } + } + if (changedSettings.printerName) { + logger.debug( + "onUserSettingsChange, changing to printerName:", + changedSettings.printerName + ); + this.printForm.printerChanging = true; + this.printForm.disable(el => el.id != "printer-picker"); + let { printerName } = changedSettings; + // Treat a printerName change separately, because it involves a settings + // object switch and we don't want to set the new name on the old settings. + changedSettings = await this.refreshSettings(printerName); + if (printerName != this.currentPrinterName) { + // Don't continue this update if the printer changed again. + return; + } + this.printForm.printerChanging = false; + this.printForm.enable(); + } else { + changedSettings = this.getSettingsToUpdate(); + } + + let shouldPreviewUpdate = + (await this.updateSettings( + changedSettings, + !!changedSettings.printerName + )) && previewableChange; + + if (shouldPreviewUpdate && !printPending) { + // We do not need to arm the preview task if the user has already printed + // and finalized any deferred tasks. + this.updatePrintPreview(); + } + document.dispatchEvent( + new CustomEvent("print-settings", { + detail: this.viewSettings, + }) + ); + }, + + async updateSettings(changedSettings = {}, printerChanged = false) { + let updatePreviewWithoutFlag = false; + let flags = 0; + logger.debug("updateSettings ", changedSettings, printerChanged); + + if (printerChanged || changedSettings.paperId) { + // The paper's margin properties are async, + // so resolve those now before we update the settings + try { + let paperWrapper = await PrintSettingsViewProxy.fetchPaperMargins( + changedSettings.paperId || this.viewSettings.paperId + ); + + // See if we also need to change the custom margin values + + let paperHeightInInches = paperWrapper.paper.height * INCHES_PER_POINT; + let paperWidthInInches = paperWrapper.paper.width * INCHES_PER_POINT; + let height = + (changedSettings.orientation || this.viewSettings.orientation) == 0 + ? paperHeightInInches + : paperWidthInInches; + let width = + (changedSettings.orientation || this.viewSettings.orientation) == 0 + ? paperWidthInInches + : paperHeightInInches; + + function verticalMarginsInvalid(margins) { + return ( + parseFloat(margins.marginTop) + parseFloat(margins.marginBottom) > + height - + paperWrapper.unwriteableMarginTop - + paperWrapper.unwriteableMarginBottom + ); + } + + function horizontalMarginsInvalid(margins) { + return ( + parseFloat(margins.marginRight) + parseFloat(margins.marginLeft) > + width - + paperWrapper.unwriteableMarginRight - + paperWrapper.unwriteableMarginLeft + ); + } + + let unwriteableMarginsInvalid = false; + if ( + verticalMarginsInvalid(this.viewSettings.customMargins) || + this.viewSettings.customMargins.marginTop < 0 || + this.viewSettings.customMargins.marginBottom < 0 + ) { + let { marginTop, marginBottom } = this.viewSettings.defaultMargins; + if (verticalMarginsInvalid(this.viewSettings.defaultMargins)) { + let marginsNone = this.getMarginPresets("none"); + marginTop = marginsNone.marginTop; + marginBottom = marginsNone.marginBottom; + unwriteableMarginsInvalid = true; + } + changedSettings.marginTop = changedSettings.customMarginTop = + marginTop; + changedSettings.marginBottom = changedSettings.customMarginBottom = + marginBottom; + delete this._userChangedSettings.customMargins; + } + + if ( + horizontalMarginsInvalid(this.viewSettings.customMargins) || + this.viewSettings.customMargins.marginLeft < 0 || + this.viewSettings.customMargins.marginRight < 0 + ) { + let { marginLeft, marginRight } = this.viewSettings.defaultMargins; + if (horizontalMarginsInvalid(this.viewSettings.defaultMargins)) { + let marginsNone = this.getMarginPresets("none"); + marginLeft = marginsNone.marginLeft; + marginRight = marginsNone.marginRight; + unwriteableMarginsInvalid = true; + } + changedSettings.marginLeft = changedSettings.customMarginLeft = + marginLeft; + changedSettings.marginRight = changedSettings.customMarginRight = + marginRight; + delete this._userChangedSettings.customMargins; + } + + if (unwriteableMarginsInvalid) { + changedSettings.ignoreUnwriteableMargins = true; + } + } catch (e) { + this.reportPrintingError("PAPER_MARGINS"); + throw e; + } + } + + for (let [setting, value] of Object.entries(changedSettings)) { + // Always write paper changes back to settings as pref-derived values could be bad + if ( + this.viewSettings[setting] != value || + (printerChanged && setting == "paperId") + ) { + if (setting == "pageRanges") { + // The page range is kept as an array. If the user switches between all + // and custom with no specified range input (which is represented as an + // empty array), we do not want to send an update. + if (!this.viewSettings[setting].length && !value.length) { + continue; + } + } + this.viewSettings[setting] = value; + + if ( + setting in this.settingFlags && + setting in this._userChangedSettings + ) { + flags |= this.settingFlags[setting]; + } + updatePreviewWithoutFlag |= + this._nonFlaggedUpdatePreviewSettings.has(setting); + } + } + + let shouldPreviewUpdate = + flags || printerChanged || updatePreviewWithoutFlag; + logger.debug( + "updateSettings, calculated flags:", + flags, + "shouldPreviewUpdate:", + shouldPreviewUpdate + ); + if (flags) { + this.saveSettingsToPrefs(flags); + } + return shouldPreviewUpdate; + }, + + saveSettingsToPrefs(flags) { + PSSVC.maybeSavePrintSettingsToPrefs(this.settings, flags); + }, + + /** + * Queue a task to update the print preview. It will start immediately or when + * the in progress update completes. + */ + async updatePrintPreview() { + // Make sure the rendering state is set so we don't visibly update the + // sheet count with incomplete data. + this._updatePrintPreviewTask.arm(); + }, + + /** + * Creates a print preview or refreshes the preview with new settings when omitted. + * + * @return {Promise} Resolves when the preview has been updated. + */ + async _updatePrintPreview() { + let { settings } = this; + + const isFirstCall = !this.printInitiationTime; + if (isFirstCall) { + let params = new URLSearchParams(location.search); + this.printInitiationTime = parseInt( + params.get("printInitiationTime"), + 10 + ); + const elapsed = Date.now() - this.printInitiationTime; + Services.telemetry + .getHistogramById("PRINT_INIT_TO_PLATFORM_SENT_SETTINGS_MS") + .add(elapsed); + } + + let totalPageCount, sheetCount, isEmpty, orientation, pageWidth, pageHeight; + try { + // This resolves with a PrintPreviewSuccessInfo dictionary. + let { sourceVersion } = this.viewSettings; + let sourceURI = this.activeURI; + this._lastPrintPreviewSettings = settings; + ({ + totalPageCount, + sheetCount, + isEmpty, + orientation, + pageWidth, + pageHeight, + } = await this.printPreviewEl.printPreview(settings, { + sourceVersion, + sourceURI, + })); + } catch (e) { + this.reportPrintingError("PRINT_PREVIEW"); + console.error(e); + throw e; + } + + // If there is a set orientation, update the settings to use it. In this + // case, the document will already have used this orientation to create + // the print preview. + if (orientation != "unspecified") { + const kIPrintSettings = Ci.nsIPrintSettings; + settings.orientation = + orientation == "landscape" + ? kIPrintSettings.kLandscapeOrientation + : kIPrintSettings.kPortraitOrientation; + document.dispatchEvent(new CustomEvent("hide-orientation")); + } + + // If the page size is set, check whether we should use it as our paper size. + let isUsingPageRuleSizeAsPaperSize = + settings.usePageRuleSizeAsPaperSize && + pageWidth !== null && + pageHeight !== null; + if (isUsingPageRuleSizeAsPaperSize) { + // We canonically represent paper sizes using the width/height of a portrait-oriented sheet, + // with landscape-orientation applied as a supplemental rotation. + // If the page-size is landscape oriented, we flip the pageWidth / pageHeight here + // in order to pass a canonical representation into the paper-size settings. + if (orientation == "landscape") { + [pageHeight, pageWidth] = [pageWidth, pageHeight]; + } + + let matchedPaper = PrintSettingsViewProxy.getBestPaperMatch( + pageWidth, + pageHeight, + settings.kPaperSizeInches + ); + if (matchedPaper) { + settings.paperId = matchedPaper.id; + } + + settings.paperWidth = pageWidth; + settings.paperHeight = pageHeight; + settings.paperSizeUnit = settings.kPaperSizeInches; + document.dispatchEvent(new CustomEvent("hide-paper-size")); + } + + this.previewIsEmpty = isEmpty; + // If the preview is empty, we know our range is greater than the number of pages. + // We have to send a pageRange update to display a non-empty page. + if (this.previewIsEmpty) { + this.viewSettings.pageRanges = []; + this.updatePrintPreview(); + } + + document.dispatchEvent( + new CustomEvent("page-count", { + detail: { sheetCount, totalPages: totalPageCount }, + }) + ); + + if (isFirstCall) { + const elapsed = Date.now() - this.printInitiationTime; + Services.telemetry + .getHistogramById("PRINT_INIT_TO_PREVIEW_DOC_SHOWN_MS") + .add(elapsed); + } + }, + + async getPrintDestinations() { + const printerList = Cc["@mozilla.org/gfx/printerlist;1"].createInstance( + Ci.nsIPrinterList + ); + let printers; + + if (Cu.isInAutomation) { + printers = window._mockPrinters || []; + } else { + try { + printers = await printerList.printers; + } catch (e) { + this.reportPrintingError("PRINTER_LIST"); + throw e; + } + } + + let fallbackPaperList; + try { + fallbackPaperList = await printerList.fallbackPaperList; + } catch (e) { + this.reportPrintingError("FALLBACK_PAPER_LIST"); + throw e; + } + + let lastUsedPrinterName; + try { + lastUsedPrinterName = PSSVC.lastUsedPrinterName; + } catch (e) { + this.reportPrintingError("LAST_USED_PRINTER"); + throw e; + } + const defaultPrinterName = printerList.systemDefaultPrinterName; + const printersByName = {}; + + let lastUsedPrinter; + let defaultSystemPrinter; + + let saveToPdfPrinter = { + nameId: "printui-destination-pdf-label", + value: PrintUtils.SAVE_TO_PDF_PRINTER, + }; + printersByName[PrintUtils.SAVE_TO_PDF_PRINTER] = { + supportsColor: true, + supportsMonochrome: false, + name: PrintUtils.SAVE_TO_PDF_PRINTER, + }; + + if (lastUsedPrinterName == PrintUtils.SAVE_TO_PDF_PRINTER) { + lastUsedPrinter = saveToPdfPrinter; + } + + let destinations = [ + saveToPdfPrinter, + ...printers.map(printer => { + printer.QueryInterface(Ci.nsIPrinter); + const { name } = printer; + printersByName[printer.name] = { printer }; + const destination = { name, value: name }; + + if (name == lastUsedPrinterName) { + lastUsedPrinter = destination; + } + if (name == defaultPrinterName) { + defaultSystemPrinter = destination; + } + + return destination; + }), + ]; + + let selectedPrinter = + lastUsedPrinter || defaultSystemPrinter || saveToPdfPrinter; + + return { + destinations, + fallbackPaperList, + selectedPrinter, + printersByName, + defaultSystemPrinter, + }; + }, + + getMarginPresets(marginSize, paperWrapper) { + switch (marginSize) { + case "minimum": { + let marginSource = paperWrapper || this.defaultSettings; + return { + marginTop: marginSource.unwriteableMarginTop, + marginRight: marginSource.unwriteableMarginRight, + marginBottom: marginSource.unwriteableMarginBottom, + marginLeft: marginSource.unwriteableMarginLeft, + }; + } + case "none": + return { + marginTop: 0, + marginLeft: 0, + marginBottom: 0, + marginRight: 0, + }; + case "custom": + return { + marginTop: + PrintSettingsViewProxy._lastCustomMarginValues.marginTop ?? + this.settings.marginTop, + marginBottom: + PrintSettingsViewProxy._lastCustomMarginValues.marginBottom ?? + this.settings.marginBottom, + marginLeft: + PrintSettingsViewProxy._lastCustomMarginValues.marginLeft ?? + this.settings.marginLeft, + marginRight: + PrintSettingsViewProxy._lastCustomMarginValues.marginRight ?? + this.settings.marginRight, + }; + default: { + let minimum = this.getMarginPresets("minimum", paperWrapper); + return { + marginTop: !isNaN(minimum.marginTop) + ? Math.max(minimum.marginTop, this.defaultSettings.marginTop) + : this.defaultSettings.marginTop, + marginRight: !isNaN(minimum.marginRight) + ? Math.max(minimum.marginRight, this.defaultSettings.marginRight) + : this.defaultSettings.marginRight, + marginBottom: !isNaN(minimum.marginBottom) + ? Math.max(minimum.marginBottom, this.defaultSettings.marginBottom) + : this.defaultSettings.marginBottom, + marginLeft: !isNaN(minimum.marginLeft) + ? Math.max(minimum.marginLeft, this.defaultSettings.marginLeft) + : this.defaultSettings.marginLeft, + }; + } + } + }, + + reportPrintingError(aMessage) { + logger.debug("reportPrintingError:", aMessage); + Services.telemetry.keyedScalarAdd("printing.error", aMessage, 1); + }, + + /** + * 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(aWindow, aHaveSelection, aSettings) { + return PrintUtils.handleSystemPrintDialog( + aWindow, + aHaveSelection, + aSettings + ); + }, +}; + +var PrintSettingsViewProxy = { + get defaultHeadersAndFooterValues() { + const defaultBranch = Services.prefs.getDefaultBranch(""); + let settingValues = {}; + for (let [name, pref] of Object.entries(this.headerFooterSettingsPrefs)) { + settingValues[name] = defaultBranch.getStringPref(pref); + } + // We only need to retrieve these defaults once and they will not change + Object.defineProperty(this, "defaultHeadersAndFooterValues", { + value: settingValues, + }); + return settingValues; + }, + + headerFooterSettingsPrefs: { + footerStrCenter: "print.print_footercenter", + footerStrLeft: "print.print_footerleft", + footerStrRight: "print.print_footerright", + headerStrCenter: "print.print_headercenter", + headerStrLeft: "print.print_headerleft", + headerStrRight: "print.print_headerright", + }, + + // Custom margins are not saved by a pref, so we need to keep track of them + // in order to save the value. + _lastCustomMarginValues: { + marginTop: null, + marginBottom: null, + marginLeft: null, + marginRight: null, + }, + + // This list was taken from nsDeviceContextSpecWin.cpp which records telemetry on print target type + knownSaveToFilePrinters: new Set([ + "Microsoft Print to PDF", + "Adobe PDF", + "Bullzip PDF Printer", + "CutePDF Writer", + "doPDF", + "Foxit Reader PDF Printer", + "Nitro PDF Creator", + "novaPDF", + "PDF-XChange", + "PDF24 PDF", + "PDFCreator", + "PrimoPDF", + "Soda PDF", + "Solid PDF Creator", + "Universal Document Converter", + "Microsoft XPS Document Writer", + ]), + + getBestPaperMatch(paperWidth, paperHeight, paperSizeUnit) { + let paperSizes = Object.values(this.availablePaperSizes); + if (!(paperWidth && paperHeight)) { + return null; + } + // first try to match on the paper dimensions using the current units + let unitsPerPoint; + let altUnitsPerPoint; + if (paperSizeUnit == PrintEventHandler.settings.kPaperSizeMillimeters) { + unitsPerPoint = MM_PER_POINT; + altUnitsPerPoint = INCHES_PER_POINT; + } else { + unitsPerPoint = INCHES_PER_POINT; + altUnitsPerPoint = MM_PER_POINT; + } + // equality to 1pt. + const equal = (a, b) => Math.abs(a - b) < 1; + const findMatch = (widthPts, heightPts) => + paperSizes.find(paperWrapper => { + // the dimensions on the nsIPaper object are in points + let result = + equal(widthPts, paperWrapper.paper.width) && + equal(heightPts, paperWrapper.paper.height); + return result; + }); + // Look for a paper with matching dimensions, using the current printer's + // paper size unit, then the alternate unit + let matchedPaper = + findMatch(paperWidth / unitsPerPoint, paperHeight / unitsPerPoint) || + findMatch(paperWidth / altUnitsPerPoint, paperHeight / altUnitsPerPoint); + + if (matchedPaper) { + return matchedPaper; + } + return null; + }, + + async fetchPaperMargins(paperId) { + // resolve any async and computed properties we need on the paper + let paperWrapper = this.availablePaperSizes[paperId]; + if (!paperWrapper) { + throw new Error("Can't fetchPaperMargins: " + paperId); + } + if (paperWrapper._resolved) { + // We've already resolved and calculated these values + return paperWrapper; + } + let margins; + try { + margins = await paperWrapper.paper.unwriteableMargin; + } catch (e) { + this.reportPrintingError("UNWRITEABLE_MARGIN"); + throw e; + } + margins.QueryInterface(Ci.nsIPaperMargin); + + // margin dimensions are given on the paper in points, setting values need to be in inches + paperWrapper.unwriteableMarginTop = margins.top * INCHES_PER_POINT; + paperWrapper.unwriteableMarginRight = margins.right * INCHES_PER_POINT; + paperWrapper.unwriteableMarginBottom = margins.bottom * INCHES_PER_POINT; + paperWrapper.unwriteableMarginLeft = margins.left * INCHES_PER_POINT; + // No need to re-resolve static properties + paperWrapper._resolved = true; + return paperWrapper; + }, + + async resolvePropertiesForPrinter(printerName) { + // resolve any async properties we need on the printer + let printerInfo = this.availablePrinters[printerName]; + if (printerInfo._resolved) { + // Store a convenience reference + this.availablePaperSizes = printerInfo.availablePaperSizes; + return printerInfo; + } + + // Await the async printer data. + if (printerInfo.printer) { + let basePrinterInfo; + try { + [ + printerInfo.supportsDuplex, + printerInfo.supportsColor, + printerInfo.supportsMonochrome, + basePrinterInfo, + ] = await Promise.all([ + printerInfo.printer.supportsDuplex, + printerInfo.printer.supportsColor, + printerInfo.printer.supportsMonochrome, + printerInfo.printer.printerInfo, + ]); + } catch (e) { + this.reportPrintingError("PRINTER_SETTINGS"); + throw e; + } + basePrinterInfo.QueryInterface(Ci.nsIPrinterInfo); + basePrinterInfo.defaultSettings.QueryInterface(Ci.nsIPrintSettings); + + printerInfo.paperList = basePrinterInfo.paperList; + printerInfo.defaultSettings = basePrinterInfo.defaultSettings; + } else if (printerName == PrintUtils.SAVE_TO_PDF_PRINTER) { + // The Mozilla PDF pseudo-printer has no actual nsIPrinter implementation + printerInfo.defaultSettings = PSSVC.createNewPrintSettings(); + printerInfo.defaultSettings.printerName = printerName; + printerInfo.defaultSettings.toFileName = ""; + printerInfo.defaultSettings.outputFormat = + Ci.nsIPrintSettings.kOutputFormatPDF; + printerInfo.defaultSettings.outputDestination = + Ci.nsIPrintSettings.kOutputDestinationFile; + printerInfo.defaultSettings.usePageRuleSizeAsPaperSize = + Services.prefs.getBoolPref( + "print.save_as_pdf.use_page_rule_size_as_paper_size.enabled", + false + ); + printerInfo.paperList = this.fallbackPaperList; + } + printerInfo.settings = printerInfo.defaultSettings.clone(); + // Apply any previously persisted user values + // Don't apply kInitSavePrintToFile though, that should only be true for + // the PDF printer. + printerInfo.settings.outputDestination = + printerName == PrintUtils.SAVE_TO_PDF_PRINTER + ? Ci.nsIPrintSettings.kOutputDestinationFile + : Ci.nsIPrintSettings.kOutputDestinationPrinter; + let flags = + printerInfo.settings.kInitSaveAll ^ + printerInfo.settings.kInitSavePrintToFile; + PSSVC.initPrintSettingsFromPrefs(printerInfo.settings, true, flags); + // We set `isInitializedFromPrinter` to make sure that that's set on the + // SAVE_TO_PDF_PRINTER settings. The naming is poor, but that tells the + // platform code that the settings object is complete. + printerInfo.settings.isInitializedFromPrinter = true; + + printerInfo.settings.toFileName = ""; + + // prepare the available paper sizes for this printer + if (!printerInfo.paperList?.length) { + logger.warn( + "Printer has empty paperList: ", + printerInfo.printer.id, + "using fallbackPaperList" + ); + printerInfo.paperList = this.fallbackPaperList; + } + // don't trust the settings to provide valid paperSizeUnit values + let sizeUnit = + printerInfo.settings.paperSizeUnit == + printerInfo.settings.kPaperSizeMillimeters + ? printerInfo.settings.kPaperSizeMillimeters + : printerInfo.settings.kPaperSizeInches; + let papersById = (printerInfo.availablePaperSizes = {}); + // Store a convenience reference + this.availablePaperSizes = papersById; + + for (let paper of printerInfo.paperList) { + paper.QueryInterface(Ci.nsIPaper); + // Bug 1662239: I'm seeing multiple duplicate entries for each paper size + // so ensure we have one entry per name + if (!papersById[paper.id]) { + papersById[paper.id] = { + paper, + id: paper.id, + name: paper.name, + // XXXsfoster: Eventually we want to get the unit from the nsIPaper object + sizeUnit, + }; + } + } + // Update our cache of all the paper sizes by name + Object.assign(PrintEventHandler.allPaperSizes, papersById); + + // The printer properties don't change, mark this as resolved for next time + printerInfo._resolved = true; + logger.debug("Resolved printerInfo:", printerInfo); + return printerInfo; + }, + + get(target, name) { + switch (name) { + case "currentPaper": { + let paperId = this.get(target, "paperId"); + return paperId && this.availablePaperSizes[paperId]; + } + + case "marginPresets": + let paperWrapper = this.get(target, "currentPaper"); + return { + none: PrintEventHandler.getMarginPresets("none", paperWrapper), + minimum: PrintEventHandler.getMarginPresets("minimum", paperWrapper), + default: PrintEventHandler.getMarginPresets("default", paperWrapper), + custom: PrintEventHandler.getMarginPresets("custom", paperWrapper), + }; + + case "marginOptions": { + let allMarginPresets = this.get(target, "marginPresets"); + let uniqueMargins = new Set(); + let marginsEnabled = {}; + for (let name of ["none", "default", "minimum", "custom"]) { + let { marginTop, marginLeft, marginBottom, marginRight } = + allMarginPresets[name]; + let key = [marginTop, marginLeft, marginBottom, marginRight].join( + "," + ); + // Custom margins are initialized to default margins + marginsEnabled[name] = !uniqueMargins.has(key) || name == "custom"; + uniqueMargins.add(key); + } + return marginsEnabled; + } + + case "margins": + let marginSettings = { + marginTop: target.marginTop, + marginLeft: target.marginLeft, + marginBottom: target.marginBottom, + marginRight: target.marginRight, + }; + // see if they match the none, minimum, or default margin values + let allMarginPresets = this.get(target, "marginPresets"); + const marginsMatch = function (lhs, rhs) { + return Object.keys(marginSettings).every( + name => lhs[name].toFixed(2) == rhs[name].toFixed(2) + ); + }; + const potentialPresets = (function () { + let presets = []; + const minimumIsNone = marginsMatch( + allMarginPresets.none, + allMarginPresets.minimum + ); + // We only attempt to match the serialized values against the "none" + // preset if the unwriteable margins are being ignored or are zero. + if (target.ignoreUnwriteableMargins || minimumIsNone) { + presets.push("none"); + } + if (!minimumIsNone) { + presets.push("minimum"); + } + presets.push("default"); + return presets; + })(); + for (let presetName of potentialPresets) { + let marginPresets = allMarginPresets[presetName]; + if (marginsMatch(marginSettings, marginPresets)) { + return presetName; + } + } + + // Fall back to custom for other values + return "custom"; + + case "defaultMargins": + return PrintEventHandler.getMarginPresets( + "default", + this.get(target, "currentPaper") + ); + + case "customMargins": + return PrintEventHandler.getMarginPresets( + "custom", + this.get(target, "currentPaper") + ); + + case "paperSizes": + return Object.values(this.availablePaperSizes) + .sort((a, b) => a.name.localeCompare(b.name)) + .map(paper => { + return { + name: paper.name, + value: paper.id, + }; + }); + + case "supportsDuplex": + return this.availablePrinters[target.printerName].supportsDuplex; + + case "printDuplex": + switch (target.duplex) { + case Ci.nsIPrintSettings.kDuplexNone: + break; + case Ci.nsIPrintSettings.kDuplexFlipOnLongEdge: + return "long-edge"; + case Ci.nsIPrintSettings.kDuplexFlipOnShortEdge: + return "short-edge"; + default: + logger.warn("Unexpected duplex value: ", target.duplex); + } + return "off"; + case "printBackgrounds": + return target.printBGImages || target.printBGColors; + + case "printFootersHeaders": + // if any of the footer and headers settings have a non-empty string value + // we consider that "enabled" + return Object.keys(this.headerFooterSettingsPrefs).some( + name => !!target[name] + ); + + case "supportsColor": + return this.availablePrinters[target.printerName].supportsColor; + + case "willSaveToFile": + return ( + target.outputFormat == Ci.nsIPrintSettings.kOutputFormatPDF || + this.knownSaveToFilePrinters.has(target.printerName) + ); + case "supportsMonochrome": + return this.availablePrinters[target.printerName].supportsMonochrome; + case "defaultSystemPrinter": + return ( + this.defaultSystemPrinter?.value || + Object.getOwnPropertyNames(this.availablePrinters).find( + name => name != PrintUtils.SAVE_TO_PDF_PRINTER + ) + ); + + case "numCopies": + return this.get(target, "willSaveToFile") ? 1 : target.numCopies; + case "sourceVersion": + return this._sourceVersion; + } + return target[name]; + }, + + set(target, name, value) { + switch (name) { + case "margins": + if (!["default", "minimum", "none", "custom"].includes(value)) { + logger.warn("Unexpected margin preset name: ", value); + value = "default"; + } + let paperWrapper = this.get(target, "currentPaper"); + let marginPresets = PrintEventHandler.getMarginPresets( + value, + paperWrapper + ); + for (let [settingName, presetValue] of Object.entries(marginPresets)) { + target[settingName] = presetValue; + } + target.honorPageRuleMargins = value == "default"; + target.ignoreUnwriteableMargins = value == "none"; + break; + + case "paperId": { + let paperId = value; + let paperWrapper = this.availablePaperSizes[paperId]; + // Dimensions on the paper object are in pts. + // We convert to the printer's specified unit when updating settings + let unitsPerPoint = + paperWrapper.sizeUnit == target.kPaperSizeMillimeters + ? MM_PER_POINT + : INCHES_PER_POINT; + // paperWidth and paperHeight are calculated values that we always treat as suspect and + // re-calculate whenever the paperId changes + target.paperSizeUnit = paperWrapper.sizeUnit; + target.paperWidth = paperWrapper.paper.width * unitsPerPoint; + target.paperHeight = paperWrapper.paper.height * unitsPerPoint; + // Unwriteable margins were pre-calculated from their async values when the paper size + // was selected. They are always in inches + target.unwriteableMarginTop = paperWrapper.unwriteableMarginTop; + target.unwriteableMarginRight = paperWrapper.unwriteableMarginRight; + target.unwriteableMarginBottom = paperWrapper.unwriteableMarginBottom; + target.unwriteableMarginLeft = paperWrapper.unwriteableMarginLeft; + target.paperId = paperWrapper.paper.id; + // pull new margin values for the new paper size + this.set(target, "margins", this.get(target, "margins")); + break; + } + + case "printerName": + // Can't set printerName, settings objects belong to a specific printer. + break; + + case "printBackgrounds": + target.printBGImages = value; + target.printBGColors = value; + break; + + case "printDuplex": { + let duplex = (function () { + switch (value) { + case "off": + break; + case "long-edge": + return Ci.nsIPrintSettings.kDuplexFlipOnLongEdge; + case "short-edge": + return Ci.nsIPrintSettings.kDuplexFlipOnShortEdge; + default: + logger.warn("Unexpected duplex name: ", value); + } + return Ci.nsIPrintSettings.kDuplexNone; + })(); + + target.duplex = duplex; + break; + } + + case "printFootersHeaders": + // To disable header & footers, set them all to empty. + // To enable, restore default values for each of the header & footer settings. + for (let [settingName, defaultValue] of Object.entries( + this.defaultHeadersAndFooterValues + )) { + target[settingName] = value ? defaultValue : ""; + } + break; + + case "customMargins": + if (value != null) { + for (let [settingName, newVal] of Object.entries(value)) { + target[settingName] = newVal; + this._lastCustomMarginValues[settingName] = newVal; + } + } + break; + + case "customMarginTop": + case "customMarginBottom": + case "customMarginLeft": + case "customMarginRight": + let customMarginName = "margin" + name.substring(12); + this.set( + target, + "customMargins", + Object.assign({}, this.get(target, "customMargins"), { + [customMarginName]: value, + }) + ); + break; + + case "sourceVersion": + this._sourceVersion = value; + this.set(target, "printSelectionOnly", value == "selection"); + if (value == "simplified") { + this.set(target, "printBackgrounds", false); + } + break; + + default: + target[name] = value; + } + }, +}; + +/* + * Custom elements ---------------------------------------------------- + */ + +function PrintUIControlMixin(superClass) { + return class PrintUIControl extends superClass { + connectedCallback() { + this.setAttribute("autocomplete", "off"); + this.initialize(); + this.render(); + } + + initialize() { + if (this._initialized) { + return; + } + this._initialized = true; + if (this.templateId) { + let template = this.ownerDocument.getElementById(this.templateId); + let templateContent = template.content; + this.appendChild(templateContent.cloneNode(true)); + } + + document.addEventListener("print-settings", ({ detail: settings }) => { + this.update(settings); + }); + + this.addEventListener("input", this); + } + + render() {} + + update(settings) {} + + dispatchSettingsChange(changedSettings) { + this.dispatchEvent( + new CustomEvent("update-print-settings", { + bubbles: true, + detail: changedSettings, + }) + ); + } + + cancelSettingsChange(changedSettings) { + this.dispatchEvent( + new CustomEvent("cancel-print-settings", { + bubbles: true, + detail: changedSettings, + }) + ); + } + + handleEvent(event) {} + }; +} + +class PrintUIForm extends PrintUIControlMixin(HTMLFormElement) { + initialize() { + super.initialize(); + + this.addEventListener("submit", this); + this.addEventListener("click", this); + this.addEventListener("revalidate", this); + + this._printerDestination = this.querySelector("#destination"); + this.printButton = this.querySelector("#print-button"); + } + + removeNonPdfSettings() { + let selectors = ["#backgrounds", "#source-version-selection"]; + for (let selector of selectors) { + this.querySelector(selector).remove(); + } + let moreSettings = this.querySelector("#more-settings-options"); + if (moreSettings.children.length <= 1) { + moreSettings.remove(); + } + } + + requestPrint() { + this.requestSubmit(this.printButton); + } + + update(settings) { + // If there are no default system printers available and we are not on mac, + // we should hide the system dialog because it won't be populated with + // the correct settings. Mac and Gtk support save to pdf functionality + // in the native dialog, so it can be shown regardless. + this.querySelector("#system-print").hidden = + AppConstants.platform === "win" && !settings.defaultSystemPrinter; + + this.querySelector("#two-sided-printing").hidden = !settings.supportsDuplex; + } + + enable() { + let isValid = this.checkValidity(); + document.body.toggleAttribute("invalid", !isValid); + if (isValid) { + for (let element of this.elements) { + if (!element.hasAttribute("disallowed")) { + element.disabled = false; + } + } + // aria-describedby will usually cause the first value to be reported. + // Unfortunately, screen readers don't pick up description changes from + // dialogs, so we must use a live region. To avoid double reporting of + // the first value, we don't set aria-live initially. We only set it for + // subsequent updates. + // aria-live is set on the parent because sheetCount itself might be + // hidden and then shown, and updates are only reported for live + // regions that were already visible. + document + .querySelector("#sheet-count") + .parentNode.setAttribute("aria-live", "polite"); + } else { + // Find the invalid element + let invalidElement; + for (let element of this.elements) { + if (!element.checkValidity()) { + invalidElement = element; + break; + } + } + let section = invalidElement.closest(".section-block"); + document.body.toggleAttribute("invalid", !isValid); + // We're hiding the sheet count and aria-describedby includes the + // content of hidden elements, so remove aria-describedby. + document.body.removeAttribute("aria-describedby"); + for (let element of this.elements) { + // If we're valid, enable all inputs. + // Otherwise, disable the valid inputs other than the cancel button and the elements + // in the invalid section. + element.disabled = + element.hasAttribute("disallowed") || + (!isValid && + element.validity.valid && + element.name != "cancel" && + element.closest(".section-block") != this._printerDestination && + element.closest(".section-block") != section); + } + } + } + + disable(filterFn) { + for (let element of this.elements) { + if (filterFn && !filterFn(element)) { + continue; + } + element.disabled = element.name != "cancel"; + } + } + + handleEvent(e) { + if (e.target.id == "open-dialog-link") { + this.dispatchEvent(new Event("open-system-dialog", { bubbles: true })); + return; + } + + if (e.type == "submit") { + e.preventDefault(); + if (e.submitter.name == "print" && this.checkValidity()) { + this.dispatchEvent(new Event("print", { bubbles: true })); + } + } else if ( + (e.type == "input" || e.type == "revalidate") && + !this.printerChanging + ) { + this.enable(); + } + } +} +customElements.define("print-form", PrintUIForm, { extends: "form" }); + +class PrintSettingSelect extends PrintUIControlMixin(HTMLSelectElement) { + initialize() { + super.initialize(); + this.addEventListener("keypress", this); + } + + connectedCallback() { + this.settingName = this.dataset.settingName; + super.connectedCallback(); + } + + setOptions(optionValues = []) { + this.textContent = ""; + for (let optionData of optionValues) { + let opt = new Option( + optionData.name, + "value" in optionData ? optionData.value : optionData.name + ); + if (optionData.nameId) { + document.l10n.setAttributes(opt, optionData.nameId); + } + // option selectedness is set via update() and assignment to this.value + this.options.add(opt); + } + } + + update(settings) { + if (this.settingName) { + this.value = settings[this.settingName]; + } + } + + handleEvent(e) { + if (e.type == "input" && this.settingName) { + this.dispatchSettingsChange({ + [this.settingName]: e.target.value, + }); + } else if (e.type == "keypress") { + if ( + e.key == "Enter" && + (!e.metaKey || AppConstants.platform == "macosx") + ) { + this.form.requestPrint(); + } + } + } +} +customElements.define("setting-select", PrintSettingSelect, { + extends: "select", +}); + +class PrintSettingNumber extends PrintUIControlMixin(HTMLInputElement) { + initialize() { + super.initialize(); + this.addEventListener("beforeinput", e => this.preventWhitespaceEntry(e)); + this.addEventListener("paste", e => this.pasteWithoutWhitespace(e)); + } + + connectedCallback() { + this.type = "number"; + this.settingName = this.dataset.settingName; + super.connectedCallback(); + } + + update(settings) { + if (this.settingName) { + this.value = settings[this.settingName]; + } + } + + preventWhitespaceEntry(e) { + if (e.data && !e.data.trim().length) { + e.preventDefault(); + } + } + + pasteWithoutWhitespace(e) { + // Prevent original value from being pasted + e.preventDefault(); + + // Manually update input's value with sanitized clipboard data + let paste = (e.clipboardData || window.clipboardData) + .getData("text") + .trim(); + this.value = paste; + } + + handleEvent(e) { + switch (e.type) { + case "input": + if (this.settingName && this.checkValidity()) { + this.dispatchSettingsChange({ + [this.settingName]: this.value, + }); + } + break; + } + } +} +customElements.define("setting-number", PrintSettingNumber, { + extends: "input", +}); + +class PrintSettingCheckbox extends PrintUIControlMixin(HTMLInputElement) { + connectedCallback() { + this.type = "checkbox"; + this.settingName = this.dataset.settingName; + super.connectedCallback(); + } + + update(settings) { + this.checked = settings[this.settingName]; + } + + handleEvent(e) { + this.dispatchSettingsChange({ + [this.settingName]: this.checked, + }); + } +} +customElements.define("setting-checkbox", PrintSettingCheckbox, { + extends: "input", +}); + +class PrintSettingRadio extends PrintUIControlMixin(HTMLInputElement) { + connectedCallback() { + this.type = "radio"; + this.settingName = this.dataset.settingName; + super.connectedCallback(); + } + + update(settings) { + this.checked = settings[this.settingName] == this.value; + } + + handleEvent(e) { + this.dispatchSettingsChange({ + [this.settingName]: this.value, + }); + } +} +customElements.define("setting-radio", PrintSettingRadio, { + extends: "input", +}); + +class DestinationPicker extends PrintSettingSelect { + initialize() { + super.initialize(); + document.addEventListener("available-destinations", this); + } + + update(settings) { + super.update(settings); + let isPdf = settings.outputFormat == Ci.nsIPrintSettings.kOutputFormatPDF; + this.setAttribute("output", isPdf ? "pdf" : "paper"); + } + + handleEvent(e) { + super.handleEvent(e); + + if (e.type == "available-destinations") { + this.setOptions(e.detail); + } + } +} +customElements.define("destination-picker", DestinationPicker, { + extends: "select", +}); + +class ColorModePicker extends PrintSettingSelect { + update(settings) { + this.value = settings[this.settingName] ? "color" : "bw"; + let canSwitch = settings.supportsColor && settings.supportsMonochrome; + if (this.disablePicker != canSwitch) { + this.toggleAttribute("disallowed", !canSwitch); + this.disabled = !canSwitch; + } + this.disablePicker = canSwitch; + } + + handleEvent(e) { + if (e.type == "input") { + // turn our string value into the expected boolean + this.dispatchSettingsChange({ + [this.settingName]: this.value == "color", + }); + } + } +} +customElements.define("color-mode-select", ColorModePicker, { + extends: "select", +}); + +class PaperSizePicker extends PrintSettingSelect { + initialize() { + super.initialize(); + this._printerName = null; + this._section = this.closest(".section-block"); + document.addEventListener("hide-paper-size", this); + } + + update(settings) { + if (settings.printerName !== this._printerName) { + this._printerName = settings.printerName; + this.setOptions(settings.paperSizes); + } + this.value = settings.paperId; + + // Unhide the paper-size picker, if we've stopped using the page size as paper-size. + if (this._section.hidden && !settings.usePageRuleSizeAsPaperSize) { + this._section.hidden = false; + } + } + + handleEvent(e) { + super.handleEvent(e); + const { type } = e; + if (type == "hide-paper-size") { + this._section.hidden = true; + } + } +} +customElements.define("paper-size-select", PaperSizePicker, { + extends: "select", +}); + +class OrientationInput extends PrintUIControlMixin(HTMLElement) { + initialize() { + super.initialize(); + document.addEventListener("hide-orientation", this); + } + + get templateId() { + return "orientation-template"; + } + + update(settings) { + for (let input of this.querySelectorAll("input")) { + input.checked = settings.orientation == input.value; + } + } + + handleEvent(e) { + if (e.type == "hide-orientation") { + document.getElementById("orientation").hidden = true; + return; + } + this.dispatchSettingsChange({ + orientation: e.target.value, + }); + } +} +customElements.define("orientation-input", OrientationInput); + +class CopiesInput extends PrintUIControlMixin(HTMLElement) { + get templateId() { + return "copy-template"; + } + + initialize() { + super.initialize(); + this._copiesSection = this.closest(".section-block"); + this._copiesInput = this.querySelector("#copies-count"); + this._copiesError = this.querySelector("#error-invalid-copies"); + } + + update(settings) { + this._copiesSection.hidden = settings.willSaveToFile; + this._copiesError.hidden = true; + } + + handleEvent(e) { + this._copiesError.hidden = this._copiesInput.checkValidity(); + } +} +customElements.define("copy-count-input", CopiesInput); + +class ScaleInput extends PrintUIControlMixin(HTMLElement) { + get templateId() { + return "scale-template"; + } + + initialize() { + super.initialize(); + + this._percentScale = this.querySelector("#percent-scale"); + this._shrinkToFitChoice = this.querySelector("#fit-choice"); + this._scaleChoice = this.querySelector("#percent-scale-choice"); + this._scaleError = this.querySelector("#error-invalid-scale"); + } + + updateScale() { + this.dispatchSettingsChange({ + scaling: Number(this._percentScale.value / 100), + }); + } + + update(settings) { + let { scaling, shrinkToFit, printerName } = settings; + this._shrinkToFitChoice.checked = shrinkToFit; + this._scaleChoice.checked = !shrinkToFit; + if (this.disableScale != shrinkToFit) { + this._percentScale.disabled = shrinkToFit; + this._percentScale.toggleAttribute("disallowed", shrinkToFit); + } + this.disableScale = shrinkToFit; + if (!this.printerName) { + this.printerName = printerName; + } + + // If the user had an invalid input and switches back to "fit to page", + // we repopulate the scale field with the stored, valid scaling value. + let isValid = this._percentScale.checkValidity(); + if ( + !this._percentScale.value || + (this._shrinkToFitChoice.checked && !isValid) || + (this.printerName != printerName && !isValid) + ) { + // Only allow whole numbers. 0.14 * 100 would have decimal places, etc. + this._percentScale.value = parseInt(scaling * 100, 10); + this.printerName = printerName; + if (!isValid) { + this.dispatchEvent(new Event("revalidate", { bubbles: true })); + this._scaleError.hidden = true; + } + } + } + + handleEvent(e) { + if (e.target == this._shrinkToFitChoice || e.target == this._scaleChoice) { + if (!this._percentScale.checkValidity()) { + this._percentScale.value = 100; + } + let scale = + e.target == this._shrinkToFitChoice + ? 1 + : Number(this._percentScale.value / 100); + this.dispatchSettingsChange({ + shrinkToFit: this._shrinkToFitChoice.checked, + scaling: scale, + }); + this._scaleError.hidden = true; + } else if (e.type == "input") { + if (this._percentScale.checkValidity()) { + this.updateScale(); + } + } + + window.clearTimeout(this.showErrorTimeoutId); + if (this._percentScale.validity.valid) { + this._scaleError.hidden = true; + } else { + this.cancelSettingsChange({ scaling: true }); + this.showErrorTimeoutId = window.setTimeout(() => { + this._scaleError.hidden = false; + }, INPUT_DELAY_MS); + } + } +} +customElements.define("scale-input", ScaleInput); + +class PageRangeInput extends PrintUIControlMixin(HTMLElement) { + initialize() { + super.initialize(); + + this._rangeInput = this.querySelector("#custom-range"); + this._rangeInput.title = ""; + this._rangePicker = this.querySelector("#range-picker"); + this._rangePickerEvenOption = this._rangePicker.namedItem("even"); + this._rangeError = this.querySelector("#error-invalid-range"); + this._startRangeOverflowError = this.querySelector( + "#error-invalid-start-range-overflow" + ); + + this._pagesSet = new Set(); + + this.addEventListener("keypress", this); + this.addEventListener("paste", this); + document.addEventListener("page-count", this); + } + + get templateId() { + return "page-range-template"; + } + + updatePageRange() { + let isCustom = this._rangePicker.value == "custom"; + let isCurrent = this._rangePicker.value == "current"; + + if (!isCurrent) { + this._currentPage = null; + } + + if (isCustom) { + this.validateRangeInput(); + } else if (isCurrent) { + this._currentPage = this._rangeInput.value = + this._currentPage || this.getCurrentVisiblePageNumber(); + this.validateRangeInput(); + } else { + this._pagesSet.clear(); + + if (this._rangePicker.value == "odd") { + for (let i = 1; i <= this._numPages; i += 2) { + this._pagesSet.add(i); + } + } else if (this._rangePicker.value == "even") { + for (let i = 2; i <= this._numPages; i += 2) { + this._pagesSet.add(i); + } + } + + if (!this._rangeInput.checkValidity()) { + this._rangeInput.setCustomValidity(""); + this._rangeInput.value = ""; + } + } + + this.dispatchEvent(new Event("revalidate", { bubbles: true })); + + document.l10n.setAttributes( + this._rangeError, + "printui-error-invalid-range", + { + numPages: this._numPages, + } + ); + + // If it's valid, update the page range and hide the error messages. + // Otherwise, set the appropriate error message + if (this._rangeInput.validity.valid || !isCustom) { + window.clearTimeout(this.showErrorTimeoutId); + this._startRangeOverflowError.hidden = this._rangeError.hidden = true; + } else { + this._rangeInput.focus(); + } + } + + dispatchPageRange(shouldCancel = true) { + window.clearTimeout(this.showErrorTimeoutId); + if ( + this._rangeInput.validity.valid || + this._rangePicker.value != "custom" + ) { + this.dispatchSettingsChange({ + pageRanges: this.formatPageRange(), + }); + } else { + if (shouldCancel) { + this.cancelSettingsChange({ pageRanges: true }); + } + this.showErrorTimeoutId = window.setTimeout(() => { + this._rangeError.hidden = + this._rangeInput.validationMessage != "invalid"; + this._startRangeOverflowError.hidden = + this._rangeInput.validationMessage != "startRangeOverflow"; + }, INPUT_DELAY_MS); + } + } + + // The platform expects pageRanges to be an array of + // ranges represented by ints. + // Ex: Printing pages 1-3 would return [1,3] + // Ex: Printing page 1 would return [1,1] + // Ex: Printing pages 1-2,4 would return [1,2,4,4] + formatPageRange() { + if ( + this._pagesSet.size == 0 || + (this._rangePicker.value == "custom" && this._rangeInput.value == "") || + this._rangePicker.value == "all" + ) { + // Show all pages. + return []; + } + let pages = Array.from(this._pagesSet).sort((a, b) => a - b); + + let formattedRanges = []; + let startRange = pages[0]; + let endRange = pages[0]; + formattedRanges.push(startRange); + + for (let i = 1; i < pages.length; i++) { + let currentPage = pages[i - 1]; + let nextPage = pages[i]; + if (nextPage > currentPage + 1) { + formattedRanges.push(endRange); + startRange = endRange = nextPage; + formattedRanges.push(startRange); + } else { + endRange = nextPage; + } + } + formattedRanges.push(endRange); + + return formattedRanges; + } + + update(settings) { + let { pageRanges, printerName } = settings; + + this.toggleAttribute("all-pages", !pageRanges.length); + if (!this.printerName) { + this.printerName = printerName; + } + + let isValid = this._rangeInput.checkValidity(); + + if (this.printerName != printerName && !isValid) { + this.printerName = printerName; + this._rangeInput.value = ""; + this.updatePageRange(); + this.dispatchPageRange(); + } + } + + handleKeypress(e) { + let char = String.fromCharCode(e.charCode); + let acceptedChar = char.match(/^[0-9,-]$/); + if (!acceptedChar && !char.match("\x00") && !e.ctrlKey && !e.metaKey) { + e.preventDefault(); + } + } + + handlePaste(e) { + let paste = (e.clipboardData || window.clipboardData) + .getData("text") + .trim(); + if (!paste.match(/^[0-9,-]*$/)) { + e.preventDefault(); + } + } + + // This method has been abstracted into a helper for testing purposes + _validateRangeInput(value, numPages) { + this._pagesSet.clear(); + var ranges = value.split(","); + + for (let range of ranges) { + let rangeParts = range.split("-"); + if (rangeParts.length > 2) { + this._rangeInput.setCustomValidity("invalid"); + this._rangeInput.title = ""; + this._pagesSet.clear(); + return; + } + let startRange = parseInt(rangeParts[0], 10); + let endRange = parseInt( + rangeParts.length == 2 ? rangeParts[1] : rangeParts[0], + 10 + ); + + if (isNaN(startRange) && isNaN(endRange)) { + continue; + } + + // If the startRange was not specified, then we infer this + // to be 1. + if (isNaN(startRange) && rangeParts[0] == "") { + startRange = 1; + } + // If the end range was not specified, then we infer this + // to be the total number of pages. + if (isNaN(endRange) && rangeParts[1] == "") { + endRange = numPages; + } + + // Check the range for errors + if (endRange < startRange) { + this._rangeInput.setCustomValidity("startRangeOverflow"); + this._pagesSet.clear(); + return; + } else if ( + startRange > numPages || + endRange > numPages || + startRange == 0 + ) { + this._rangeInput.setCustomValidity("invalid"); + this._rangeInput.title = ""; + this._pagesSet.clear(); + return; + } + + for (let i = startRange; i <= endRange; i++) { + this._pagesSet.add(i); + } + } + + this._rangeInput.setCustomValidity(""); + } + + validateRangeInput() { + let value = ["custom", "current"].includes(this._rangePicker.value) + ? this._rangeInput.value + : ""; + this._validateRangeInput(value, this._numPages); + } + + getCurrentVisiblePageNumber() { + let pageNum = parseInt( + PrintEventHandler.printPreviewEl.lastPreviewBrowser.getAttribute( + "current-page" + ) + ); + return isNaN(pageNum) ? 1 : pageNum; + } + + handleEvent(e) { + if (e.type == "keypress") { + if (e.target == this._rangeInput) { + this.handleKeypress(e); + } + return; + } + + if (e.type === "paste" && e.target == this._rangeInput) { + this.handlePaste(e); + return; + } + + if (e.type == "page-count") { + let { totalPages } = e.detail; + // This means we have already handled the page count event + // and do not need to dispatch another event. + if (this._numPages == totalPages) { + return; + } + + this._numPages = totalPages; + this._rangeInput.disabled = false; + this._rangePickerEvenOption.disabled = this._numPages < 2; + + let prevPages = Array.from(this._pagesSet); + this.updatePageRange(); + if ( + prevPages.length != this._pagesSet.size || + !prevPages.every(page => this._pagesSet.has(page)) + ) { + // If the calculated set of pages has changed then we need to dispatch + // a new pageRanges setting :( + // Ideally this would be resolved in the settings code since it should + // only happen for the "N-" case where pages N through the end of the + // document are in the range. + this.dispatchPageRange(false); + } + + return; + } + + if (e.target == this._rangePicker) { + this._rangeInput.hidden = e.target.value != "custom"; + this.updatePageRange(); + this.dispatchPageRange(); + if (!this._rangeInput.hidden) { + this._rangeInput.select(); + } + } else if (e.target == this._rangeInput) { + this._rangeInput.focus(); + if (this._numPages) { + this.updatePageRange(); + this.dispatchPageRange(); + } + } + } +} +customElements.define("page-range-input", PageRangeInput); + +class MarginsPicker extends PrintUIControlMixin(HTMLElement) { + initialize() { + super.initialize(); + + this._marginPicker = this.querySelector("#margins-picker"); + this._customTopMargin = this.querySelector("#custom-margin-top"); + this._customBottomMargin = this.querySelector("#custom-margin-bottom"); + this._customLeftMargin = this.querySelector("#custom-margin-left"); + this._customRightMargin = this.querySelector("#custom-margin-right"); + this._marginError = this.querySelector("#error-invalid-margin"); + this._sizeUnit = null; + this._toInchesMultiplier = 1; + } + + get templateId() { + return "margins-template"; + } + + updateCustomMargins() { + let newMargins = { + marginTop: this.toInchValue(this._customTopMargin.value), + marginBottom: this.toInchValue(this._customBottomMargin.value), + marginLeft: this.toInchValue(this._customLeftMargin.value), + marginRight: this.toInchValue(this._customRightMargin.value), + }; + + this.dispatchSettingsChange({ + margins: "custom", + customMargins: newMargins, + }); + this._marginError.hidden = true; + } + + updateMaxValues() { + let maxWidth = this.toCurrentUnitValue(this._maxWidth); + let maxHeight = this.toCurrentUnitValue(this._maxHeight); + this._customTopMargin.max = this.formatMaxAttr( + maxHeight - this._customBottomMargin.value + ); + this._customBottomMargin.max = this.formatMaxAttr( + maxHeight - this._customTopMargin.value + ); + this._customLeftMargin.max = this.formatMaxAttr( + maxWidth - this._customRightMargin.value + ); + this._customRightMargin.max = this.formatMaxAttr( + maxWidth - this._customLeftMargin.value + ); + } + + truncateTwoDecimals(val) { + if (val.split(".")[1].length > 2) { + let dotIndex = val.indexOf("."); + return val.slice(0, dotIndex + 3); + } + return val; + } + + formatMaxAttr(val) { + const strVal = val.toString(); + if (strVal.includes(".")) { + return this.truncateTwoDecimals(strVal); + } + return val; + } + + formatMargin(target) { + if (target.value.includes(".")) { + target.value = this.truncateTwoDecimals(target.value); + } + } + + toCurrentUnitValue(val) { + if (typeof val == "string") { + val = parseFloat(val); + } + return val / this._toInchesMultiplier; + } + + toInchValue(val) { + if (typeof val == "string") { + val = parseFloat(val); + } + return val * this._toInchesMultiplier; + } + + setAllMarginValues(settings) { + this._customTopMargin.value = this.toCurrentUnitValue( + settings.customMargins.marginTop + ).toFixed(2); + this._customBottomMargin.value = this.toCurrentUnitValue( + settings.customMargins.marginBottom + ).toFixed(2); + this._customLeftMargin.value = this.toCurrentUnitValue( + settings.customMargins.marginLeft + ).toFixed(2); + this._customRightMargin.value = this.toCurrentUnitValue( + settings.customMargins.marginRight + ).toFixed(2); + } + + update(settings) { + // Re-evaluate which margin options should be enabled whenever the printer or paper changes + this._toInchesMultiplier = + settings.paperSizeUnit == settings.kPaperSizeMillimeters + ? INCHES_PER_MM + : 1; + if ( + settings.paperId !== this._paperId || + settings.printerName !== this._printerName || + settings.orientation !== this._orientation + ) { + let enabledMargins = settings.marginOptions; + for (let option of this._marginPicker.options) { + option.hidden = !enabledMargins[option.value]; + } + this._paperId = settings.paperId; + this._printerName = settings.printerName; + this._orientation = settings.orientation; + + // Paper dimensions are in the paperSizeUnit. As the margin values are in inches + // we'll normalize to that when storing max dimensions + let height = + this._orientation == 0 ? settings.paperHeight : settings.paperWidth; + let width = + this._orientation == 0 ? settings.paperWidth : settings.paperHeight; + let heightInches = + Math.round(this._toInchesMultiplier * height * 100) / 100; + let widthInches = + Math.round(this._toInchesMultiplier * width * 100) / 100; + + this._maxHeight = + heightInches - + settings.unwriteableMarginTop - + settings.unwriteableMarginBottom; + this._maxWidth = + widthInches - + settings.unwriteableMarginLeft - + settings.unwriteableMarginRight; + + // The values in custom fields should be initialized to custom margin values + // and must be overriden if they are no longer valid. + this.setAllMarginValues(settings); + this.updateMaxValues(); + this.dispatchEvent(new Event("revalidate", { bubbles: true })); + this._marginError.hidden = true; + } + + if (settings.paperSizeUnit !== this._sizeUnit) { + this._sizeUnit = settings.paperSizeUnit; + let unitStr = + this._sizeUnit == settings.kPaperSizeMillimeters ? "mm" : "inches"; + for (let elem of this.querySelectorAll("[data-unit-prefix-l10n-id]")) { + let l10nId = elem.getAttribute("data-unit-prefix-l10n-id") + unitStr; + elem.setAttribute("data-l10n-id", l10nId); + } + } + + // We need to ensure we don't override the value if the value should be custom. + if (this._marginPicker.value != "custom") { + // Reset the custom margin values if they are not valid and revalidate the form + if ( + !this._customTopMargin.checkValidity() || + !this._customBottomMargin.checkValidity() || + !this._customLeftMargin.checkValidity() || + !this._customRightMargin.checkValidity() + ) { + window.clearTimeout(this.showErrorTimeoutId); + this.setAllMarginValues(settings); + this.updateMaxValues(); + this.dispatchEvent(new Event("revalidate", { bubbles: true })); + this._marginError.hidden = true; + } + if (settings.margins == "custom") { + // Ensure that we display the custom margin boxes + this.querySelector(".margin-group").hidden = false; + } + this._marginPicker.value = settings.margins; + } + } + + handleEvent(e) { + if (e.target == this._marginPicker) { + let customMargin = e.target.value == "custom"; + this.querySelector(".margin-group").hidden = !customMargin; + if (customMargin) { + // Update the custom margin values to ensure consistency + this.updateCustomMargins(); + return; + } + + this.dispatchSettingsChange({ + margins: e.target.value, + customMargins: null, + }); + } + + if ( + e.target == this._customTopMargin || + e.target == this._customBottomMargin || + e.target == this._customLeftMargin || + e.target == this._customRightMargin + ) { + if (e.target.checkValidity()) { + this.updateMaxValues(); + } + if ( + this._customTopMargin.validity.valid && + this._customBottomMargin.validity.valid && + this._customLeftMargin.validity.valid && + this._customRightMargin.validity.valid + ) { + this.formatMargin(e.target); + this.updateCustomMargins(); + } else if (e.target.validity.stepMismatch) { + // If this is the third digit after the decimal point, we should + // truncate the string. + this.formatMargin(e.target); + } + } + + window.clearTimeout(this.showErrorTimeoutId); + if ( + this._customTopMargin.validity.valid && + this._customBottomMargin.validity.valid && + this._customLeftMargin.validity.valid && + this._customRightMargin.validity.valid + ) { + this._marginError.hidden = true; + } else { + this.cancelSettingsChange({ customMargins: true, margins: true }); + this.showErrorTimeoutId = window.setTimeout(() => { + this._marginError.hidden = false; + }, INPUT_DELAY_MS); + } + } +} +customElements.define("margins-select", MarginsPicker); + +class TwistySummary extends PrintUIControlMixin(HTMLElement) { + get isOpen() { + return this.closest("details")?.hasAttribute("open"); + } + + get templateId() { + return "twisty-summary-template"; + } + + initialize() { + if (this._initialized) { + return; + } + super.initialize(); + this.label = this.querySelector(".label"); + + this.addEventListener("click", this); + let shouldOpen = Services.prefs.getBoolPref( + "print.more-settings.open", + false + ); + this.closest("details").open = shouldOpen; + this.updateSummary(shouldOpen); + } + + handleEvent(e) { + let willOpen = !this.isOpen; + Services.prefs.setBoolPref("print.more-settings.open", willOpen); + this.updateSummary(willOpen); + } + + updateSummary(open) { + document.l10n.setAttributes( + this.label, + open + ? this.getAttribute("data-open-l10n-id") + : this.getAttribute("data-closed-l10n-id") + ); + } +} +customElements.define("twisty-summary", TwistySummary); + +class PageCount extends PrintUIControlMixin(HTMLElement) { + initialize() { + super.initialize(); + document.addEventListener("page-count", this); + } + + update(settings) { + this.numCopies = settings.numCopies; + this.duplex = settings.duplex; + this.outputDestination = settings.outputDestination; + this.render(); + } + + render() { + if (!this.numCopies || !this.sheetCount) { + return; + } + + let sheetCount = this.sheetCount; + + // When printing to a printer (not to a file) update + // the sheet count to account for duplex printing. + if ( + this.outputDestination == Ci.nsIPrintSettings.kOutputDestinationPrinter && + this.duplex != Ci.nsIPrintSettings.kDuplexNone + ) { + sheetCount = Math.ceil(sheetCount / 2); + } + + sheetCount *= this.numCopies; + + document.l10n.setAttributes(this, "printui-sheets-count", { + sheetCount, + }); + + // The loading attribute must be removed on first render + if (this.hasAttribute("loading")) { + this.removeAttribute("loading"); + } + + if (this.id) { + // We're showing the sheet count, so let it describe the dialog. + document.body.setAttribute("aria-describedby", this.id); + } + } + + handleEvent(e) { + this.sheetCount = e.detail.sheetCount; + this.render(); + } +} +customElements.define("page-count", PageCount); + +class PrintBackgrounds extends PrintSettingCheckbox { + update(settings) { + super.update(settings); + let isSimplified = settings.sourceVersion == "simplified"; + this.disabled = isSimplified; + this.toggleAttribute("disallowed", isSimplified); + this.checked = !isSimplified && settings.printBackgrounds; + } +} +customElements.define("print-backgrounds", PrintBackgrounds, { + extends: "input", +}); + +class PrintButton extends PrintUIControlMixin(HTMLButtonElement) { + update(settings) { + let l10nId = + settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER + ? "printui-primary-button-save" + : "printui-primary-button"; + document.l10n.setAttributes(this, l10nId); + } +} +customElements.define("print-button", PrintButton, { extends: "button" }); + +class CancelButton extends HTMLButtonElement { + constructor() { + super(); + this.addEventListener("click", () => { + this.dispatchEvent(new Event("cancel-print", { bubbles: true })); + }); + } +} +customElements.define("cancel-button", CancelButton, { extends: "button" }); + +async function pickFileName(contentTitle, currentURI) { + let picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + let [title] = await document.l10n.formatMessages([ + { id: "printui-save-to-pdf-title" }, + ]); + title = title.value; + + let filename; + if (contentTitle != "") { + filename = contentTitle; + } else { + let url = new URL(currentURI); + let path = decodeURIComponent(url.pathname); + path = path.replace(/\/$/, ""); + filename = path.split("/").pop(); + if (filename == "") { + filename = url.hostname; + } + } + if (!filename.endsWith(".pdf")) { + // macOS and linux don't set the extension based on the default extension. + // Windows won't add the extension a second time, fortunately. + // If it already ends with .pdf though, adding it again isn't needed. + filename += ".pdf"; + } + filename = DownloadPaths.sanitize(filename); + + picker.init( + window.docShell.chromeEventHandler.ownerGlobal, + title, + Ci.nsIFilePicker.modeSave + ); + picker.appendFilter("PDF", "*.pdf"); + picker.defaultExtension = "pdf"; + picker.defaultString = filename; + + let retval = await new Promise(resolve => picker.open(resolve)); + + if (retval == 1) { + throw new Error({ reason: "cancelled" }); + } else { + // OK clicked (retval == 0) or replace confirmed (retval == 2) + + // Workaround: When trying to replace an existing file that is open in another application (i.e. a locked file), + // the print progress listener is never called. This workaround ensures that a correct status is always returned. + try { + let fstream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + fstream.init(picker.file, 0x2a, 0o666, 0); // ioflags = write|create|truncate, file permissions = rw-rw-rw- + fstream.close(); + + // Remove the file to reduce the likelihood of the user opening an empty or damaged fle when the + // preview is loading + await IOUtils.remove(picker.file.path); + } catch (e) { + throw new Error({ reason: retval == 0 ? "not_saved" : "not_replaced" }); + } + } + + return picker.file.path; +} diff --git a/toolkit/components/printing/content/printPageSetup.js b/toolkit/components/printing/content/printPageSetup.js new file mode 100644 index 0000000000..91f5786243 --- /dev/null +++ b/toolkit/components/printing/content/printPageSetup.js @@ -0,0 +1,538 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var gDialog; +var paramBlock; +var gPrintService = null; +var gPrintSettings = null; +var gStringBundle = null; +var gDoingMetric = false; + +var gPrintSettingsInterface = Ci.nsIPrintSettings; +var gDoDebug = false; + +// --------------------------------------------------- +function initDialog() { + gDialog = {}; + + gDialog.orientation = document.getElementById("orientation"); + gDialog.portrait = document.getElementById("portrait"); + gDialog.landscape = document.getElementById("landscape"); + + gDialog.printBG = document.getElementById("printBG"); + + gDialog.shrinkToFit = document.getElementById("shrinkToFit"); + + gDialog.marginGroup = document.getElementById("marginGroup"); + + gDialog.marginPage = document.getElementById("marginPage"); + gDialog.marginTop = document.getElementById("marginTop"); + gDialog.marginBottom = document.getElementById("marginBottom"); + gDialog.marginLeft = document.getElementById("marginLeft"); + gDialog.marginRight = document.getElementById("marginRight"); + + gDialog.topInput = document.getElementById("topInput"); + gDialog.bottomInput = document.getElementById("bottomInput"); + gDialog.leftInput = document.getElementById("leftInput"); + gDialog.rightInput = document.getElementById("rightInput"); + + gDialog.hLeftOption = document.getElementById("hLeftOption"); + gDialog.hCenterOption = document.getElementById("hCenterOption"); + gDialog.hRightOption = document.getElementById("hRightOption"); + + gDialog.fLeftOption = document.getElementById("fLeftOption"); + gDialog.fCenterOption = document.getElementById("fCenterOption"); + gDialog.fRightOption = document.getElementById("fRightOption"); + + gDialog.scalingLabel = document.getElementById("scalingInput"); + gDialog.scalingInput = document.getElementById("scalingInput"); + + gDialog.enabled = false; + + document.addEventListener("dialogaccept", onAccept); +} + +// --------------------------------------------------- +function isListOfPrinterFeaturesAvailable() { + return Services.prefs.getBoolPref( + "print.tmp.printerfeatures." + + gPrintSettings.printerName + + ".has_special_printerfeatures", + false + ); +} + +// --------------------------------------------------- +function checkDouble(element) { + element.value = element.value.replace(/[^.0-9]/g, ""); +} + +// Theoretical paper width/height. +var gPageWidth = 8.5; +var gPageHeight = 11.0; + +// --------------------------------------------------- +function setOrientation() { + var selection = gDialog.orientation.selectedItem; + + var style = "background-color:white;"; + if ( + (selection == gDialog.portrait && gPageWidth > gPageHeight) || + (selection == gDialog.landscape && gPageWidth < gPageHeight) + ) { + // Swap width/height. + var temp = gPageHeight; + gPageHeight = gPageWidth; + gPageWidth = temp; + } + var div = gDoingMetric ? 100 : 10; + style += + "width:" + + gPageWidth / div + + unitString() + + ";height:" + + gPageHeight / div + + unitString() + + ";"; + gDialog.marginPage.setAttribute("style", style); +} + +// --------------------------------------------------- +function unitString() { + return gPrintSettings.paperSizeUnit == + gPrintSettingsInterface.kPaperSizeInches + ? "in" + : "mm"; +} + +// --------------------------------------------------- +function checkMargin(value, max, other) { + // Don't draw this margin bigger than permitted. + return Math.min(value, max - other.value); +} + +// --------------------------------------------------- +function changeMargin(node) { + // Correct invalid input. + checkDouble(node); + + // Reset the margin height/width for this node. + var val = node.value; + var nodeToStyle; + var attr = "width"; + if (node == gDialog.topInput) { + nodeToStyle = gDialog.marginTop; + val = checkMargin(val, gPageHeight, gDialog.bottomInput); + attr = "height"; + } else if (node == gDialog.bottomInput) { + nodeToStyle = gDialog.marginBottom; + val = checkMargin(val, gPageHeight, gDialog.topInput); + attr = "height"; + } else if (node == gDialog.leftInput) { + nodeToStyle = gDialog.marginLeft; + val = checkMargin(val, gPageWidth, gDialog.rightInput); + } else { + nodeToStyle = gDialog.marginRight; + val = checkMargin(val, gPageWidth, gDialog.leftInput); + } + var style = attr + ":" + val / 10 + unitString() + ";"; + nodeToStyle.setAttribute("style", style); +} + +// --------------------------------------------------- +function changeMargins() { + changeMargin(gDialog.topInput); + changeMargin(gDialog.bottomInput); + changeMargin(gDialog.leftInput); + changeMargin(gDialog.rightInput); +} + +// --------------------------------------------------- +async function customize(node) { + // If selection is now "Custom..." then prompt user for custom setting. + if (node.value == 6) { + let [title, promptText] = await document.l10n.formatValues([ + { id: "custom-prompt-title" }, + { id: "custom-prompt-prompt" }, + ]); + var result = { value: node.custom }; + var ok = Services.prompt.prompt(window, title, promptText, result, null, { + value: false, + }); + if (ok) { + node.custom = result.value; + } + } +} + +// --------------------------------------------------- +function setHeaderFooter(node, value) { + node.value = hfValueToId(value); + if (node.value == 6) { + // Remember current Custom... value. + node.custom = value; + } else { + // Start with empty Custom... value. + node.custom = ""; + } +} + +var gHFValues = []; +gHFValues["&T"] = 1; +gHFValues["&U"] = 2; +gHFValues["&D"] = 3; +gHFValues["&P"] = 4; +gHFValues["&PT"] = 5; + +function hfValueToId(val) { + if (val in gHFValues) { + return gHFValues[val]; + } + if (val.length) { + return 6; // Custom... + } + return 0; // --blank-- +} + +function hfIdToValue(node) { + var result = ""; + switch (parseInt(node.value)) { + case 0: + break; + case 1: + result = "&T"; + break; + case 2: + result = "&U"; + break; + case 3: + result = "&D"; + break; + case 4: + result = "&P"; + break; + case 5: + result = "&PT"; + break; + case 6: + result = node.custom; + break; + } + return result; +} + +async function lastUsedPrinterNameOrDefault() { + let printerList = Cc["@mozilla.org/gfx/printerlist;1"].getService( + Ci.nsIPrinterList + ); + let lastUsedName = gPrintService.lastUsedPrinterName; + let printers = await printerList.printers; + for (let printer of printers) { + printer.QueryInterface(Ci.nsIPrinter); + if (printer.name == lastUsedName) { + return lastUsedName; + } + } + return printerList.systemDefaultPrinterName; +} + +async function setPrinterDefaultsForSelectedPrinter() { + if (gPrintSettings.printerName == "") { + gPrintSettings.printerName = await lastUsedPrinterNameOrDefault(); + } + + // First get any defaults from the printer + gPrintService.initPrintSettingsFromPrinter( + gPrintSettings.printerName, + gPrintSettings + ); + + // now augment them with any values from last time + gPrintService.initPrintSettingsFromPrefs( + gPrintSettings, + true, + gPrintSettingsInterface.kInitSaveAll + ); + + if (gDoDebug) { + dump( + "pagesetup/setPrinterDefaultsForSelectedPrinter: printerName='" + + gPrintSettings.printerName + + "', orientation='" + + gPrintSettings.orientation + + "'\n" + ); + } +} + +// --------------------------------------------------- +async function loadDialog() { + var print_orientation = 0; + var print_margin_top = 0.5; + var print_margin_left = 0.5; + var print_margin_bottom = 0.5; + var print_margin_right = 0.5; + + try { + gPrintService = Cc["@mozilla.org/gfx/printsettings-service;1"]; + if (gPrintService) { + gPrintService = gPrintService.getService(); + if (gPrintService) { + gPrintService = gPrintService.QueryInterface( + Ci.nsIPrintSettingsService + ); + } + } + } catch (ex) { + dump("loadDialog: ex=" + ex + "\n"); + } + + await setPrinterDefaultsForSelectedPrinter(); + + gDialog.printBG.checked = + gPrintSettings.printBGColors || gPrintSettings.printBGImages; + + gDialog.shrinkToFit.checked = gPrintSettings.shrinkToFit; + + gDialog.scalingLabel.disabled = gDialog.scalingInput.disabled = + gDialog.shrinkToFit.checked; + + if ( + gPrintSettings.paperSizeUnit == gPrintSettingsInterface.kPaperSizeInches + ) { + document.l10n.setAttributes( + gDialog.marginGroup, + "margin-group-label-inches" + ); + gDoingMetric = false; + } else { + document.l10n.setAttributes( + gDialog.marginGroup, + "margin-group-label-metric" + ); + // Also, set global page dimensions for A4 paper, in millimeters (assumes portrait at this point). + gPageWidth = 2100; + gPageHeight = 2970; + gDoingMetric = true; + } + + print_orientation = gPrintSettings.orientation; + print_margin_top = convertMarginInchesToUnits( + gPrintSettings.marginTop, + gDoingMetric + ); + print_margin_left = convertMarginInchesToUnits( + gPrintSettings.marginLeft, + gDoingMetric + ); + print_margin_right = convertMarginInchesToUnits( + gPrintSettings.marginRight, + gDoingMetric + ); + print_margin_bottom = convertMarginInchesToUnits( + gPrintSettings.marginBottom, + gDoingMetric + ); + + if (gDoDebug) { + dump("print_orientation " + print_orientation + "\n"); + + dump("print_margin_top " + print_margin_top + "\n"); + dump("print_margin_left " + print_margin_left + "\n"); + dump("print_margin_right " + print_margin_right + "\n"); + dump("print_margin_bottom " + print_margin_bottom + "\n"); + } + + if (print_orientation == gPrintSettingsInterface.kPortraitOrientation) { + gDialog.orientation.selectedItem = gDialog.portrait; + } else if ( + print_orientation == gPrintSettingsInterface.kLandscapeOrientation + ) { + gDialog.orientation.selectedItem = gDialog.landscape; + } + + // Set orientation the first time on a timeout so the dialog sizes to the + // maximum height specified in the .xul file. Otherwise, if the user switches + // from landscape to portrait, the content grows and the buttons are clipped. + setTimeout(setOrientation, 0); + + gDialog.topInput.value = print_margin_top.toFixed(1); + gDialog.bottomInput.value = print_margin_bottom.toFixed(1); + gDialog.leftInput.value = print_margin_left.toFixed(1); + gDialog.rightInput.value = print_margin_right.toFixed(1); + changeMargins(); + + setHeaderFooter(gDialog.hLeftOption, gPrintSettings.headerStrLeft); + setHeaderFooter(gDialog.hCenterOption, gPrintSettings.headerStrCenter); + setHeaderFooter(gDialog.hRightOption, gPrintSettings.headerStrRight); + + setHeaderFooter(gDialog.fLeftOption, gPrintSettings.footerStrLeft); + setHeaderFooter(gDialog.fCenterOption, gPrintSettings.footerStrCenter); + setHeaderFooter(gDialog.fRightOption, gPrintSettings.footerStrRight); + + gDialog.scalingInput.value = (gPrintSettings.scaling * 100).toFixed(0); + + // Enable/disable widgets based in the information whether the selected + // printer supports the matching feature or not + if (isListOfPrinterFeaturesAvailable()) { + if ( + Services.prefs.getBoolPref( + "print.tmp.printerfeatures." + + gPrintSettings.printerName + + ".can_change_orientation" + ) + ) { + gDialog.orientation.removeAttribute("disabled"); + } else { + gDialog.orientation.setAttribute("disabled", "true"); + } + } + + // Give initial focus to the orientation radio group. + // Done on a timeout due to to bug 103197. + setTimeout(function () { + gDialog.orientation.focus(); + }, 0); +} + +// --------------------------------------------------- +function onLoad() { + // Init gDialog. + initDialog(); + + if (window.arguments[0] != null) { + gPrintSettings = window.arguments[0].QueryInterface(Ci.nsIPrintSettings); + paramBlock = window.arguments[1].QueryInterface(Ci.nsIDialogParamBlock); + } else if (gDoDebug) { + alert("window.arguments[0] == null!"); + } + + // default return value is "cancel" + paramBlock.SetInt(0, 0); + + if (gPrintSettings) { + loadDialog(); + } else if (gDoDebug) { + alert("Could initialize gDialog, PrintSettings is null!"); + } +} + +function convertUnitsMarginToInches(aVal, aIsMetric) { + if (aIsMetric) { + return aVal / 25.4; + } + return aVal; +} + +function convertMarginInchesToUnits(aVal, aIsMetric) { + if (aIsMetric) { + return aVal * 25.4; + } + return aVal; +} + +// --------------------------------------------------- +function onAccept() { + if (gPrintSettings) { + if (gDialog.orientation.selectedItem == gDialog.portrait) { + gPrintSettings.orientation = gPrintSettingsInterface.kPortraitOrientation; + } else { + gPrintSettings.orientation = + gPrintSettingsInterface.kLandscapeOrientation; + } + + // save these out so they can be picked up by the device spec + gPrintSettings.marginTop = convertUnitsMarginToInches( + gDialog.topInput.value, + gDoingMetric + ); + gPrintSettings.marginLeft = convertUnitsMarginToInches( + gDialog.leftInput.value, + gDoingMetric + ); + gPrintSettings.marginBottom = convertUnitsMarginToInches( + gDialog.bottomInput.value, + gDoingMetric + ); + gPrintSettings.marginRight = convertUnitsMarginToInches( + gDialog.rightInput.value, + gDoingMetric + ); + + gPrintSettings.headerStrLeft = hfIdToValue(gDialog.hLeftOption); + gPrintSettings.headerStrCenter = hfIdToValue(gDialog.hCenterOption); + gPrintSettings.headerStrRight = hfIdToValue(gDialog.hRightOption); + + gPrintSettings.footerStrLeft = hfIdToValue(gDialog.fLeftOption); + gPrintSettings.footerStrCenter = hfIdToValue(gDialog.fCenterOption); + gPrintSettings.footerStrRight = hfIdToValue(gDialog.fRightOption); + + gPrintSettings.printBGColors = gDialog.printBG.checked; + gPrintSettings.printBGImages = gDialog.printBG.checked; + + gPrintSettings.shrinkToFit = gDialog.shrinkToFit.checked; + + var scaling = document.getElementById("scalingInput").value; + if (scaling < 10.0) { + scaling = 10.0; + } + if (scaling > 500.0) { + scaling = 500.0; + } + scaling /= 100.0; + gPrintSettings.scaling = scaling; + + if (gDoDebug) { + dump("******* Page Setup Accepting ******\n"); + dump("print_margin_top " + gDialog.topInput.value + "\n"); + dump("print_margin_left " + gDialog.leftInput.value + "\n"); + dump("print_margin_right " + gDialog.bottomInput.value + "\n"); + dump("print_margin_bottom " + gDialog.rightInput.value + "\n"); + } + } + + // set return value to "ok" + if (paramBlock) { + paramBlock.SetInt(0, 1); + } else { + dump("*** FATAL ERROR: No paramBlock\n"); + } + + var flags = + gPrintSettingsInterface.kInitSaveMargins | + gPrintSettingsInterface.kInitSaveHeaderLeft | + gPrintSettingsInterface.kInitSaveHeaderCenter | + gPrintSettingsInterface.kInitSaveHeaderRight | + gPrintSettingsInterface.kInitSaveFooterLeft | + gPrintSettingsInterface.kInitSaveFooterCenter | + gPrintSettingsInterface.kInitSaveFooterRight | + gPrintSettingsInterface.kInitSaveBGColors | + gPrintSettingsInterface.kInitSaveBGImages | + gPrintSettingsInterface.kInitSaveInColor | + gPrintSettingsInterface.kInitSaveReversed | + gPrintSettingsInterface.kInitSaveOrientation | + gPrintSettingsInterface.kInitSaveShrinkToFit | + gPrintSettingsInterface.kInitSaveScaling; + + // Note that this file is Windows only code, so this doesn't handle saving + // for other platforms. + // XXX Should we do this in nsPrintDialogServiceWin::ShowPageSetup (the code + // that invokes us), since ShowPageSetup is where we do the saving for the + // other platforms? + gPrintService.maybeSavePrintSettingsToPrefs(gPrintSettings, flags); +} + +// --------------------------------------------------- +function onCancel() { + // set return value to "cancel" + if (paramBlock) { + paramBlock.SetInt(0, 0); + } else { + dump("*** FATAL ERROR: No paramBlock\n"); + } + + return true; +} diff --git a/toolkit/components/printing/content/printPageSetup.xhtml b/toolkit/components/printing/content/printPageSetup.xhtml new file mode 100644 index 0000000000..235e8a474b --- /dev/null +++ b/toolkit/components/printing/content/printPageSetup.xhtml @@ -0,0 +1,291 @@ +<?xml version="1.0"?> +<!-- -*- Mode: HTML -*- --> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://global/skin/in-content/common.css"?> +<?xml-stylesheet href="chrome://global/skin/printPageSetup.css" type="text/css"?> + +<window + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="onLoad();" + oncancel="return onCancel();" + data-l10n-id="print-setup" + dialogroot="true" + persist="screenX screenY" + screenX="24" + screenY="24" +> + <dialog id="printPageSetupDialog"> + <linkset> + <html:link rel="localization" href="toolkit/printing/printDialogs.ftl" /> + </linkset> + + <script src="chrome://global/content/globalOverlay.js" /> + <script src="chrome://global/content/editMenuOverlay.js" /> + <script src="chrome://global/content/printPageSetup.js" /> + + <tabbox flex="1"> + <tabs> + <tab data-l10n-id="basic-tab" /> + <tab data-l10n-id="advanced-tab" /> + </tabs> + <tabpanels flex="1"> + <vbox> + <html:fieldset> + <html:legend + ><label data-l10n-id="format-group-label" + /></html:legend> + <vbox class="groupbox-body"> + <hbox align="center"> + <label control="orientation" data-l10n-id="orientation-label" /> + <radiogroup id="orientation" oncommand="setOrientation()"> + <hbox align="center"> + <radio + id="portrait" + class="portrait-page" + data-l10n-id="portrait" + /> + <radio + id="landscape" + class="landscape-page" + data-l10n-id="landscape" + /> + </hbox> + </radiogroup> + </hbox> + <separator /> + <hbox align="center"> + <label control="scalingInput" data-l10n-id="scale" /> + <html:input + id="scalingInput" + size="4" + oninput="checkDouble(this)" + /> + <label data-l10n-id="scale-percent" /> + <separator /> + <checkbox + id="shrinkToFit" + data-l10n-id="shrink-to-fit" + oncommand="gDialog.scalingInput.disabled = gDialog.scalingLabel.disabled = this.checked" + /> + </hbox> + </vbox> + </html:fieldset> + <html:fieldset> + <html:legend + ><label data-l10n-id="options-group-label" + /></html:legend> + <checkbox + id="printBG" + class="groupbox-body" + data-l10n-id="print-bg" + /> + </html:fieldset> + </vbox> + <vbox> + <html:fieldset> + <html:legend + ><label id="marginGroup" data-l10n-id="margin-group-label" + /></html:legend> + <vbox class="groupbox-body"> + <hbox align="center"> + <spacer flex="1" /> + <label control="topInput" data-l10n-id="margin-top" /> + <html:input + id="topInput" + size="5" + oninput="changeMargin(this)" + /> + <!-- This invisible label (with same content as the visible one!) is used + to ensure that the <input> is centered above the page. The same + technique is deployed for the bottom/left/right input fields, below. --> + <label + data-l10n-id="margin-top-invisible" + style="visibility: hidden" + /> + <spacer flex="1" /> + </hbox> + <hbox dir="ltr"> + <spacer flex="1" /> + <vbox> + <spacer flex="1" /> + <label control="leftInput" data-l10n-id="margin-left" /> + <html:input + id="leftInput" + size="5" + oninput="changeMargin(this)" + /> + <label + data-l10n-id="margin-left-invisible" + style="visibility: hidden" + /> + <spacer flex="1" /> + </vbox> + <!-- The "margin page" draws a simulated printout page with dashed lines + for the margins. The height/width style attributes of the marginTop, + marginBottom, marginLeft, and marginRight elements are set by + the JS code dynamically based on the user input. --> + <vbox id="marginPage" style="height: 29.7mm"> + <box id="marginTop" style="height: 0.05in" /> + <hbox flex="1" dir="ltr"> + <box id="marginLeft" style="width: 0.025in" /> + <box + style=" + border: 1px; + border-style: dashed; + border-color: gray; + " + flex="1" + /> + <box id="marginRight" style="width: 0.025in" /> + </hbox> + <box id="marginBottom" style="height: 0.05in" /> + </vbox> + <vbox> + <spacer flex="1" /> + <label control="rightInput" data-l10n-id="margin-right" /> + <html:input + id="rightInput" + size="5" + oninput="changeMargin(this)" + /> + <label + data-l10n-id="margin-right-invisible" + style="visibility: hidden" + /> + <spacer flex="1" /> + </vbox> + <spacer flex="1" /> + </hbox> + <hbox align="center"> + <spacer flex="1" /> + <label control="bottomInput" data-l10n-id="margin-bottom" /> + <html:input + id="bottomInput" + size="5" + oninput="changeMargin(this)" + /> + <label + data-l10n-id="margin-bottom-invisible" + style="visibility: hidden" + /> + <spacer flex="1" /> + </hbox> + </vbox> + </html:fieldset> + <html:fieldset> + <html:legend + ><label data-l10n-id="header-footer-label" + /></html:legend> + <box id="header-footer-grid" class="groupbox-body" dir="ltr"> + <menulist + id="hLeftOption" + oncommand="customize(this)" + data-l10n-id="header-left-tip" + > + <menupopup> + <menuitem value="0" data-l10n-id="hf-blank" /> + <menuitem value="1" data-l10n-id="hf-title" /> + <menuitem value="2" data-l10n-id="hf-url" /> + <menuitem value="3" data-l10n-id="hf-date-and-time" /> + <menuitem value="4" data-l10n-id="hf-page" /> + <menuitem value="5" data-l10n-id="hf-page-and-total" /> + <menuitem value="6" data-l10n-id="hf-custom" /> + </menupopup> + </menulist> + <menulist + id="hCenterOption" + oncommand="customize(this)" + data-l10n-id="header-center-tip" + > + <menupopup> + <menuitem value="0" data-l10n-id="hf-blank" /> + <menuitem value="1" data-l10n-id="hf-title" /> + <menuitem value="2" data-l10n-id="hf-url" /> + <menuitem value="3" data-l10n-id="hf-date-and-time" /> + <menuitem value="4" data-l10n-id="hf-page" /> + <menuitem value="5" data-l10n-id="hf-page-and-total" /> + <menuitem value="6" data-l10n-id="hf-custom" /> + </menupopup> + </menulist> + <menulist + id="hRightOption" + oncommand="customize(this)" + data-l10n-id="header-right-tip" + > + <menupopup> + <menuitem value="0" data-l10n-id="hf-blank" /> + <menuitem value="1" data-l10n-id="hf-title" /> + <menuitem value="2" data-l10n-id="hf-url" /> + <menuitem value="3" data-l10n-id="hf-date-and-time" /> + <menuitem value="4" data-l10n-id="hf-page" /> + <menuitem value="5" data-l10n-id="hf-page-and-total" /> + <menuitem value="6" data-l10n-id="hf-custom" /> + </menupopup> + </menulist> + <vbox align="center"> + <label data-l10n-id="hf-left-label" /> + </vbox> + <vbox align="center"> + <label data-l10n-id="hf-center-label" /> + </vbox> + <vbox align="center"> + <label data-l10n-id="hf-right-label" /> + </vbox> + <menulist + id="fLeftOption" + oncommand="customize(this)" + data-l10n-id="footer-left-tip" + > + <menupopup> + <menuitem value="0" data-l10n-id="hf-blank" /> + <menuitem value="1" data-l10n-id="hf-title" /> + <menuitem value="2" data-l10n-id="hf-url" /> + <menuitem value="3" data-l10n-id="hf-date-and-time" /> + <menuitem value="4" data-l10n-id="hf-page" /> + <menuitem value="5" data-l10n-id="hf-page-and-total" /> + <menuitem value="6" data-l10n-id="hf-custom" /> + </menupopup> + </menulist> + <menulist + id="fCenterOption" + oncommand="customize(this)" + data-l10n-id="footer-center-tip" + > + <menupopup> + <menuitem value="0" data-l10n-id="hf-blank" /> + <menuitem value="1" data-l10n-id="hf-title" /> + <menuitem value="2" data-l10n-id="hf-url" /> + <menuitem value="3" data-l10n-id="hf-date-and-time" /> + <menuitem value="4" data-l10n-id="hf-page" /> + <menuitem value="5" data-l10n-id="hf-page-and-total" /> + <menuitem value="6" data-l10n-id="hf-custom" /> + </menupopup> + </menulist> + <menulist + id="fRightOption" + oncommand="customize(this)" + data-l10n-id="footer-right-tip" + > + <menupopup> + <menuitem value="0" data-l10n-id="hf-blank" /> + <menuitem value="1" data-l10n-id="hf-title" /> + <menuitem value="2" data-l10n-id="hf-url" /> + <menuitem value="3" data-l10n-id="hf-date-and-time" /> + <menuitem value="4" data-l10n-id="hf-page" /> + <menuitem value="5" data-l10n-id="hf-page-and-total" /> + <menuitem value="6" data-l10n-id="hf-custom" /> + </menupopup> + </menulist> + </box> + </html:fieldset> + </vbox> + </tabpanels> + </tabbox> + </dialog> +</window> diff --git a/toolkit/components/printing/content/printPagination.css b/toolkit/components/printing/content/printPagination.css new file mode 100644 index 0000000000..5eb7b46924 --- /dev/null +++ b/toolkit/components/printing/content/printPagination.css @@ -0,0 +1,141 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +:host { + /* in-content/common.css variables */ + --blue-50: #0a84ff; + --grey-90-a10: rgba(12, 12, 13, 0.1); + --shadow-30: 0 4px 16px var(--grey-90-a10); + --border-active-shadow: var(--blue-50); +} + +:host { + display: block; + position: absolute; + bottom: 24px; + inset-inline-start: 50%; + translate: -50%; +} +:host(:-moz-locale-dir(rtl)) { + translate: 50%; +} + +.container { + margin-inline: auto; + align-items: center; + display: flex; + justify-content: center; + box-shadow: var(--shadow-30); + color: var(--toolbar-color); + background-color: var(--toolbar-bgcolor); + border-radius: 6px; + border-style: none; +} +.container::before { + content: ""; + display: block; + position: absolute; + inset: 0; + z-index: -1; + background-color: ButtonFace; + border-radius: 6px; +} + +.toolbarButton, +.toolbarCenter { + align-self: stretch; + flex: 0 0 auto; + padding: var(--toolbarbutton-outer-padding); + border: none; + border-inline-end: 1px solid ThreeDShadow; + border-block: 1px solid ThreeDShadow; + color: inherit; + background-color: transparent; + min-width: calc(2 * var(--toolbarbutton-inner-padding) + 16px); + min-height: calc(2 * var(--toolbarbutton-inner-padding) + 16px); +} +.startItem { + border-inline-start: 1px solid ThreeDShadow; + border-start-start-radius: 6px; + border-end-start-radius: 6px; +} +.endItem { + border-start-end-radius: 6px; + border-end-end-radius: 6px; +} + +.toolbarButton::after { + content: ""; + display: inline-block; + width: 16px; + height: 16px; + vertical-align: text-bottom; + text-align: center; + background-repeat: no-repeat; + background-position: center center; + background-size: 12px; + -moz-context-properties: fill, fill-opacity; + fill: var(--toolbarbutton-icon-fill); +} + +.toolbarButton:hover { + background-color: var(--toolbarbutton-hover-background); +} +.toolbarButton:hover:active { + background-color: var(--toolbarbutton-active-background); +} +.toolbarButton::-moz-focus-inner { + border: none; +} +.toolbarButton:focus { + z-index: 1; +} + +.toolbarButton:-moz-focusring { + outline: 2px solid var(--border-active-shadow); +} + +.toolbarCenter { + flex-shrink: 0; + /* 3 chars + (3px border + 1px padding) on both sides */ + min-width: calc(8px + 3ch); + padding: 0 32px; + display: flex; + align-items: center; + justify-content: center; +} + +#navigateHome::after, +#navigateEnd::after { + background-image: url("chrome://global/skin/icons/chevron.svg"); +} + +#navigatePrevious::after, +#navigateNext::after { + background-image: url("chrome://global/skin/icons/arrow-left.svg"); +} + +#navigatePrevious:-moz-locale-dir(rtl)::after, +#navigateNext:-moz-locale-dir(ltr)::after { + background-image: url("chrome://global/skin/icons/arrow-right.svg"); +} + +#navigateEnd:-moz-locale-dir(rtl)::after, +#navigateHome:-moz-locale-dir(ltr)::after { + transform: scaleX(-1); +} + +/* progressively hide the navigation buttons when the print preview is too narrow to fit */ +@media (max-width: 550px) { + #navigatePrevious, + #navigateNext, + #navigateEnd, + #navigateHome { + display: none; + } + .toolbarCenter { + border-inline-start: 1px solid ThreeDShadow; + border-radius: 6px; + } +} diff --git a/toolkit/components/printing/content/printPreviewPagination.js b/toolkit/components/printing/content/printPreviewPagination.js new file mode 100644 index 0000000000..c503f0a8cb --- /dev/null +++ b/toolkit/components/printing/content/printPreviewPagination.js @@ -0,0 +1,185 @@ +// This file is loaded into the browser window scope. +/* eslint-env mozilla/browser-window */ + +// -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +customElements.define( + "printpreview-pagination", + class PrintPreviewPagination extends HTMLElement { + static get markup() { + return ` + <html:link rel="stylesheet" href="chrome://global/content/printPagination.css" /> + <html:div class="container"> + <html:button id="navigateHome" class="toolbarButton startItem" data-l10n-id="printpreview-homearrow-button"></html:button> + <html:button id="navigatePrevious" class="toolbarButton" data-l10n-id="printpreview-previousarrow-button"></html:button> + <html:div class="toolbarCenter"><html:span id="sheetIndicator" data-l10n-id="printpreview-sheet-of-sheets" data-l10n-args='{ "sheetNum": 1, "sheetCount": 1 }'></html:span></html:div> + <html:button id="navigateNext" class="toolbarButton" data-l10n-id="printpreview-nextarrow-button"></html:button> + <html:button id="navigateEnd" class="toolbarButton endItem" data-l10n-id="printpreview-endarrow-button"></html:button> + </html:div> + `; + } + + static get defaultProperties() { + return { + currentPage: 1, + sheetCount: 1, + }; + } + + get previewBrowser() { + return this._previewBrowser; + } + + set previewBrowser(aBrowser) { + this._previewBrowser = aBrowser; + } + + observePreviewBrowser(browser) { + if (browser == this.previewBrowser || !this.isConnected) { + return; + } + this.previewBrowser = browser; + this.mutationObserver.disconnect(); + this.mutationObserver.observe(browser, { + attributes: ["current-page", "sheet-count"], + }); + this.updateFromBrowser(); + } + + connectedCallback() { + MozXULElement.insertFTLIfNeeded("toolkit/printing/printPreview.ftl"); + + const shadowRoot = this.attachShadow({ mode: "open" }); + document.l10n.connectRoot(shadowRoot); + + let fragment = MozXULElement.parseXULToFragment(this.constructor.markup); + this.shadowRoot.append(fragment); + + this.elements = { + sheetIndicator: shadowRoot.querySelector("#sheetIndicator"), + homeButton: shadowRoot.querySelector("#navigateHome"), + previousButton: shadowRoot.querySelector("#navigatePrevious"), + nextButton: shadowRoot.querySelector("#navigateNext"), + endButton: shadowRoot.querySelector("#navigateEnd"), + }; + + this.shadowRoot.addEventListener("click", this); + + this.mutationObserver = new MutationObserver(() => + this.updateFromBrowser() + ); + + // Initial render with some default values + // We'll be updated with real values when available + this.update(this.constructor.defaultProperties); + } + + disconnectedCallback() { + document.l10n.disconnectRoot(this.shadowRoot); + this.shadowRoot.textContent = ""; + this.mutationObserver?.disconnect(); + delete this.mutationObserver; + this.currentPreviewBrowserObserver?.disconnect(); + delete this.currentPreviewBrowserObserver; + } + + handleEvent(event) { + if (event.type == "click" && event.button != 0) { + return; + } + event.stopPropagation(); + + switch (event.target) { + case this.elements.homeButton: + this.navigate(0, 0, "home"); + break; + case this.elements.previousButton: + this.navigate(-1, 0, 0); + break; + case this.elements.nextButton: + this.navigate(1, 0, 0); + break; + case this.elements.endButton: + this.navigate(0, 0, "end"); + break; + } + } + + navigate(aDirection, aPageNum, aHomeOrEnd) { + const nsIWebBrowserPrint = Ci.nsIWebBrowserPrint; + let targetNum; + let navType; + // we use only one of aHomeOrEnd, aDirection, or aPageNum + if (aHomeOrEnd) { + // We're going to either the very first page ("home"), or the + // very last page ("end"). + if (aHomeOrEnd == "home") { + targetNum = 1; + navType = nsIWebBrowserPrint.PRINTPREVIEW_HOME; + } else { + targetNum = this.sheetCount; + navType = nsIWebBrowserPrint.PRINTPREVIEW_END; + } + } else if (aPageNum) { + // We're going to a specific page (aPageNum) + targetNum = Math.min(Math.max(1, aPageNum), this.sheetCount); + navType = nsIWebBrowserPrint.PRINTPREVIEW_GOTO_PAGENUM; + } else { + // aDirection is either +1 or -1, and allows us to increment + // or decrement our currently viewed page. + targetNum = Math.min( + Math.max(1, this.currentSheet + aDirection), + this.sheetCount + ); + navType = nsIWebBrowserPrint.PRINTPREVIEW_GOTO_PAGENUM; + } + + // Preemptively update our own state, rather than waiting for the message from the child process + // This allows subsequent clicks of next/back to advance 1 page per click if possible + // and keeps the UI feeling more responsive + this.update({ currentPage: targetNum }); + + this.previewBrowser.sendMessageToActor( + "Printing:Preview:Navigate", + { + navType, + pageNum: targetNum, + }, + "Printing" + ); + } + + update(data = {}) { + if (data.sheetCount) { + this.sheetCount = data.sheetCount; + } + if (data.currentPage) { + this.currentSheet = data.currentPage; + } + document.l10n.setAttributes( + this.elements.sheetIndicator, + this.elements.sheetIndicator.dataset.l10nId, + { + sheetNum: this.currentSheet, + sheetCount: this.sheetCount, + } + ); + } + + updateFromBrowser() { + let sheetCount = parseInt( + this.previewBrowser.getAttribute("sheet-count"), + 10 + ); + let currentPage = parseInt( + this.previewBrowser.getAttribute("current-page"), + 10 + ); + this.update({ sheetCount, currentPage }); + } + } +); diff --git a/toolkit/components/printing/content/printUtils.js b/toolkit/components/printing/content/printUtils.js new file mode 100644 index 0000000000..f1e3489fe7 --- /dev/null +++ b/toolkit/components/printing/content/printUtils.js @@ -0,0 +1,822 @@ +// This file is loaded into the browser window scope. +/* eslint-env mozilla/browser-window */ + +// -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * PrintUtils is a utility for front-end code to trigger common print + * operations (printing, show print preview, show page settings). + * + * Unfortunately, likely due to inconsistencies in how different operating + * systems do printing natively, our XPCOM-level printing interfaces + * are a bit confusing and the method by which we do something basic + * like printing a page is quite circuitous. + * + * To compound that, we need to support remote browsers, and that means + * kicking off the print jobs in the content process. This means we send + * messages back and forth to that process via the Printing actor. + * + * This also means that <xul:browser>'s that hope to use PrintUtils must have + * their type attribute set to "content". + * + * Messages sent: + * + * Printing:Preview:Enter + * This message is sent to put content into print preview mode. We pass + * the content window of the browser we're showing the preview of, and + * the target of the message is the browser that we'll be showing the + * preview in. + * + * Printing:Preview:Exit + * This message is sent to take content out of print preview mode. + */ + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "SHOW_PAGE_SETUP_MENU", + "print.show_page_setup_menu", + false +); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "PRINT_ALWAYS_SILENT", + "print.always_print_silent", + false +); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "PREFER_SYSTEM_DIALOG", + "print.prefer_system_dialog", + false +); + +ChromeUtils.defineESModuleGetters(this, { + PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs", +}); + +var PrintUtils = { + SAVE_TO_PDF_PRINTER: "Mozilla Save to PDF", + + get _bundle() { + delete this._bundle; + return (this._bundle = Services.strings.createBundle( + "chrome://global/locale/printing.properties" + )); + }, + + async checkForSelection(browsingContext) { + try { + let sourceActor = + browsingContext.currentWindowGlobal.getActor("PrintingSelection"); + // Need the await for the try to trigger... + return await sourceActor.sendQuery("PrintingSelection:HasSelection", {}); + } catch (e) { + console.error(e); + } + return false; + }, + + /** + * Updates the hidden state of the "Page Setup" menu items in the File menu, + * depending on the value of the `print.show_page_setup_menu` pref. + * Note: not all platforms have a "Page Setup" menu item (or Page Setup + * window). + */ + updatePrintSetupMenuHiddenState() { + let pageSetupMenuItem = document.getElementById("menu_printSetup"); + if (pageSetupMenuItem) { + pageSetupMenuItem.hidden = !SHOW_PAGE_SETUP_MENU; + } + }, + + /** + * Shows the page setup dialog, and saves any settings changed in + * that dialog if print.save_print_settings is set to true. + * + * @return true on success, false on failure + */ + showPageSetup() { + let printSettings = this.getPrintSettings(); + // If we come directly from the Page Setup menu, the hack in + // _enterPrintPreview will not have been invoked to set the last used + // printer name. For the reasons outlined at that hack, we want that set + // here too. + let PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService( + Ci.nsIPrintSettingsService + ); + if (!PSSVC.lastUsedPrinterName) { + if (printSettings.printerName) { + PSSVC.maybeSaveLastUsedPrinterNameToPrefs(printSettings.printerName); + PSSVC.maybeSavePrintSettingsToPrefs( + printSettings, + Ci.nsIPrintSettings.kInitSaveAll + ); + } + } + try { + var PRINTDIALOGSVC = Cc[ + "@mozilla.org/widget/printdialog-service;1" + ].getService(Ci.nsIPrintDialogService); + PRINTDIALOGSVC.showPageSetupDialog(window, printSettings, null); + } catch (e) { + dump("showPageSetup " + e + "\n"); + return false; + } + return true; + }, + + /** + * This call exists in a separate method so it can be easily overridden where + * `gBrowser` doesn't exist (e.g. Thunderbird). + * + * @see getTabDialogBox in tabbrowser.js + */ + getTabDialogBox(sourceBrowser) { + return gBrowser.getTabDialogBox(sourceBrowser); + }, + + getPreviewBrowser(sourceBrowser) { + let dialogBox = this.getTabDialogBox(sourceBrowser); + for (let dialog of dialogBox.getTabDialogManager()._dialogs) { + let browser = dialog._box.querySelector(".printPreviewBrowser"); + if (browser) { + return browser; + } + } + return null; + }, + + /** + * Opens the tab modal version of the print UI for the current tab. + * + * @param aBrowsingContext + * The BrowsingContext of the window to print. + * @param aExistingPreviewBrowser + * An existing browser created for printing from window.print(). + * @param aPrintInitiationTime + * The time the print was initiated (typically by the user) as obtained + * from `Date.now()`. That is, the initiation time as the number of + * milliseconds since January 1, 1970. + * @param aPrintSelectionOnly + * Whether to print only the active selection of the given browsing + * context. + * @param aPrintFrameOnly + * Whether to print the selected frame only + * @return promise resolving when the dialog is open, rejected if the preview + * fails. + */ + _openTabModalPrint( + aBrowsingContext, + aOpenWindowInfo, + aPrintInitiationTime, + aPrintSelectionOnly, + aPrintFrameOnly + ) { + let sourceBrowser = aBrowsingContext.top.embedderElement; + let previewBrowser = this.getPreviewBrowser(sourceBrowser); + if (previewBrowser) { + // Don't open another dialog if we're already printing. + // + // XXX This can be racy can't it? getPreviewBrowser looks at browser that + // we set up after opening the dialog. But I guess worst case we just + // open two dialogs so... + throw new Error("Tab-modal print UI already open"); + } + + // Create the print preview dialog. + let args = PromptUtils.objectToPropBag({ + printSelectionOnly: !!aPrintSelectionOnly, + isArticle: sourceBrowser.isArticle, + printFrameOnly: !!aPrintFrameOnly, + }); + let dialogBox = this.getTabDialogBox(sourceBrowser); + let { closedPromise, dialog } = dialogBox.open( + `chrome://global/content/print.html?printInitiationTime=${aPrintInitiationTime}`, + { features: "resizable=no", sizeTo: "available" }, + args + ); + closedPromise.catch(e => { + console.error(e); + }); + + let settingsBrowser = dialog._frame; + let printPreview = new PrintPreview({ + sourceBrowsingContext: aBrowsingContext, + settingsBrowser, + topBrowsingContext: aBrowsingContext.top, + activeBrowsingContext: aBrowsingContext, + openWindowInfo: aOpenWindowInfo, + printFrameOnly: aPrintFrameOnly, + }); + // This will create the source browser in connectedCallback() if we sent + // openWindowInfo. Otherwise the browser will be null. + settingsBrowser.parentElement.insertBefore(printPreview, settingsBrowser); + return printPreview.sourceBrowser; + }, + + /** + * Initialize a print, this will open the tab modal UI if it is enabled or + * defer to the native dialog/silent print. + * + * @param aBrowsingContext + * The BrowsingContext of the window to print. + * Note that the browsing context could belong to a subframe of the + * tab that called window.print, or similar shenanigans. + * @param aOptions + * {windowDotPrintOpenWindowInfo} + * Non-null if this call comes from window.print(). + * This is the nsIOpenWindowInfo object that has to + * be passed down to createBrowser in order for the + * static clone that has been cretaed in the child + * process to be linked to the browser it creates + * in the parent process. + * {printSelectionOnly} Whether to print only the active selection of + * the given browsing context. + * {printFrameOnly} Whether to print the selected frame. + */ + startPrintWindow(aBrowsingContext, aOptions) { + const printInitiationTime = Date.now(); + + // At most, one of these is set. + let { printSelectionOnly, printFrameOnly, windowDotPrintOpenWindowInfo } = + aOptions || {}; + + if ( + windowDotPrintOpenWindowInfo && + !windowDotPrintOpenWindowInfo.isForWindowDotPrint + ) { + throw new Error("Only expect openWindowInfo for window.print()"); + } + + let browsingContext = aBrowsingContext; + if (printSelectionOnly) { + // Ensure that we use the window with focus/selection if the context menu + // (from which 'Print selection' was selected) happens to have been opened + // over a different frame. + let focusedBc = Services.focus.focusedContentBrowsingContext; + if ( + focusedBc && + focusedBc.top.embedderElement == browsingContext.top.embedderElement + ) { + browsingContext = focusedBc; + } + } + + if (!PRINT_ALWAYS_SILENT && !PREFER_SYSTEM_DIALOG) { + return this._openTabModalPrint( + browsingContext, + windowDotPrintOpenWindowInfo, + printInitiationTime, + printSelectionOnly, + printFrameOnly + ); + } + + const useSystemDialog = PREFER_SYSTEM_DIALOG && !PRINT_ALWAYS_SILENT; + + let browser = null; + if (windowDotPrintOpenWindowInfo) { + // When we're called by handleStaticCloneCreatedForPrint(), we must + // return this browser. + browser = this.createParentBrowserForStaticClone( + browsingContext, + windowDotPrintOpenWindowInfo + ); + browsingContext = browser.browsingContext; + } + + // This code is wrapped in an async function so that we can await the async + // functions that it calls. + async function makePrintSettingsAndInvokePrint() { + let settings = PrintUtils.getPrintSettings( + /*aPrinterName*/ "", + /*aDefaultsOnly*/ false, + /*aAllowPseudoPrinter*/ !useSystemDialog + ); + settings.printSelectionOnly = printSelectionOnly; + if ( + settings.outputDestination == + Ci.nsIPrintSettings.kOutputDestinationFile && + !settings.toFileName + ) { + // TODO(bug 1748004): We should consider generating the file name + // from the document's title as we do in print.js's pickFileName + // (including using DownloadPaths.sanitize!). + // For now, the following is for consistency with the behavior + // prior to bug 1669149 part 3. + let dest = undefined; + try { + dest = Services.dirsvc.get("CurWorkD", Ci.nsIFile).path; + } catch (e) {} + if (!dest) { + dest = Services.dirsvc.get("Home", Ci.nsIFile).path; + } + settings.toFileName = PathUtils.join(dest, "mozilla.pdf"); + } + + if (useSystemDialog) { + const hasSelection = await PrintUtils.checkForSelection( + browsingContext + ); + + // Prompt the user to choose a printer and make any desired print + // settings changes. + let doPrint = false; + try { + doPrint = await PrintUtils.handleSystemPrintDialog( + browsingContext.topChromeWindow, + hasSelection, + settings + ); + if (!doPrint) { + return; + } + } finally { + // Clean up browser if we aren't going to use it. + if (!doPrint && browser) { + browser.remove(); + } + } + } + + // At some point we should handle the Promise that this returns (at + // least report rejection to telemetry). + browsingContext.print(settings); + } + + // We need to return to the event loop before calling + // makePrintSettingsAndInvokePrint() if we were called for `window.print()`. + // That's because if that function synchronously calls `browser.remove()` + // or `browsingContext.print()` before we return `browser`, the nested + // event loop that is being spun in the content process under the + // OpenInternal call in nsGlobalWindowOuter::Print will still be active. + // In the case of `browser.remove()`, nsGlobalWindowOuter::Print would then + // get unhappy once OpenInternal does return since it will fail to return + // a BrowsingContext. In the case of `browsingContext.print()`, we would + // re-enter nsGlobalWindowOuter::Print under the nested event loop and + // printing would then fail since the outer nsGlobalWindowOuter::Print call + // wouldn't yet have created the static clone. + setTimeout(makePrintSettingsAndInvokePrint, 0); + + return browser; + }, + + togglePrintPreview(aBrowsingContext) { + let dialogBox = this.getTabDialogBox(aBrowsingContext.top.embedderElement); + let dialogs = dialogBox.getTabDialogManager().dialogs; + let previewDialog = dialogs.find(d => + d._box.querySelector(".printSettingsBrowser") + ); + if (previewDialog) { + previewDialog.close(); + return; + } + this.startPrintWindow(aBrowsingContext); + }, + + /** + * Called when a content process has created a new BrowsingContext for a + * static clone of a document that is to be printed, but we do NOT yet have a + * CanonicalBrowsingContext counterpart in the parent process. This only + * happens in the following cases: + * + * - content script invoked window.print() in the content process, or: + * - silent printing is enabled, and UI code previously invoked + * startPrintWindow which called BrowsingContext.print(), and we're now + * being called back by the content process to parent the static clone. + * + * In the latter case we only need to create the CanonicalBrowsingContext, + * link it to it's content process counterpart, and inserted it into + * the document tree; the print in the content process has already been + * initiated. + * + * In the former case we additionally need to check if we should open the + * tab modal print UI (if not silent printing), obtain a valid + * nsIPrintSettings object, and tell the content process to initiate the + * print with this settings object. + */ + handleStaticCloneCreatedForPrint(aOpenWindowInfo) { + let browsingContext = aOpenWindowInfo.parent; + if (aOpenWindowInfo.isForWindowDotPrint) { + return this.startPrintWindow(browsingContext, { + windowDotPrintOpenWindowInfo: aOpenWindowInfo, + }); + } + return this.createParentBrowserForStaticClone( + browsingContext, + aOpenWindowInfo + ); + }, + + createParentBrowserForStaticClone(aBrowsingContext, aOpenWindowInfo) { + // XXX This code is only called when silent printing, so we're really + // abusing PrintPreview here. See bug 1768020. + let printPreview = new PrintPreview({ + sourceBrowsingContext: aBrowsingContext, + openWindowInfo: aOpenWindowInfo, + }); + let browser = printPreview.createPreviewBrowser("source"); + document.documentElement.append(browser); + return browser; + }, + + // "private" methods and members. Don't use them. + + _getErrorCodeForNSResult(nsresult) { + const MSG_CODES = [ + "GFX_PRINTER_NO_PRINTER_AVAILABLE", + "GFX_PRINTER_NAME_NOT_FOUND", + "GFX_PRINTER_COULD_NOT_OPEN_FILE", + "GFX_PRINTER_STARTDOC", + "GFX_PRINTER_ENDDOC", + "GFX_PRINTER_STARTPAGE", + "GFX_PRINTER_DOC_IS_BUSY", + "ABORT", + "NOT_AVAILABLE", + "NOT_IMPLEMENTED", + "OUT_OF_MEMORY", + "UNEXPECTED", + ]; + + for (let code of MSG_CODES) { + let nsErrorResult = "NS_ERROR_" + code; + if (Cr[nsErrorResult] == nsresult) { + return code; + } + } + + // PERR_FAILURE is the catch-all error message if we've gotten one that + // we don't recognize. + return "FAILURE"; + }, + + _displayPrintingError(nsresult, isPrinting, browser) { + // The nsresults from a printing error are mapped to strings that have + // similar names to the errors themselves. For example, for error + // NS_ERROR_GFX_PRINTER_NO_PRINTER_AVAILABLE, the name of the string + // for the error message is: PERR_GFX_PRINTER_NO_PRINTER_AVAILABLE. What's + // more, if we're in the process of doing a print preview, it's possible + // that there are strings specific for print preview for these errors - + // if so, the names of those strings have _PP as a suffix. It's possible + // that no print preview specific strings exist, in which case it is fine + // to fall back to the original string name. + let msgName = "PERR_" + this._getErrorCodeForNSResult(nsresult); + let msg, title; + if (!isPrinting) { + // Try first with _PP suffix. + let ppMsgName = msgName + "_PP"; + try { + msg = this._bundle.GetStringFromName(ppMsgName); + } catch (e) { + // We allow localizers to not have the print preview error string, + // and just fall back to the printing error string. + } + } + + if (!msg) { + msg = this._bundle.GetStringFromName(msgName); + } + + title = this._bundle.GetStringFromName( + isPrinting + ? "print_error_dialog_title" + : "printpreview_error_dialog_title" + ); + + Services.prompt.asyncAlert( + browser.browsingContext, + Services.prompt.MODAL_TYPE_TAB, + title, + msg + ); + + Services.telemetry.keyedScalarAdd( + "printing.error", + this._getErrorCodeForNSResult(nsresult), + 1 + ); + }, + + getPrintSettings(aPrinterName, aDefaultsOnly, aAllowPseudoPrinter = true) { + var printSettings; + try { + var PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService( + Ci.nsIPrintSettingsService + ); + + function isValidPrinterName(aPrinterName) { + return ( + aPrinterName && + (aAllowPseudoPrinter || + aPrinterName != PrintUtils.SAVE_TO_PDF_PRINTER) + ); + } + + // We must not try to print using an nsIPrintSettings without a printer + // name set. + const printerName = (function () { + if (isValidPrinterName(aPrinterName)) { + return aPrinterName; + } + if (isValidPrinterName(PSSVC.lastUsedPrinterName)) { + return PSSVC.lastUsedPrinterName; + } + return Cc["@mozilla.org/gfx/printerlist;1"].getService( + Ci.nsIPrinterList + ).systemDefaultPrinterName; + })(); + + printSettings = PSSVC.createNewPrintSettings(); + printSettings.printerName = printerName; + + // First get any defaults from the printer. We want to skip this for Save + // to PDF since it isn't a real printer and will throw. + if (printSettings.printerName != this.SAVE_TO_PDF_PRINTER) { + PSSVC.initPrintSettingsFromPrinter( + printSettings.printerName, + printSettings + ); + } + + if (!aDefaultsOnly) { + // Apply any settings that have been saved for this printer. + PSSVC.initPrintSettingsFromPrefs( + printSettings, + true, + printSettings.kInitSaveAll + ); + } + } catch (e) { + console.error("PrintUtils.getPrintSettings failed: ", e, "\n"); + } + return printSettings; + }, + + // Show the system print dialog, saving modified preferences. + // Returns true if the user clicked print (Not cancel). + async handleSystemPrintDialog(aWindow, aHasSelection, aSettings) { + // Prompt the user to choose a printer and make any desired print + // settings changes. + try { + const svc = Cc["@mozilla.org/widget/printdialog-service;1"].getService( + Ci.nsIPrintDialogService + ); + await svc.showPrintDialog(aWindow, aHasSelection, aSettings); + } catch (e) { + if (e.result == Cr.NS_ERROR_ABORT) { + return false; + } + throw e; + } + + // Update the saved last used printer name and print settings: + var PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService( + Ci.nsIPrintSettingsService + ); + PSSVC.maybeSaveLastUsedPrinterNameToPrefs(aSettings.printerName); + PSSVC.maybeSavePrintSettingsToPrefs( + aSettings, + Ci.nsIPrintSettings.kPrintDialogPersistSettings + ); + return true; + }, +}; + +class PrintPreview extends MozElements.BaseControl { + constructor({ + sourceBrowsingContext, + settingsBrowser, + topBrowsingContext, + activeBrowsingContext, + openWindowInfo, + printFrameOnly, + }) { + super(); + this.sourceBrowsingContext = sourceBrowsingContext; + this.settingsBrowser = settingsBrowser; + this.topBrowsingContext = topBrowsingContext; + this.activeBrowsingContext = activeBrowsingContext; + this.openWindowInfo = openWindowInfo; + this.printFrameOnly = printFrameOnly; + + this.printSelectionOnly = false; + this.simplifyPage = false; + this.sourceBrowser = null; + this.selectionBrowser = null; + this.simplifiedBrowser = null; + this.lastPreviewBrowser = null; + } + + connectedCallback() { + if (this.childElementCount > 0) { + return; + } + this.setAttribute("flex", "1"); + this.append( + MozXULElement.parseXULToFragment(` + <stack class="previewStack" rendering="true" flex="1" previewtype="primary"> + <vbox class="previewRendering" flex="1"> + <h1 class="print-pending-label" data-l10n-id="printui-loading"></h1> + </vbox> + <html:printpreview-pagination class="printPreviewNavigation"></html:printpreview-pagination> + </stack> + `) + ); + this.stack = this.firstElementChild; + this.paginator = this.querySelector("printpreview-pagination"); + + if (this.openWindowInfo) { + // For window.print() we need a browser right away for the contents to be + // cloned into, create it now. + this.createPreviewBrowser("source"); + } + } + + disconnectedCallback() { + this.exitPrintPreview(); + } + + getSourceBrowsingContext() { + if (this.openWindowInfo) { + // If openWindowInfo is set this was for window.print() and the source + // contents have already been cloned into the preview browser. + return this.sourceBrowser.browsingContext; + } + return this.sourceBrowsingContext; + } + + get currentBrowsingContext() { + return this.lastPreviewBrowser.browsingContext; + } + + exitPrintPreview() { + this.sourceBrowser?.frameLoader?.exitPrintPreview(); + this.simplifiedBrowser?.frameLoader?.exitPrintPreview(); + this.selectionBrowser?.frameLoader?.exitPrintPreview(); + + this.textContent = ""; + } + + async printPreview(settings, { sourceVersion, sourceURI }) { + this.stack.setAttribute("rendering", true); + + let result = await this._printPreview(settings, { + sourceVersion, + sourceURI, + }); + + let browser = this.lastPreviewBrowser; + this.stack.setAttribute("previewtype", browser.getAttribute("previewtype")); + browser.setAttribute("sheet-count", result.sheetCount); + // The view resets to the top of the document on update bug 1686737. + browser.setAttribute("current-page", 1); + this.paginator.observePreviewBrowser(browser); + await document.l10n.translateElements([browser]); + + this.stack.removeAttribute("rendering"); + + return result; + } + + async _printPreview(settings, { sourceVersion, sourceURI }) { + let printSelectionOnly = sourceVersion == "selection"; + let simplifyPage = sourceVersion == "simplified"; + let selectionTypeBrowser; + let previewBrowser; + + // Select the existing preview browser elements, these could be null. + if (printSelectionOnly) { + selectionTypeBrowser = this.selectionBrowser; + previewBrowser = this.selectionBrowser; + } else { + selectionTypeBrowser = this.sourceBrowser; + previewBrowser = simplifyPage + ? this.simplifiedBrowser + : this.sourceBrowser; + } + + settings.docURL = sourceURI; + + if (previewBrowser) { + this.lastPreviewBrowser = previewBrowser; + if (this.openWindowInfo) { + // We only want to use openWindowInfo for the window.print() browser, + // we can get rid of it now. + this.openWindowInfo = null; + } + // This browser has been rendered already, just update it. + return previewBrowser.frameLoader.printPreview(settings, null); + } + + if (!selectionTypeBrowser) { + // Need to create a non-simplified browser. + selectionTypeBrowser = this.createPreviewBrowser( + simplifyPage ? "source" : sourceVersion + ); + let browsingContext = + printSelectionOnly || this.printFrameOnly + ? this.activeBrowsingContext + : this.topBrowsingContext; + let result = await selectionTypeBrowser.frameLoader.printPreview( + settings, + browsingContext + ); + // If this isn't simplified then we're done. + if (!simplifyPage) { + this.lastPreviewBrowser = selectionTypeBrowser; + return result; + } + } + + // We have the base selection/primary browser but need to simplify. + previewBrowser = this.createPreviewBrowser(sourceVersion); + await previewBrowser.browsingContext.currentWindowGlobal + .getActor("Printing") + .sendQuery("Printing:Preview:ParseDocument", { + URL: sourceURI, + windowID: + selectionTypeBrowser.browsingContext.currentWindowGlobal + .outerWindowId, + }); + + // We've parsed a simplified version into the preview browser. Convert that to + // a print preview as usual. + this.lastPreviewBrowser = previewBrowser; + return previewBrowser.frameLoader.printPreview( + settings, + previewBrowser.browsingContext + ); + } + + createPreviewBrowser(sourceVersion) { + let browser = document.createXULElement("browser"); + let browsingContext = + sourceVersion == "selection" || + this.printFrameOnly || + (sourceVersion == "source" && this.openWindowInfo) + ? this.sourceBrowsingContext + : this.sourceBrowsingContext.top; + if (sourceVersion == "source" && this.openWindowInfo) { + browser.openWindowInfo = this.openWindowInfo; + } else { + let userContextId = browsingContext.originAttributes.userContextId; + if (userContextId) { + browser.setAttribute("usercontextid", userContextId); + } + browser.setAttribute( + "initialBrowsingContextGroupId", + browsingContext.group.id + ); + } + browser.setAttribute("type", "content"); + let remoteType = browsingContext.currentRemoteType; + if (remoteType) { + browser.setAttribute("remoteType", remoteType); + browser.setAttribute("remote", "true"); + } + // When the print process finishes, we get closed by + // nsDocumentViewer::OnDonePrinting, or by the print preview code. + // + // When that happens, we should remove us from the DOM if connected. + browser.addEventListener("DOMWindowClose", function (e) { + if (this.isConnected) { + this.remove(); + } + e.stopPropagation(); + e.preventDefault(); + }); + + if (this.settingsBrowser) { + browser.addEventListener("contextmenu", function (e) { + e.preventDefault(); + }); + + browser.setAttribute("previewtype", sourceVersion); + browser.classList.add("printPreviewBrowser"); + browser.setAttribute("flex", "1"); + browser.setAttribute("printpreview", "true"); + browser.setAttribute("nodefaultsrc", "true"); + document.l10n.setAttributes(browser, "printui-preview-label"); + + this.stack.insertBefore(browser, this.paginator); + + if (sourceVersion == "source") { + this.sourceBrowser = browser; + } else if (sourceVersion == "selection") { + this.selectionBrowser = browser; + } else if (sourceVersion == "simplified") { + this.simplifiedBrowser = browser; + } + } else { + browser.style.visibility = "collapse"; + } + return browser; + } +} +customElements.define("print-preview", PrintPreview); diff --git a/toolkit/components/printing/content/simplifyMode.css b/toolkit/components/printing/content/simplifyMode.css new file mode 100644 index 0000000000..71e5d8fc1c --- /dev/null +++ b/toolkit/components/printing/content/simplifyMode.css @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* This file defines specific rules for print preview when using simplify mode. + * Some of these rules (styling for title and author on the header element) + * already exist in aboutReader.css, however, we decoupled it from the original + * file so we don't need to load a bunch of extra queries that will not take + * effect when using the simplify page checkbox. */ +:root { + --font-size: 16px; + --content-width: 30em; + --line-height: 1.6em; +} + +body { + padding: 0px; +} + +.container { + max-width: 100%; + font-family: Georgia, "Times New Roman", serif; +} + +.header > h1 { + font-size: 1.6em; + line-height: 1.25em; + margin: 30px 0; +} + +.header > .credits { + font-size: 0.9em; + line-height: 1.48em; + margin: 0 0 30px 0; + font-style: italic; +} diff --git a/toolkit/components/printing/content/toggle-group.css b/toolkit/components/printing/content/toggle-group.css new file mode 100644 index 0000000000..94922eb963 --- /dev/null +++ b/toolkit/components/printing/content/toggle-group.css @@ -0,0 +1,79 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +/* + * A radiogroup styled to hide the radio buttons + * and present tab-like labels as buttons + */ + +.toggle-group { + display: inline-flex; +} + +.toggle-group-label { + display: inline-flex; + align-items: center; + margin: 0; + padding: 6px 10px; + background-color: var(--in-content-button-background); +} + +.toggle-group-input { + position: absolute; + inset-inline-start: -100px; +} + +.toggle-group-label-iconic::before { + width: 16px; + height: 16px; + margin-inline-end: 5px; + -moz-context-properties: fill; + fill: currentColor; +} + +.toggle-group-label:first-of-type { + border-start-start-radius: 4px; + border-end-start-radius: 4px; +} + +.toggle-group-label:last-of-type { + border-start-end-radius: 4px; + border-end-end-radius: 4px; +} + +.toggle-group-input:disabled + .toggle-group-label { + opacity: 0.4; +} + +.toggle-group-input:enabled + .toggle-group-label:hover { + background-color: var(--in-content-button-background-hover); + color: var(--in-content-button-text-color-hover); +} + +.toggle-group-input:enabled + .toggle-group-label:hover:active { + background-color: var(--in-content-button-background-active); + color: var(--in-content-button-text-color-active); +} + +.toggle-group-input:focus-visible + .toggle-group-label { + outline: var(--in-content-focus-outline); + outline-offset: var(--in-content-focus-outline-offset); + z-index: 1; +} + +.toggle-group-input:checked + .toggle-group-label { + background-color: var(--in-content-primary-button-background); + color: var(--in-content-primary-button-text-color); +} + +.toggle-group-input:enabled:checked + .toggle-group-label:hover { + background-color: var(--in-content-primary-button-background-hover); + color: var(--in-content-primary-button-text-color-hover); +} + +.toggle-group-input:enabled:checked + .toggle-group-label:hover:active { + background-color: var(--in-content-primary-button-background-active); + color: var(--in-content-primary-button-text-color-active); +} diff --git a/toolkit/components/printing/jar.mn b/toolkit/components/printing/jar.mn new file mode 100644 index 0000000000..43cb119c63 --- /dev/null +++ b/toolkit/components/printing/jar.mn @@ -0,0 +1,17 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +toolkit.jar: +#ifdef XP_WIN + content/global/printPageSetup.js (content/printPageSetup.js) + content/global/printPageSetup.xhtml (content/printPageSetup.xhtml) +#endif + content/global/printPreviewPagination.js (content/printPreviewPagination.js) + content/global/printUtils.js (content/printUtils.js) + content/global/print.js (content/print.js) + content/global/print.html (content/print.html) + content/global/print.css (content/print.css) + content/global/toggle-group.css (content/toggle-group.css) + content/global/simplifyMode.css (content/simplifyMode.css) + content/global/printPagination.css (content/printPagination.css) diff --git a/toolkit/components/printing/moz.build b/toolkit/components/printing/moz.build new file mode 100644 index 0000000000..33ce2241e4 --- /dev/null +++ b/toolkit/components/printing/moz.build @@ -0,0 +1,12 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +JAR_MANIFESTS += ["jar.mn"] + +BROWSER_CHROME_MANIFESTS += ["tests/browser.ini"] + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "Printing") diff --git a/toolkit/components/printing/tests/browser.ini b/toolkit/components/printing/tests/browser.ini new file mode 100644 index 0000000000..e8c015601d --- /dev/null +++ b/toolkit/components/printing/tests/browser.ini @@ -0,0 +1,90 @@ +[DEFAULT] +support-files = + head.js + simplifyArticleSample.html + +[browser_cancel_close_print.js] +[browser_destination_change.js] +[browser_print_settings_fallback.js] +[browser_empty_paper_sizes.js] + +[browser_modal_print.js] +support-files = + file_portrait.html + file_landscape.html + file_first_portrait.html + file_first_landscape.html + +[browser_modal_resize.js] + +[browser_pdf_hidden_settings.js] +support-files = + file_pdf.pdf +[browser_print_copies.js] +[browser_print_paper_sizes.js] +[browser_pdf_printer_settings.js] +[browser_preview_more_settings.js] +[browser_print_bcg_id_overflow.js] +[browser_print_context_menu.js] +[browser_print_disabled.js] +[browser_print_duplex.js] +skip-if = (verify && (os == 'mac')) # bug 1675609 +[browser_print_margins.js] +[browser_print_frame.js] +[browser_print_selection.js] +[browser_print_stream.js] +[browser_print_page_range.js] +[browser_print_page_size.js] +support-files = + page_size.html + page_size_a5.html + page_size_a5_landscape.html + page_size_zero.html + page_size_zero_width.html +[browser_print_pdf_on_frame_load.js] +support-files = + file_print_pdf_on_frame_load.html + file_multi_page_pdf.pdf +[browser_print_scaling.js] +[browser_print_simplified_mode.js] +support-files = + simplifyNonArticleSample.html +[browser_sheet_count.js] +[browser_toolbar_button_toggle.js] +[browser_ui_labels.js] +[browser_window_print.js] +support-files = + file_window_print.html + file_window_print_delayed_during_load.html + file_window_print_oop_iframe.html + file_window_print_sandboxed_iframe.html + file_window_print_another_iframe_and_remove.html + file_window_print_iframe_remove_on_afterprint.html + file_window_print_srcdoc_base_uri.html + file_window_print_reentrant.html + file_coop_header2.html + file_coop_header2.html^headers^ + +[browser_preview_in_container.js] +support-files = + file_print.html + +[browser_preview_navigation.js] +support-files = + longerArticle.html + +[browser_preview_print_coop.js] +support-files = + file_coop_header.html + file_coop_header.html^headers^ + +[browser_preview_print_link_modulepreload.js] +support-files = + file_link_modulepreload.html + +[browser_print_in_container.js] +skip-if = + tsan # Bug 1683730 + os == "linux" && bits == 64 && debug # Bug 1683279 + os == "linux" && asan # Bug 1683279 +[browser_system_dialog_subdialog_hidden.js] diff --git a/toolkit/components/printing/tests/browser_cancel_close_print.js b/toolkit/components/printing/tests/browser_cancel_close_print.js new file mode 100644 index 0000000000..7fc311a33f --- /dev/null +++ b/toolkit/components/printing/tests/browser_cancel_close_print.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function testCloseWhilePrinting() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + await helper.setupMockPrint(); + helper.mockFilePicker("output.pdf"); + + await helper.withClosingFn(async () => { + let cancelButton = helper.get("cancel-button"); + is( + helper.doc.l10n.getAttributes(cancelButton).id, + "printui-cancel-button", + "The cancel button is using the 'cancel' string" + ); + EventUtils.sendKey("return", helper.win); + is( + helper.doc.l10n.getAttributes(cancelButton).id, + "printui-close-button", + "The cancel button is using the 'close' string" + ); + helper.resolvePrint(); + }); + }); +}); diff --git a/toolkit/components/printing/tests/browser_destination_change.js b/toolkit/components/printing/tests/browser_destination_change.js new file mode 100644 index 0000000000..34d435e6df --- /dev/null +++ b/toolkit/components/printing/tests/browser_destination_change.js @@ -0,0 +1,155 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let pdfPrinterName = "Mozilla Save to PDF"; +let fastPrinterName = "Fast"; +let slowPrinterName = "Slow"; + +async function setupPrinters(helper) { + helper.addMockPrinter(fastPrinterName); + + let resolvePrinterInfo; + helper.addMockPrinter({ + name: slowPrinterName, + printerInfoPromise: new Promise(resolve => { + resolvePrinterInfo = resolve; + }), + }); + + await SpecialPowers.pushPrefEnv({ + set: [["print.printer_Slow.print_orientation", 1]], + }); + + return resolvePrinterInfo; +} + +async function changeDestination(helper, dir) { + let picker = helper.get("printer-picker"); + let changed = BrowserTestUtils.waitForEvent(picker, "change"); + + let pickerOpened = BrowserTestUtils.waitForSelectPopupShown(window); + picker.focus(); + EventUtils.sendKey("space", helper.win); + await pickerOpened; + EventUtils.sendKey(dir, window); + EventUtils.sendKey("return", window); + await changed; +} + +function assertFormEnabled(form) { + for (let element of form.elements) { + if (element.hasAttribute("disallowed")) { + ok(element.disabled, `${element.id} is disallowed`); + } else { + ok(!element.disabled, `${element.id} is enabled`); + } + } +} + +function assertFormDisabled(form) { + for (let element of form.elements) { + if (element.id == "printer-picker" || element.id == "cancel-button") { + ok(!element.disabled, `${element.id} is enabled`); + } else { + ok(element.disabled, `${element.id} is disabled`); + } + } +} + +add_task(async function testSlowDestinationChange() { + await PrintHelper.withTestPage(async helper => { + let resolvePrinterInfo = await setupPrinters(helper); + await helper.startPrint(); + + let destinationPicker = helper.get("printer-picker"); + let printForm = helper.get("print"); + + info("Changing to fast printer should change settings"); + await helper.assertSettingsChanged( + { printerName: pdfPrinterName, orientation: 0 }, + { printerName: fastPrinterName, orientation: 0 }, + async () => { + await changeDestination(helper, "down"); + is(destinationPicker.value, fastPrinterName, "Fast printer selected"); + // Wait one frame so the print settings promises resolve. + await helper.awaitAnimationFrame(); + assertFormEnabled(printForm); + } + ); + + info("Changing to slow printer should not change settings yet"); + await helper.assertSettingsNotChanged( + { printerName: fastPrinterName, orientation: 0 }, + async () => { + await changeDestination(helper, "down"); + is(destinationPicker.value, slowPrinterName, "Slow printer selected"); + // Wait one frame, since the settings are blocked on resolvePrinterInfo + // the settings shouldn't change. + await helper.awaitAnimationFrame(); + assertFormDisabled(printForm); + } + ); + + await helper.assertSettingsChanged( + { printerName: fastPrinterName, orientation: 0 }, + { printerName: slowPrinterName, orientation: 1 }, + async () => { + resolvePrinterInfo(); + await helper.waitForSettingsEvent(); + assertFormEnabled(printForm); + } + ); + + await helper.closeDialog(); + }); +}); + +add_task(async function testSwitchAwayFromSlowDestination() { + await PrintHelper.withTestPage(async helper => { + let resolvePrinterInfo = await setupPrinters(helper); + await helper.startPrint(); + + let destinationPicker = helper.get("printer-picker"); + let printForm = helper.get("print"); + + // Load the fast printer. + await helper.waitForSettingsEvent(async () => { + await changeDestination(helper, "down"); + }); + await helper.awaitAnimationFrame(); + assertFormEnabled(printForm); + + // "Load" the slow printer. + await changeDestination(helper, "down"); + is(destinationPicker.value, slowPrinterName, "Slow printer selected"); + // Wait an animation frame, since there's no settings event. + await helper.awaitAnimationFrame(); + assertFormDisabled(printForm); + + // Switch back to the fast printer. + await helper.waitForSettingsEvent(async () => { + await changeDestination(helper, "up"); + }); + helper.assertSettingsMatch({ + printerName: fastPrinterName, + orientation: 0, + }); + + await helper.awaitAnimationFrame(); + assertFormEnabled(printForm); + + // Let the slow printer settings resolve, the orientation shouldn't change. + resolvePrinterInfo(); + // Wait so the settings event can trigger, if this case isn't handled. + await helper.awaitAnimationFrame(); + helper.assertSettingsMatch({ + printerName: fastPrinterName, + orientation: 0, + }); + assertFormEnabled(printForm); + + await helper.closeDialog(); + }); +}); diff --git a/toolkit/components/printing/tests/browser_empty_paper_sizes.js b/toolkit/components/printing/tests/browser_empty_paper_sizes.js new file mode 100644 index 0000000000..a7ce568bb8 --- /dev/null +++ b/toolkit/components/printing/tests/browser_empty_paper_sizes.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function testSanityCheckPaperList() { + const mockPrinterName = "Fake Printer"; + await PrintHelper.withTestPage(async helper => { + let paperList = [ + PrintHelper.createMockPaper({ + id: "regular", + name: "Regular Paper", + }), + PrintHelper.createMockPaper({ + id: "large", + name: "Large Size", + width: 720, + height: 1224, + }), + ]; + helper.addMockPrinter({ name: mockPrinterName, paperList }); + await helper.startPrint(); + await helper.dispatchSettingsChange({ printerName: mockPrinterName }); + await helper.waitForSettingsEvent(); + + is( + helper.settings.printerName, + mockPrinterName, + "The Fake Printer is current printer" + ); + is( + Object.values(helper.win.PrintSettingsViewProxy.availablePaperSizes) + .length, + 2, + "There are 2 paper sizes" + ); + ok( + helper.win.PrintSettingsViewProxy.availablePaperSizes.regular, + "'regular' paper size is available" + ); + ok( + helper.win.PrintSettingsViewProxy.availablePaperSizes.large, + "'large' paper size is available" + ); + }); +}); + +add_task(async function testEmptyPaperListGetsFallbackPaperSizes() { + const mockPrinterName = "Fake Printer"; + await PrintHelper.withTestPage(async helper => { + helper.addMockPrinter(mockPrinterName); + await helper.startPrint(); + + is( + Object.values(helper.win.PrintSettingsViewProxy.availablePrinters).length, + 2, + "There are 2 available printers" + ); + ok( + helper.win.PrintSettingsViewProxy.availablePrinters[mockPrinterName], + "The Fake Printer is one of our availablePrinters" + ); + + await helper.dispatchSettingsChange({ printerName: mockPrinterName }); + await helper.waitForSettingsEvent(); + + is( + helper.settings.printerName, + mockPrinterName, + "The Fake Printer is current printer" + ); + is( + helper.get("printer-picker").value, + mockPrinterName, + "The Fake Printer is selected" + ); + + let printerList = Cc["@mozilla.org/gfx/printerlist;1"].createInstance( + Ci.nsIPrinterList + ); + let fallbackPaperList = await printerList.fallbackPaperList; + let paperPickerSizes = Array.from( + helper.get("paper-size-picker").options + ).map(o => o.value); + for (let paper of fallbackPaperList) { + ok( + helper.win.PrintSettingsViewProxy.availablePaperSizes[paper.id], + "Fallback paper size: " + paper.id + " is available" + ); + ok( + paperPickerSizes.includes(paper.id), + "There is a paper size options for " + paper.id + ); + } + await helper.closeDialog(); + }); +}); diff --git a/toolkit/components/printing/tests/browser_modal_print.js b/toolkit/components/printing/tests/browser_modal_print.js new file mode 100644 index 0000000000..c3e3f94874 --- /dev/null +++ b/toolkit/components/printing/tests/browser_modal_print.js @@ -0,0 +1,309 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function assertExpectedPrintPage(helper) { + is( + helper.sourceURI, + PrintHelper.defaultTestPageUrlHTTPS, + "The URL of the browser is the one we expect" + ); +} + +add_task(async function testModalPrintDialog() { + await PrintHelper.withTestPageHTTPS(async helper => { + helper.assertDialogClosed(); + + await helper.startPrint(); + + helper.assertDialogOpen(); + + // Check that we're printing the right page. + assertExpectedPrintPage(helper); + + // Close the dialog with Escape. + await helper.withClosingFn(() => { + EventUtils.synthesizeKey("VK_ESCAPE", {}, helper.win); + }); + + helper.assertDialogClosed(); + }); +}); + +add_task(async function testPrintMultiple() { + await PrintHelper.withTestPageHTTPS(async helper => { + helper.assertDialogClosed(); + + // First print as usual. + await helper.startPrint(); + helper.assertDialogOpen(); + assertExpectedPrintPage(helper); + + // Trigger the command a few more times, verify the overlay still exists. + ignoreAllUncaughtExceptions(true); + for (let i = 0; i < 3; ++i) { + try { + await helper.startPrint(); + } finally { + helper.assertDialogOpen(); + } + } + ignoreAllUncaughtExceptions(false); + + // Verify it's still the correct page. + assertExpectedPrintPage(helper); + + // Make sure we clean up, ideally this would be handled by the helper. + await TestUtils.waitForTick(); + await helper.closeDialog(); + }); +}); + +add_task(async function testCancelButton() { + await PrintHelper.withTestPageHTTPS(async helper => { + helper.assertDialogClosed(); + await helper.startPrint(); + helper.assertDialogOpen(); + + let cancelButton = helper.doc.querySelector("button[name=cancel]"); + ok(cancelButton, "Got the cancel button"); + await helper.withClosingFn(() => + EventUtils.synthesizeMouseAtCenter(cancelButton, {}, helper.win) + ); + + helper.assertDialogClosed(); + }); +}); + +add_task(async function testTabOrder() { + await PrintHelper.withTestPageHTTPS(async helper => { + helper.assertDialogClosed(); + await helper.startPrint(); + helper.assertDialogOpen(); + + const printerPicker = helper.doc.getElementById("printer-picker"); + is( + helper.doc.activeElement, + printerPicker, + "Initial focus on printer picker" + ); + + const previewBrowser = document.querySelector(".printPreviewBrowser"); + ok(previewBrowser, "Got the print preview browser"); + + let focused; + let navigationShadowRoot = document.querySelector( + ".printPreviewNavigation" + ).shadowRoot; + for (let buttonId of [ + "navigateEnd", + "navigateNext", + "navigatePrevious", + "navigateHome", + ]) { + let button = navigationShadowRoot.getElementById(buttonId); + focused = BrowserTestUtils.waitForEvent(button, "focus"); + await EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + await focused; + } + + focused = BrowserTestUtils.waitForEvent(previewBrowser, "focus"); + await EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + await focused; + ok(true, "Print preview focused after shift+tab through the paginator"); + + focused = BrowserTestUtils.waitForEvent(gNavToolbox, "focus", true); + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + await focused; + ok(true, "Toolbox focused after shift+tab"); + + focused = BrowserTestUtils.waitForEvent(previewBrowser, "focus"); + EventUtils.synthesizeKey("KEY_Tab"); + await focused; + ok(true, "Print preview focused after tab"); + + for (let buttonId of [ + "navigateHome", + "navigatePrevious", + "navigateNext", + "navigateEnd", + ]) { + let button = navigationShadowRoot.getElementById(buttonId); + focused = BrowserTestUtils.waitForEvent(button, "focus"); + await EventUtils.synthesizeKey("KEY_Tab"); + await focused; + } + focused = BrowserTestUtils.waitForEvent(printerPicker, "focus"); + EventUtils.synthesizeKey("KEY_Tab"); + await focused; + ok(true, "Printer picker focused after tab"); + + const lastButtonId = + AppConstants.platform == "win" ? "cancel-button" : "print-button"; + const lastButton = helper.doc.getElementById(lastButtonId); + + focused = BrowserTestUtils.waitForEvent(lastButton, "focus"); + lastButton.focus(); + await focused; + ok(true, "Last button focused"); + + focused = BrowserTestUtils.waitForEvent(gNavToolbox, "focus", true); + EventUtils.synthesizeKey("KEY_Tab"); + await focused; + ok(true, "Toolbox focused after tab"); + + focused = BrowserTestUtils.waitForEvent(lastButton, "focus"); + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + await focused; + ok(true, "Last button focused after shift+tab"); + + await helper.withClosingFn(() => { + EventUtils.synthesizeKey("VK_ESCAPE", {}); + }); + + helper.assertDialogClosed(); + }); +}); + +async function testPrintWithEnter(testFn, filename) { + await PrintHelper.withTestPageHTTPS(async helper => { + await helper.startPrint(); + + let file = helper.mockFilePicker(filename); + await testFn(helper); + await helper.assertPrintToFile(file, () => { + EventUtils.sendKey("return", helper.win); + const cancelButton = helper.doc.querySelector(`button[name="cancel"]`); + ok(!cancelButton.disabled, "Cancel button is not disabled"); + const printButton = helper.doc.querySelector(`button[name="print"]`); + ok(printButton.disabled, "Print button is disabled"); + }); + }); +} + +add_task(async function testEnterAfterLoadPrints() { + info("Test print without moving focus"); + await testPrintWithEnter(() => {}, "print_initial_focus.pdf"); +}); + +add_task(async function testEnterPrintsFromPageRangeSelect() { + info("Test print from page range select"); + await testPrintWithEnter(helper => { + let pageRangePicker = helper.get("range-picker"); + pageRangePicker.focus(); + is( + helper.doc.activeElement, + pageRangePicker, + "Page range select is focused" + ); + }, "print_page_range_select.pdf"); +}); + +add_task(async function testEnterPrintsFromOrientation() { + info("Test print on Enter from focused orientation input"); + await testPrintWithEnter(helper => { + let portrait = helper.get("portrait"); + portrait.focus(); + is(helper.doc.activeElement, portrait, "Portrait is focused"); + }, "print_orientation_focused.pdf"); +}); + +add_task(async function testPrintOnNewWindowDoesntClose() { + info("Test that printing doesn't close a window with a single tab"); + let win = await BrowserTestUtils.openNewBrowserWindow(); + + await PrintHelper.withTestPageHTTPS(async helper => { + await helper.startPrint(); + let file = helper.mockFilePicker("print_new_window_close.pdf"); + await helper.assertPrintToFile(file, () => { + EventUtils.sendKey("return", helper.win); + }); + }); + ok(!win.closed, "Shouldn't be closed"); + await BrowserTestUtils.closeWindow(win); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function testPrintProgressIndicator() { + await PrintHelper.withTestPageHTTPS(async helper => { + await helper.startPrint(); + + helper.setupMockPrint(); + + let progressIndicator = helper.get("print-progress"); + ok(progressIndicator.hidden, "Progress indicator is hidden"); + + let indicatorShown = BrowserTestUtils.waitForAttributeRemoval( + "hidden", + progressIndicator + ); + helper.click(helper.get("print-button")); + await indicatorShown; + + ok(!progressIndicator.hidden, "Progress indicator is shown on print start"); + + await helper.withClosingFn(async () => { + await helper.resolvePrint(); + }); + }); +}); + +add_task(async function testPageSizePortrait() { + await PrintHelper.withTestPageHTTPS(async helper => { + await helper.startPrint(); + + let orientation = helper.get("orientation"); + ok(orientation.hidden, "Orientation selector is hidden"); + + is( + helper.settings.orientation, + Ci.nsIPrintSettings.kPortraitOrientation, + "Orientation set to portrait" + ); + }, "file_portrait.html"); +}); + +add_task(async function testPageSizeLandscape() { + await PrintHelper.withTestPageHTTPS(async helper => { + await helper.startPrint(); + + let orientation = helper.get("orientation"); + ok(orientation.hidden, "Orientation selector is hidden"); + + is( + helper.settings.orientation, + Ci.nsIPrintSettings.kLandscapeOrientation, + "Orientation set to landscape" + ); + }, "file_landscape.html"); +}); + +add_task(async function testFirstPageSizePortrait() { + await PrintHelper.withTestPageHTTPS(async helper => { + await helper.startPrint(); + + let orientation = helper.get("orientation"); + ok(orientation.hidden, "Orientation selector is hidden"); + + is( + helper.settings.orientation, + Ci.nsIPrintSettings.kPortraitOrientation, + "Orientation set to portrait" + ); + }, "file_first_portrait.html"); +}); + +add_task(async function testFirstPageSizeLandscape() { + await PrintHelper.withTestPageHTTPS(async helper => { + await helper.startPrint(); + + let orientation = helper.get("orientation"); + ok(orientation.hidden, "Orientation selector is hidden"); + + is( + helper.settings.orientation, + Ci.nsIPrintSettings.kLandscapeOrientation, + "Orientation set to landscape" + ); + }, "file_first_landscape.html"); +}); diff --git a/toolkit/components/printing/tests/browser_modal_resize.js b/toolkit/components/printing/tests/browser_modal_resize.js new file mode 100644 index 0000000000..5163b5bdaf --- /dev/null +++ b/toolkit/components/printing/tests/browser_modal_resize.js @@ -0,0 +1,178 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function waitForAnimationFrames() { + // Wait for 2 animation frames in hopes it's actually done resizing. + return new Promise(resolve => + window.requestAnimationFrame(() => window.requestAnimationFrame(resolve)) + ); +} + +async function mouseMoveAndWait(elem) { + let mouseMovePromise = BrowserTestUtils.waitForEvent(elem, "mousemove"); + EventUtils.synthesizeMouseAtCenter(elem, { type: "mousemove" }); + await mouseMovePromise; + await TestUtils.waitForTick(); +} + +function closeEnough(actual, expected) { + return expected - 1 < actual && actual < expected + 1; +} + +async function resizeWindow(x, y) { + let resizePromise = BrowserTestUtils.waitForEvent(window, "resize"); + window.resizeTo( + x + window.outerWidth - window.innerWidth, + y + window.outerHeight - window.innerHeight + ); + await resizePromise; + + await waitForAnimationFrames(); + + ok( + closeEnough(window.innerWidth, x), + `Window innerWidth ${window.innerWidth} is close enough to ${x}` + ); + ok( + closeEnough(window.innerHeight, y), + `Window innerHeight ${window.innerHeight} is close enough to ${y}` + ); +} + +async function waitForExpectedSize(helper, x, y) { + // Wait a few frames, this is generally enough for the resize to happen. + await waitForAnimationFrames(); + + let isExpectedSize = () => { + let box = helper.dialog._box; + info(`Dialog is ${box.clientWidth}x${box.clientHeight}`); + if (closeEnough(box.clientWidth, x) && closeEnough(box.clientHeight, y)) { + // Make sure there's an assertion so the test passes. + ok(true, `${box.clientWidth} close enough to ${x}`); + ok(true, `${box.clientHeight} close enough to ${y}`); + return true; + } + return false; + }; + + if (isExpectedSize()) { + // We can stop now if we hit the expected size. + return; + } + + // In verify and debug runs sometimes this takes longer than expected, + // fallback to the slow method. + await TestUtils.waitForCondition(isExpectedSize, `Wait for ${x}x${y}`); +} + +async function checkPreviewNavigationVisibility(expected) { + function isHidden(elem) { + // BTU.is_hidden can't handle shadow DOM elements + return !elem.getBoundingClientRect().height; + } + + let previewStack = document.querySelector(".previewStack"); + let paginationElem = document.querySelector(".printPreviewNavigation"); + // move the mouse to a known position, then back to the preview to show the paginator + await mouseMoveAndWait(gURLBar.textbox); + await mouseMoveAndWait(previewStack); + + ok( + BrowserTestUtils.is_visible(paginationElem), + "The preview pagination toolbar is visible" + ); + for (let [id, visible] of Object.entries(expected)) { + let elem = paginationElem.shadowRoot.querySelector(`#${id}`); + if (visible) { + ok(!isHidden(elem), `Navigation element ${id} is visible`); + } else { + ok(isHidden(elem), `Navigation element ${id} is hidden`); + } + } +} + +add_task(async function testResizing() { + if (window.windowState != window.STATE_NORMAL) { + todo_is( + window.windowState, + window.STATE_NORMAL, + "windowState should be STATE_NORMAL" + ); + // On Windows the size of the window decoration depends on the size mode. + // Trying to set the inner size of a maximized window changes the size mode + // but calculates the new window size with the maximized window + // decorations. On Linux a maximized window can also cause problems when + // the window was maximized recently and the corresponding resize event is + // still outstanding. + window.restore(); + // On Linux we would have to wait for the resize event here, but the + // restored and maximized size can also be equal. Brute forcing a resize + // to a specific size works around that. + await BrowserTestUtils.waitForCondition(async () => { + let width = window.screen.availWidth * 0.75; + let height = window.screen.availHeight * 0.75; + window.resizeTo(width, height); + return ( + closeEnough(window.outerWidth, width) && + closeEnough(window.outerHeight, height) + ); + }); + } + + await PrintHelper.withTestPage(async helper => { + let { innerWidth, innerHeight } = window; + + await resizeWindow(500, 400); + + await helper.startPrint(); + + let chromeHeight = window.windowUtils.getBoundsWithoutFlushing( + document.getElementById("browser") + ).top; + + let initialWidth = 500 - 8; + let initialHeight = 400 - 16 - chromeHeight + 5; + + await waitForExpectedSize(helper, initialWidth, initialHeight); + + // check the preview pagination state for this window size + await checkPreviewNavigationVisibility({ + navigateHome: false, + navigatePrevious: false, + navigateNext: false, + navigateEnd: false, + sheetIndicator: true, + }); + + await resizeWindow(600, 500); + + await checkPreviewNavigationVisibility({ + navigateHome: true, + navigatePrevious: true, + navigateNext: true, + navigateEnd: true, + sheetIndicator: true, + }); + + // 100 wider for window, add back the old 4px padding, it's now 16px * 2. + let updatedWidth = initialWidth + 100 + 8 - 32; + await waitForExpectedSize(helper, updatedWidth, initialHeight + 100); + + await resizeWindow(1100, 900); + + await waitForExpectedSize(helper, 1000, 650); + + await checkPreviewNavigationVisibility({ + navigateHome: true, + navigatePrevious: true, + navigateNext: true, + navigateEnd: true, + sheetIndicator: true, + }); + + await helper.closeDialog(); + + await resizeWindow(innerWidth, innerHeight); + }); +}); diff --git a/toolkit/components/printing/tests/browser_pdf_hidden_settings.js b/toolkit/components/printing/tests/browser_pdf_hidden_settings.js new file mode 100644 index 0000000000..2967d91882 --- /dev/null +++ b/toolkit/components/printing/tests/browser_pdf_hidden_settings.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const hiddenPdfIds = ["backgrounds", "source-version-selection"]; + +async function checkElements({ removed, file, testName }) { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + + for (let id of hiddenPdfIds) { + is( + !helper.get(id), + removed, + `${id} is ${removed ? "" : "not "}removed (${testName})` + ); + } + + await helper.closeDialog(); + }, file); +} + +add_task(async function testSettingsShownForNonPdf() { + await checkElements({ removed: false, testName: "non-pdf" }); +}); + +add_task(async function testSettingsHiddenForPdf() { + await checkElements({ + removed: true, + file: "file_pdf.pdf", + testName: "pdf", + }); +}); diff --git a/toolkit/components/printing/tests/browser_pdf_printer_settings.js b/toolkit/components/printing/tests/browser_pdf_printer_settings.js new file mode 100644 index 0000000000..16a7d86600 --- /dev/null +++ b/toolkit/components/printing/tests/browser_pdf_printer_settings.js @@ -0,0 +1,125 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function testPDFPrinterSettings() { + await PrintHelper.withTestPage(async helper => { + // Set some bad prefs + await SpecialPowers.pushPrefEnv({ + set: [ + ["print.print_to_file", false], + ["print.print_in_color", false], + ["print.printer_Mozilla_Save_to_PDF.print_to_file", false], + ["print.printer_Mozilla_Save_to_PDF.print_in_color", false], + ], + }); + + await helper.startPrint(); + await helper.awaitAnimationFrame(); + + // Verify we end up with sane settings + let { settings } = helper; + + ok( + settings.outputDestination == Ci.nsIPrintSettings.kOutputDestinationFile, + "Check the current settings have file destination" + ); + ok( + settings.printInColor, + "Check the current settings have a truthy printInColor for the PDF printer" + ); + is( + settings.outputFormat, + Ci.nsIPrintSettings.kOutputFormatPDF, + "The PDF printer has the correct outputFormat" + ); + + await helper.closeDialog(); + await SpecialPowers.popPrefEnv(); + }); +}); + +add_task(async function testPDFCancel() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + helper.mockFilePickerCancel(); + let form = helper.doc.querySelector("#print"); + + // retrieve all elements other than cancel button + let elements = []; + for (let element of form.elements) { + if (element.name != "cancel") { + elements.push(element); + } + } + let getDisabledStates = () => elements.map(el => el.disabled); + let initialDisabledStates = getDisabledStates(); + + ok( + initialDisabledStates.some(disabled => !disabled), + "At least one enabled form element before submitting" + ); + let getShownDisabledStates = new Promise(resolve => { + MockFilePicker.showCallback = () => resolve(getDisabledStates()); + }); + + EventUtils.sendKey("return", helper.win); + + let shownDisabledStates = await getShownDisabledStates; + ok(shownDisabledStates, "Got disabled states while shown"); + ok( + shownDisabledStates.every(disabled => disabled), + "All elements were disabled when showing picker" + ); + let cancelButton = helper.doc.querySelector(`button[name="cancel"]`); + ok(!cancelButton.disabled, "Cancel button is still enabled"); + + let saveButton = form.querySelector("#print-button"); + await BrowserTestUtils.waitForAttributeRemoval("disabled", saveButton); + helper.assertDialogOpen(); + + is( + getDisabledStates().every( + (disabledState, index) => disabledState === initialDisabledStates[index] + ), + true, + "Previous disabled states match after returning to preview" + ); + + // Close the dialog with Escape. + await helper.withClosingFn(() => { + EventUtils.synthesizeKey("VK_ESCAPE", {}, helper.win); + }); + + helper.assertDialogClosed(); + }); +}); + +add_task(async function testPDFFile() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + + helper.mockFilePicker("pdfFile.pdf"); + let filePath = PathUtils.join( + Services.dirsvc.get("TmpD", Ci.nsIFile).path, + "pdfFile.pdf" + ); + + await helper.withClosingFn(() => { + EventUtils.sendKey("return", helper.win); + }); + + try { + Services.prefs.getStringPref( + "print.printer_Mozilla_Save_to_PDF.print_to_filename" + ); + ok(false, "Should have cleared the filename pref"); + } catch (ex) { + ok(true, "Cleared the filename pref"); + } + + is(await IOUtils.exists(filePath), true, "Saved pdf file exists"); + ok(await IOUtils.read(filePath), "Saved pdf file is not empty"); + }); +}); diff --git a/toolkit/components/printing/tests/browser_preview_in_container.js b/toolkit/components/printing/tests/browser_preview_in_container.js new file mode 100644 index 0000000000..297da14878 --- /dev/null +++ b/toolkit/components/printing/tests/browser_preview_in_container.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +async function runTest() { + is( + document.querySelector("print-preview"), + null, + "There shouldn't be any print preview browser" + ); + + gBrowser.selectedTab = await BrowserTestUtils.addTab( + gBrowser, + `${TEST_PATH}file_print.html`, + { userContextId: 1 } + ); + + // Wait for window.print() to run and ensure we're showing the preview... + await waitForPreviewVisible(); + + let printPreviewEl = document.querySelector("print-preview"); + await BrowserTestUtils.waitForCondition( + () => !!printPreviewEl.settingsBrowser.contentWindow._initialized + ); + await printPreviewEl.settingsBrowser.contentWindow._initialized; + let contentFound = await SpecialPowers.spawn( + printPreviewEl.sourceBrowser, + [], + () => { + return !!content.document.getElementById("printed"); + } + ); + ok(contentFound, "We should find the preview content."); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +} + +add_task(async function test_in_container() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.firstparty.isolate", false]], + }); + + await runTest(); +}); + +add_task(async function test_with_fpi() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.firstparty.isolate", true]], + }); + await runTest(); +}); diff --git a/toolkit/components/printing/tests/browser_preview_more_settings.js b/toolkit/components/printing/tests/browser_preview_more_settings.js new file mode 100644 index 0000000000..3591e66164 --- /dev/null +++ b/toolkit/components/printing/tests/browser_preview_more_settings.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function moreSettingsHonorPref() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + + ok(!helper.get("more-settings").open, "More settings is closed"); + + await helper.openMoreSettings(); + ok( + Services.prefs.getBoolPref("print.more-settings.open"), + "More settings pref has been flipped to true" + ); + + await helper.closeDialog(); + + await helper.startPrint(); + + ok(helper.get("more-settings").open, "More settings is open"); + + helper.click(helper.get("more-settings").firstElementChild); + await helper.awaitAnimationFrame(); + ok( + !Services.prefs.getBoolPref("print.more-settings.open"), + "More settings pref has been flipped to false" + ); + + await helper.closeDialog(); + + await helper.startPrint(); + + ok(!helper.get("more-settings").open, "More settings is closed"); + + await helper.closeDialog(); + }); +}); diff --git a/toolkit/components/printing/tests/browser_preview_navigation.js b/toolkit/components/printing/tests/browser_preview_navigation.js new file mode 100644 index 0000000000..1dc23fa9f4 --- /dev/null +++ b/toolkit/components/printing/tests/browser_preview_navigation.js @@ -0,0 +1,478 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function compare10nArgs(elem, expectedValues) { + let l10nArgs = elem.ownerDocument.l10n.getAttributes(elem).args; + for (let [name, value] of Object.entries(expectedValues)) { + if (value !== l10nArgs[name]) { + info( + `compare10nArgs, expected ${name}: ${value}, actual: ${l10nArgs[name]}` + ); + return false; + } + } + return true; +} + +async function waitForPageStatusUpdate(elem, expected, message) { + await TestUtils.waitForCondition( + () => compare10nArgs(elem, expected), + message + ); +} + +async function waitUntilVisible(elem, visible = true) { + await TestUtils.waitForCondition( + () => + BrowserTestUtils.is_visible(elem) && + getComputedStyle(elem).opacity == "1", + "Waiting for element to be visible and have opacity:1" + ); +} + +async function waitUntilTransparent(elem) { + // Note that is_visible considers a fully transparent element "visible" + await TestUtils.waitForCondition( + () => getComputedStyle(elem).opacity == "0", + "Waiting for element to be have opacity:0" + ); +} + +async function mouseMoveAndWait(elem) { + let mouseMovePromise = BrowserTestUtils.waitForEvent(elem, "mousemove"); + EventUtils.synthesizeMouseAtCenter(elem, { type: "mousemove" }); + await mouseMovePromise; + await TestUtils.waitForTick(); +} + +add_task(async function testToolbarVisibility() { + // move the mouse to a known position + await mouseMoveAndWait(gURLBar.textbox); + + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + + let previewStack = document.querySelector(".previewStack"); + + // The toolbar has 0 opacity until we hover or focus it + is( + getComputedStyle(helper.paginationElem).opacity, + "0", + "Initially transparent" + ); + + let visiblePromise = waitUntilVisible(helper.paginationElem); + helper.paginationElem.shadowRoot.querySelector("#navigateEnd").focus(); + await visiblePromise; + is( + getComputedStyle(helper.paginationElem).opacity, + "1", + "Opaque with button focused" + ); + + await EventUtils.synthesizeKey("KEY_Tab", {}); + await waitUntilTransparent(helper.paginationElem); + is( + getComputedStyle(helper.paginationElem).opacity, + "0", + "Returns to transparent" + ); + + visiblePromise = waitUntilVisible(helper.paginationElem); + info("Waiting for mousemove event, and for the toolbar to become opaque"); + await mouseMoveAndWait(previewStack); + await visiblePromise; + is(getComputedStyle(helper.paginationElem).opacity, "1", "Opaque toolbar"); + + // put the mouse back where it won't interfere with later tests + await mouseMoveAndWait(gURLBar.textbox); + await helper.closeDialog(); + }); +}); + +add_task(async function testPreviewSheetCount() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + + // We have to wait for the first _updatePrintPreview to get the sheet count + await waitForPageStatusUpdate( + helper.paginationSheetIndicator, + { sheetNum: 1, sheetCount: 3 }, + "Paginator indicates the correct number of sheets" + ); + + // then switch to page range 1-1 and verify page count changes + await helper.dispatchSettingsChange({ + pageRanges: ["1", "1"], + }); + await waitForPageStatusUpdate( + helper.paginationSheetIndicator, + { sheetNum: 1, sheetCount: 1 }, + "Indicates the updated number of sheets" + ); + + await helper.closeDialog(); + }, "longerArticle.html"); +}); + +add_task(async function testPreviewScroll() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + + // Wait for the first _updatePrintPreview before interacting with the preview + await waitForPageStatusUpdate( + helper.paginationSheetIndicator, + { sheetNum: 1, sheetCount: 3 }, + "Paginator indicates the correct number of sheets" + ); + let previewBrowser = helper.currentPrintPreviewBrowser; + + // scroll down the document + // and verify the indicator is updated correctly + await SpecialPowers.spawn(previewBrowser, [], async function () { + const { ContentTaskUtils } = ChromeUtils.importESModule( + "resource://testing-common/ContentTaskUtils.sys.mjs" + ); + const EventUtils = ContentTaskUtils.getEventUtils(content); + content.focus(); + EventUtils.synthesizeKey("VK_PAGE_DOWN", {}, content); + }); + await waitForPageStatusUpdate( + helper.paginationSheetIndicator, + { sheetNum: 2, sheetCount: 3 }, + "Indicator updates on scroll" + ); + + // move focus before closing the dialog + helper.get("cancel-button").focus(); + await helper.closeDialog(); + }, "longerArticle.html"); +}); + +add_task(async function testPreviewNavigationCommands() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + + // Wait for the first _updatePrintPreview before interacting with the preview + await waitForPageStatusUpdate( + helper.paginationSheetIndicator, + { sheetNum: 1, sheetCount: 3 }, + "Paginator indicates the correct number of sheets" + ); + + // click the navigation buttons + // and verify the indicator is updated correctly + EventUtils.synthesizeMouseAtCenter( + helper.paginationElem.shadowRoot.querySelector("#navigateNext"), + {} + ); + await waitForPageStatusUpdate( + helper.paginationSheetIndicator, + { sheetNum: 2, sheetCount: 3 }, + "Indicator updates on navigation to next" + ); + + EventUtils.synthesizeMouseAtCenter( + helper.paginationElem.shadowRoot.querySelector("#navigatePrevious"), + {} + ); + await waitForPageStatusUpdate( + helper.paginationSheetIndicator, + { sheetNum: 1, sheetCount: 3 }, + "Indicator updates on navigation to previous" + ); + + EventUtils.synthesizeMouseAtCenter( + helper.paginationElem.shadowRoot.querySelector("#navigateEnd"), + {} + ); + await waitForPageStatusUpdate( + helper.paginationSheetIndicator, + { sheetNum: 3, sheetCount: 3 }, + "Indicator updates on navigation to end" + ); + + EventUtils.synthesizeMouseAtCenter( + helper.paginationElem.shadowRoot.querySelector("#navigateHome"), + {} + ); + await waitForPageStatusUpdate( + helper.paginationSheetIndicator, + { sheetNum: 1, sheetCount: 3 }, + "Indicator updates on navigation to start" + ); + + // Test rapid clicks on the navigation buttons + EventUtils.synthesizeMouseAtCenter( + helper.paginationElem.shadowRoot.querySelector("#navigateNext"), + {} + ); + EventUtils.synthesizeMouseAtCenter( + helper.paginationElem.shadowRoot.querySelector("#navigateNext"), + {} + ); + await waitForPageStatusUpdate( + helper.paginationSheetIndicator, + { sheetNum: 3, sheetCount: 3 }, + "2 successive 'next' clicks correctly update the sheet indicator" + ); + + EventUtils.synthesizeMouseAtCenter( + helper.paginationElem.shadowRoot.querySelector("#navigatePrevious"), + {} + ); + EventUtils.synthesizeMouseAtCenter( + helper.paginationElem.shadowRoot.querySelector("#navigatePrevious"), + {} + ); + await waitForPageStatusUpdate( + helper.paginationSheetIndicator, + { sheetNum: 1, sheetCount: 3 }, + "2 successive 'previous' clicks correctly update the sheet indicator" + ); + + // move focus before closing the dialog + helper.get("cancel-button").focus(); + await helper.closeDialog(); + }, "longerArticle.html"); +}); + +add_task(async function testMultiplePreviewNavigation() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + const tab1 = gBrowser.selectedTab; + + await waitForPageStatusUpdate( + helper.paginationSheetIndicator, + { sheetNum: 1, sheetCount: 3 }, + "Indicator has the correct initial sheetCount" + ); + + const tab2 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + PrintHelper.defaultTestPageUrl + ); + let helper2 = new PrintHelper(tab2.linkedBrowser); + await helper2.startPrint(); + + let [previewBrowser1, previewBrowser2] = document.querySelectorAll( + ".printPreviewBrowser[previewtype='source']" + ); + ok(previewBrowser1 && previewBrowser2, "There are 2 preview browsers"); + + let [toolbar1, toolbar2] = document.querySelectorAll( + ".printPreviewNavigation" + ); + ok(toolbar1 && toolbar2, "There are 2 preview navigation toolbars"); + is( + toolbar1.previewBrowser, + previewBrowser1, + "toolbar1 has the correct previewBrowser" + ); + ok( + compare10nArgs(helper.paginationSheetIndicator, { + sheetNum: 1, + sheetCount: 3, + }), + "First toolbar has the correct content" + ); + + is( + toolbar2.previewBrowser, + previewBrowser2, + "toolbar2 has the correct previewBrowser" + ); + ok( + compare10nArgs(helper2.paginationSheetIndicator, { + sheetNum: 1, + sheetCount: 1, + }), + "2nd toolbar has the correct content" + ); + + // Switch back to the first tab and ensure the correct preview navigation is updated when clicked + await BrowserTestUtils.switchTab(gBrowser, tab1); + + EventUtils.synthesizeMouseAtCenter( + toolbar1.shadowRoot.querySelector("#navigateNext"), + {} + ); + await waitForPageStatusUpdate( + helper.paginationSheetIndicator, + { sheetNum: 2, sheetCount: 3 }, + "Indicator updates on navigation multiple" + ); + + gBrowser.removeTab(tab2); + }, "longerArticle.html"); +}); + +add_task(async function testPreviewNavigationSelection() { + await PrintHelper.withTestPage(async helper => { + await SpecialPowers.spawn(helper.sourceBrowser, [], async function () { + let element = content.document.querySelector("#page-2"); + content.window.getSelection().selectAllChildren(element); + }); + + await helper.startPrint(); + + // Wait for the first _updatePrintPreview before interacting with the preview + await waitForPageStatusUpdate( + helper.paginationSheetIndicator, + { sheetNum: 1, sheetCount: 3 }, + "Paginator indicates the correct number of sheets" + ); + + // click a navigation button + // and verify the indicator is updated correctly + EventUtils.synthesizeMouseAtCenter( + helper.paginationElem.shadowRoot.querySelector("#navigateNext"), + {} + ); + await waitForPageStatusUpdate( + helper.paginationSheetIndicator, + { sheetNum: 2, sheetCount: 3 }, + "Indicator updates on navigation next selection" + ); + + await helper.openMoreSettings(); + let printSelect = helper.get("source-version-selection-radio"); + await helper.waitForPreview(() => helper.click(printSelect)); + + // Wait for the first _updatePrintPreview before interacting with the preview + await waitForPageStatusUpdate( + helper.paginationSheetIndicator, + { sheetNum: 1, sheetCount: 2 }, + "Paginator indicates the correct number of sheets" + ); + + // click a navigation button + // and verify the indicator is updated correctly + EventUtils.synthesizeMouseAtCenter( + helper.paginationElem.shadowRoot.querySelector("#navigateNext"), + {} + ); + await waitForPageStatusUpdate( + helper.paginationSheetIndicator, + { sheetNum: 2, sheetCount: 2 }, + "Indicator updates on navigation next selection 2" + ); + + // move focus before closing the dialog + helper.get("cancel-button").focus(); + await helper.closeDialog(); + }, "longerArticle.html"); +}); + +add_task(async function testPaginatorAfterSettingsUpdate() { + const mockPrinterName = "Fake Printer"; + await PrintHelper.withTestPage(async helper => { + helper.addMockPrinter(mockPrinterName); + await helper.startPrint(); + + // Wait for the first _updatePrintPreview before interacting with the preview + await waitForPageStatusUpdate( + helper.paginationSheetIndicator, + { sheetNum: 1, sheetCount: 3 }, + "Paginator indicates the correct number of sheets" + ); + + // click the navigation buttons + // and verify the indicator is updated correctly + EventUtils.synthesizeMouseAtCenter( + helper.paginationElem.shadowRoot.querySelector("#navigateNext"), + {} + ); + await waitForPageStatusUpdate( + helper.paginationSheetIndicator, + { sheetNum: 2, sheetCount: 3 }, + "Indicator updates on navigation next after update" + ); + + // Select a new printer + await helper.dispatchSettingsChange({ printerName: mockPrinterName }); + await waitForPageStatusUpdate( + helper.paginationSheetIndicator, + { sheetNum: 1, sheetCount: 3 }, + "Indicator updates on navigation next after printer change" + ); + ok( + compare10nArgs(helper.paginationSheetIndicator, { + sheetNum: 1, + sheetCount: 3, + }), + "Sheet indicator has correct value" + ); + + // move focus before closing the dialog + helper.get("cancel-button").focus(); + await helper.closeDialog(); + }, "longerArticle.html"); +}); + +add_task(async function testTooltips() { + await SpecialPowers.pushPrefEnv({ set: [["ui.tooltipDelay", 0]] }); + const mockPrinterName = "Fake Printer"; + await PrintHelper.withTestPage(async helper => { + helper.addMockPrinter(mockPrinterName); + await helper.startPrint(); + + let paginationElem = document.querySelector(".printPreviewNavigation"); + let paginationSheetIndicator = + paginationElem.shadowRoot.querySelector("#sheetIndicator"); + + // Wait for the first _updatePrintPreview before interacting with the preview + await waitForPageStatusUpdate( + paginationSheetIndicator, + { sheetNum: 1, sheetCount: 3 }, + "Paginator indicates the correct number of sheets" + ); + + let awaitTooltipOpen = new Promise(resolve => { + window.addEventListener( + "popupshown", + function (event) { + resolve(event.originalTarget); + }, + { once: true } + ); + }); + + let navigateEnd = paginationElem.shadowRoot.querySelector("#navigateEnd"); + info("Initial mouse move to end navigation button"); + EventUtils.synthesizeMouseAtCenter(navigateEnd, { type: "mousemove" }); + let tooltip = await awaitTooltipOpen; + is(tooltip.label, navigateEnd.title, "Tooltip shows correct text"); + awaitTooltipOpen = new Promise(resolve => { + window.addEventListener( + "popupshown", + function (event) { + resolve(event.originalTarget); + }, + { once: true } + ); + }); + + let navigateNext = paginationElem.shadowRoot.querySelector("#navigateNext"); + let navigateNextRect = navigateNext.getBoundingClientRect(); + info("Initial mouse move to next navigation button"); + EventUtils.synthesizeMouseAtCenter(navigateNext, { type: "mousemove" }); + info("Waiting"); + EventUtils.synthesizeMouse( + navigateNext, + navigateNextRect.width / 2 + 5, + navigateNextRect.height / 2, + { type: "mousemove" }, + window + ); + tooltip = await awaitTooltipOpen; + is(tooltip.label, navigateNext.title, "Tooltip shows correct text"); + + // move focus before closing the dialog + helper.get("cancel-button").focus(); + await helper.awaitAnimationFrame(); + await helper.closeDialog(); + }, "longerArticle.html"); +}); diff --git a/toolkit/components/printing/tests/browser_preview_print_coop.js b/toolkit/components/printing/tests/browser_preview_print_coop.js new file mode 100644 index 0000000000..433c21e7bd --- /dev/null +++ b/toolkit/components/printing/tests/browser_preview_print_coop.js @@ -0,0 +1,20 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +/** + * Verify if the page with a COOP header can be used for printing preview. + */ +add_task(async function testTabModal() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + ok(true, "We did not crash."); + await helper.closeDialog(); + }, "file_coop_header.html"); +}); diff --git a/toolkit/components/printing/tests/browser_preview_print_link_modulepreload.js b/toolkit/components/printing/tests/browser_preview_print_link_modulepreload.js new file mode 100644 index 0000000000..84b6f2a0be --- /dev/null +++ b/toolkit/components/printing/tests/browser_preview_print_link_modulepreload.js @@ -0,0 +1,21 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +add_task(async function testLinkModulePreload() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.importMaps.enabled", true]], + }); + + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + ok(true, "We did not crash."); + await helper.closeDialog(); + }, "file_link_modulepreload.html"); +}); diff --git a/toolkit/components/printing/tests/browser_print_bcg_id_overflow.js b/toolkit/components/printing/tests/browser_print_bcg_id_overflow.js new file mode 100644 index 0000000000..3053afe236 --- /dev/null +++ b/toolkit/components/printing/tests/browser_print_bcg_id_overflow.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +// The actual uri we open doesn't really matter. +const OPENED_URI = PrintHelper.defaultTestPageUrlHTTPS; + +// Test for bug 1669554: +// +// This opens a rel=noopener window in a content process, which causes us to +// create a browsing context with an id that likely overflows an int32_t, which +// caused us to fail to parse the initialBrowsingContextGroupId attribute +// (causing us to potentially clone in the wrong process, etc). +const OPEN_NOOPENER_WINDOW = ` + <a rel="noopener" target="_blank" href="${OPENED_URI}">Open the window</a> +`; + +add_task(async function test_bc_id_overflow() { + is(document.querySelector(".printPreviewBrowser"), null); + + await BrowserTestUtils.withNewTab( + `data:text/html,` + encodeURIComponent(OPEN_NOOPENER_WINDOW), + async function (browser) { + let tabOpenedPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + OPENED_URI, + /* waitForLoad = */ true + ); + await BrowserTestUtils.synthesizeMouse("a", 0, 0, {}, browser); + let tab = await tabOpenedPromise; + let helper = new PrintHelper(tab.linkedBrowser); + await helper.startPrint(); + helper.assertDialogOpen(); + + let previewBrowser = document.querySelector(".printPreviewBrowser"); + is(typeof previewBrowser.browsingContext.group.id, "number", "Sanity"); + is( + previewBrowser.browsingContext.group.id, + tab.linkedBrowser.browsingContext.group.id, + "Group ids should match: " + tab.linkedBrowser.browsingContext.group.id + ); + is( + previewBrowser.browsingContext.group, + tab.linkedBrowser.browsingContext.group, + "Groups should match" + ); + await helper.closeDialog(); + await BrowserTestUtils.removeTab(tab); + } + ); +}); diff --git a/toolkit/components/printing/tests/browser_print_context_menu.js b/toolkit/components/printing/tests/browser_print_context_menu.js new file mode 100644 index 0000000000..39cb8d3623 --- /dev/null +++ b/toolkit/components/printing/tests/browser_print_context_menu.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const frameSource = `<a href="about:mozilla">Inner frame</a>`; +const source = `<html><h1>Top level text</h1><iframe srcdoc='${frameSource}' id="f"></iframe></html>`; + +add_task(async function testPrintFrame() { + let url = `data:text/html,${source}`; + await BrowserTestUtils.withNewTab( + { gBrowser, url }, + async function (browser) { + let contentAreaContextMenuPopup = document.getElementById( + "contentAreaContextMenu" + ); + let popupShownPromise = BrowserTestUtils.waitForEvent( + contentAreaContextMenuPopup, + "popupshown" + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#f", + { type: "contextmenu", button: 2 }, + gBrowser.selectedBrowser + ); + await popupShownPromise; + + let frameItem = document.getElementById("frame"); + let frameContextMenu = frameItem.menupopup; + popupShownPromise = BrowserTestUtils.waitForEvent( + frameContextMenu, + "popupshown" + ); + frameItem.openMenu(true); + await popupShownPromise; + + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + frameContextMenu, + "popuphidden" + ); + let item = document.getElementById("context-printframe"); + frameContextMenu.activateItem(item); + await popupHiddenPromise; + + let helper = new PrintHelper(browser); + + await helper.waitForDialog(); + + let previewBrowser = helper.currentPrintPreviewBrowser; + is( + previewBrowser.getAttribute("previewtype"), + "source", + "Source preview was rendered" + ); + + let textContent = await SpecialPowers.spawn( + previewBrowser, + [], + () => content.document.body.textContent + ); + + is(textContent, "Inner frame", "Correct content loaded"); + is( + helper.win.PrintEventHandler.printFrameOnly, + true, + "Print frame only is true" + ); + PrintHelper.resetPrintPrefs(); + } + ); +}); diff --git a/toolkit/components/printing/tests/browser_print_copies.js b/toolkit/components/printing/tests/browser_print_copies.js new file mode 100644 index 0000000000..9a97326f18 --- /dev/null +++ b/toolkit/components/printing/tests/browser_print_copies.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function testCopyError() { + await PrintHelper.withTestPage(async helper => { + helper.addMockPrinter("A printer"); + await SpecialPowers.pushPrefEnv({ + set: [["print_printer", "A printer"]], + }); + + await helper.startPrint(); + + let copyInput = helper.get("copies-count"); + let destinationPicker = helper.get("printer-picker"); + let copyError = helper.get("error-invalid-copies"); + + await helper.assertSettingsChanged( + { numCopies: 1 }, + { numCopies: 10000 }, + async () => { + await helper.waitForSettingsEvent(() => { + helper.text(copyInput, "10000"); + }); + + is(copyError.hidden, true, "Copy error is hidden"); + EventUtils.sendChar("0", helper.win); + + // Initially, the copies will be more than the max. + is(copyInput.checkValidity(), false, "Copy count is invalid"); + await BrowserTestUtils.waitForAttributeRemoval("hidden", copyError); + is(copyError.hidden, false, "Copy error is showing"); + is( + destinationPicker.disabled, + false, + "Destination picker is still enabled" + ); + + helper.text(copyInput, "10000"); + await helper.waitForSettingsEvent(); + is(copyInput.value, "10000", "Copies gets set to max value"); + is(copyInput.checkValidity(), true, "Copy count is valid again"); + await BrowserTestUtils.waitForCondition( + () => copyError.hidden, + "Wait for copy error to be hidden" + ); + } + ); + + await helper.closeDialog(); + }); +}); diff --git a/toolkit/components/printing/tests/browser_print_disabled.js b/toolkit/components/printing/tests/browser_print_disabled.js new file mode 100644 index 0000000000..87315d5952 --- /dev/null +++ b/toolkit/components/printing/tests/browser_print_disabled.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["print.enabled", false]], + }); +}); + +add_task(async function test_print_commands_disabled() { + let printCommand = document.getElementById(`cmd_print`); + let printPreview = document.getElementById(`cmd_printPreviewToggle`); + + is( + printCommand.getAttribute("disabled"), + "true", + "print command is disabled when print.enabled=false" + ); + is( + printPreview.getAttribute("disabled"), + "true", + "print preview command is disabled when print.enabled=false" + ); +}); + +add_task(async function test_window_print_disabled() { + await BrowserTestUtils.withNewTab("https://example.org/", async browser => { + await SpecialPowers.spawn(browser, [], function () { + content.window.print(); + }); + + // When the print dialog is open, the main thread of our tab is blocked, so + // the failure mode of this test is a timeout caused by the print dialog never + // closing. + + Assert.ok( + true, + "window.print doesn't do anything when print.enabled=false" + ); + }); +}); diff --git a/toolkit/components/printing/tests/browser_print_duplex.js b/toolkit/components/printing/tests/browser_print_duplex.js new file mode 100644 index 0000000000..9db33ce80e --- /dev/null +++ b/toolkit/components/printing/tests/browser_print_duplex.js @@ -0,0 +1,212 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function changeToOption(helper, index) { + return helper.waitForSettingsEvent(async function () { + let select = helper.get("duplex-select"); + select.focus(); + select.scrollIntoView({ block: "center" }); + + let popupOpen = BrowserTestUtils.waitForSelectPopupShown(window); + EventUtils.sendKey("space", helper.win); + await popupOpen; + + let selectedIndex = select.selectedIndex; + info(`Looking for ${index} from ${selectedIndex}`); + while (selectedIndex != index) { + if (index > selectedIndex) { + EventUtils.sendKey("down", window); + selectedIndex++; + } else { + EventUtils.sendKey("up", window); + selectedIndex--; + } + } + EventUtils.sendKey("return", window); + }); +} + +add_task(async function testPDFPrinterIsNonDuplex() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + await helper.openMoreSettings(); + + is( + helper.settings.printerName, + "Mozilla Save to PDF", + "Mozilla Save to PDF is the current printer." + ); + + const duplexSection = helper.get("two-sided-printing"); + ok( + duplexSection.hidden, + "The two-sided printing section should be hidden when the printer does not support duplex." + ); + + helper.assertSettingsMatch({ duplex: Ci.nsIPrintSettings.kDuplexNone }); + + await helper.closeDialog(); + }); +}); + +add_task(async function testToggleDuplexWithPortraitOrientation() { + const mockPrinterName = "DuplexWithPortrait"; + await PrintHelper.withTestPage(async helper => { + const printer = helper.addMockPrinter(mockPrinterName); + printer.supportsDuplex = Promise.resolve(true); + + await helper.startPrint(); + await helper.dispatchSettingsChange({ printerName: mockPrinterName }); + await helper.awaitAnimationFrame(); + await helper.openMoreSettings(); + + is( + helper.settings.printerName, + mockPrinterName, + "The Fake Printer is current printer" + ); + + const duplexSection = helper.get("two-sided-printing"); + ok( + !duplexSection.hidden, + "The two-sided printing section should not be hidden when the printer supports duplex." + ); + + helper.assertSettingsMatch({ + orientation: Ci.nsIPrintSettings.kPortraitOrientation, + duplex: Ci.nsIPrintSettings.kDuplexNone, + }); + + await changeToOption(helper, 1); + helper.assertSettingsMatch({ + orientation: Ci.nsIPrintSettings.kPortraitOrientation, + duplex: Ci.nsIPrintSettings.kDuplexFlipOnLongEdge, + }); + + await changeToOption(helper, 2); + helper.assertSettingsMatch({ + orientation: Ci.nsIPrintSettings.kPortraitOrientation, + duplex: Ci.nsIPrintSettings.kDuplexFlipOnShortEdge, + }); + + await changeToOption(helper, 0); + helper.assertSettingsMatch({ + orientation: Ci.nsIPrintSettings.kPortraitOrientation, + duplex: Ci.nsIPrintSettings.kDuplexNone, + }); + + await helper.closeDialog(); + }); +}); + +add_task(async function testToggleDuplexWithLandscapeOrientation() { + const mockPrinterName = "DuplexWithLandscape"; + await PrintHelper.withTestPage(async helper => { + const printer = helper.addMockPrinter(mockPrinterName); + printer.supportsDuplex = Promise.resolve(true); + + await helper.startPrint(); + await helper.dispatchSettingsChange({ printerName: mockPrinterName }); + await helper.awaitAnimationFrame(); + await helper.openMoreSettings(); + + is( + helper.settings.printerName, + mockPrinterName, + "The Fake Printer is current printer" + ); + + const duplexSection = helper.get("two-sided-printing"); + ok( + !duplexSection.hidden, + "The two-sided printing section should not be hidden when the printer supports duplex." + ); + + await helper.assertSettingsMatch({ + orientation: Ci.nsIPrintSettings.kPortraitOrientation, + duplex: Ci.nsIPrintSettings.kDuplexNone, + }); + + await helper.dispatchSettingsChange({ orientation: 1 }); + await helper.awaitAnimationFrame(); + await helper.assertSettingsMatch({ + orientation: Ci.nsIPrintSettings.kLandscapeOrientation, + duplex: Ci.nsIPrintSettings.kDuplexNone, + }); + + await changeToOption(helper, 1); + helper.assertSettingsMatch({ + orientation: Ci.nsIPrintSettings.kLandscapeOrientation, + duplex: Ci.nsIPrintSettings.kDuplexFlipOnLongEdge, + }); + + await changeToOption(helper, 2); + helper.assertSettingsMatch({ + orientation: Ci.nsIPrintSettings.kLandscapeOrientation, + duplex: Ci.nsIPrintSettings.kDuplexFlipOnShortEdge, + }); + + await changeToOption(helper, 0); + helper.assertSettingsMatch({ + orientation: Ci.nsIPrintSettings.kLandscapeOrientation, + duplex: Ci.nsIPrintSettings.kDuplexNone, + }); + + await helper.closeDialog(); + }); +}); + +add_task(async function testSwitchOrientationWithDuplexEnabled() { + const mockPrinterName = "ToggleOrientationPrinter"; + await PrintHelper.withTestPage(async helper => { + const printer = helper.addMockPrinter(mockPrinterName); + printer.supportsDuplex = Promise.resolve(true); + + await helper.startPrint(); + await helper.dispatchSettingsChange({ printerName: mockPrinterName }); + await helper.awaitAnimationFrame(); + await helper.openMoreSettings(); + + is( + helper.settings.printerName, + mockPrinterName, + "The Fake Printer is current printer" + ); + + const duplexSection = helper.get("two-sided-printing"); + ok( + !duplexSection.hidden, + "The two-sided printing section should not be hidden when the printer supports duplex." + ); + + await helper.assertSettingsMatch({ + orientation: Ci.nsIPrintSettings.kPortraitOrientation, + duplex: Ci.nsIPrintSettings.kDuplexNone, + }); + + await changeToOption(helper, 1); + + await helper.assertSettingsMatch({ + orientation: Ci.nsIPrintSettings.kPortraitOrientation, + duplex: Ci.nsIPrintSettings.kDuplexFlipOnLongEdge, + }); + + await helper.dispatchSettingsChange({ orientation: 1 }); + await helper.awaitAnimationFrame(); + await helper.assertSettingsMatch({ + orientation: Ci.nsIPrintSettings.kLandscapeOrientation, + duplex: Ci.nsIPrintSettings.kDuplexFlipOnLongEdge, + }); + + await helper.dispatchSettingsChange({ orientation: 0 }); + await helper.awaitAnimationFrame(); + await helper.assertSettingsMatch({ + orientation: Ci.nsIPrintSettings.kPortraitOrientation, + duplex: Ci.nsIPrintSettings.kDuplexFlipOnLongEdge, + }); + + await helper.closeDialog(); + }); +}); diff --git a/toolkit/components/printing/tests/browser_print_frame.js b/toolkit/components/printing/tests/browser_print_frame.js new file mode 100644 index 0000000000..b39925dfcd --- /dev/null +++ b/toolkit/components/printing/tests/browser_print_frame.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const frameSource = "<a href='about:mozilla'>some text</a>"; +const SOURCES = [ + `Something else <iframe id="f" srcdoc="${frameSource}"></iframe>`, + `Something else <iframe id="f" src="https://example.com/document-builder.sjs?html=${frameSource}"></iframe>`, +]; + +async function getPreviewText(previewBrowser) { + return SpecialPowers.spawn(previewBrowser, [], function () { + return content.document.body.textContent; + }); +} + +add_task(async function print_frame() { + let i = 0; + for (const source of SOURCES) { + is( + document.querySelector(".printPreviewBrowser"), + null, + "There shouldn't be any print preview browser" + ); + + await BrowserTestUtils.withNewTab( + "data:text/html," + source, + async function (browser) { + let frameBC = browser.browsingContext.children[0]; + let helper = new PrintHelper(browser); + + // If you change this, change nsContextMenu.printFrame() too. + PrintUtils.startPrintWindow(frameBC, { + printFrameOnly: true, + }); + + // Wait for the dialog to be fully ready. The initial preview will be + // done at this point. + await helper.waitForDialog(); + + let textContent = await getPreviewText( + helper.currentPrintPreviewBrowser + ); + is(textContent, "some text", "Correct content loaded"); + + let file = helper.mockFilePicker(`browser_print_frame-${i++}.pdf`); + await helper.assertPrintToFile(file, () => { + helper.click(helper.get("print-button")); + }); + PrintHelper.resetPrintPrefs(); + } + ); + } +}); diff --git a/toolkit/components/printing/tests/browser_print_in_container.js b/toolkit/components/printing/tests/browser_print_in_container.js new file mode 100644 index 0000000000..e8cec976fb --- /dev/null +++ b/toolkit/components/printing/tests/browser_print_in_container.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +add_task(async function test() { + let tab = await BrowserTestUtils.switchTab(gBrowser, function () { + gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + `${TEST_PATH}simplifyArticleSample.html`, + { userContextId: 1 } + ); + }); + + const helper = new PrintHelper(tab.linkedBrowser); + + helper.assertDialogClosed(); + await helper.startPrint(); + helper.assertDialogOpen(); + + let file = helper.mockFilePicker("browser_print_in_container.pdf"); + await helper.assertPrintToFile(file, () => { + helper.click(helper.get("print-button")); + }); + + ok(true, "We did not crash."); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/components/printing/tests/browser_print_margins.js b/toolkit/components/printing/tests/browser_print_margins.js new file mode 100644 index 0000000000..2e1d13d0e4 --- /dev/null +++ b/toolkit/components/printing/tests/browser_print_margins.js @@ -0,0 +1,1162 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Chaos mode slowdown causes intermittent failures - See bug 1698240. +requestLongerTimeout(2); + +async function changeMargin(helper, scroll, value) { + let marginSelect = helper.get("margins-picker"); + + info(" current value is " + marginSelect.value); + + marginSelect.focus(); + + if (scroll) { + marginSelect.scrollIntoView({ block: "center" }); + } + + marginSelect.value = value; + marginSelect.dispatchEvent( + new marginSelect.ownerGlobal.Event("input", { + bubbles: true, + composed: true, + }) + ); + marginSelect.dispatchEvent( + new marginSelect.ownerGlobal.Event("change", { + bubbles: true, + }) + ); +} + +function changeDefaultToCustom(helper) { + info("Trying to change margin from default -> custom"); + return changeMargin(helper, true, "custom"); +} + +function changeCustomToDefault(helper) { + info("Trying to change margin from custom -> default"); + return changeMargin(helper, false, "default"); +} + +function changeCustomToNone(helper) { + info("Trying to change margin from custom -> none"); + return changeMargin(helper, false, "none"); +} + +function assertPendingMarginsUpdate(helper) { + ok( + Object.keys(helper.win.PrintEventHandler._delayedChanges).length, + "At least one delayed task is added" + ); + ok( + helper.win.PrintEventHandler._delayedSettingsChangeTask.isArmed, + "The update task is armed" + ); +} + +function assertNoPendingMarginsUpdate(helper) { + ok( + !helper.win.PrintEventHandler._delayedSettingsChangeTask.isArmed, + "The update task isn't armed" + ); +} + +async function setupLetterPaper() { + const INCHES_PER_POINT = 1 / 72; + const printerList = Cc["@mozilla.org/gfx/printerlist;1"].createInstance( + Ci.nsIPrinterList + ); + let fallbackPaperList = await printerList.fallbackPaperList; + let paper = fallbackPaperList.find( + paper => + paper.width * INCHES_PER_POINT == 8.5 && + paper.height * INCHES_PER_POINT == 11 + ); + ok(paper, "Found a paper"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["print.printer_Mozilla_Save_to_PDF.print_paper_id", paper.id.toString()], + ["print.printer_Mozilla_Save_to_PDF.print_paper_size_unit", 0], + [ + "print.printer_Mozilla_Save_to_PDF.print_paper_width", + (paper.width * INCHES_PER_POINT).toString(), + ], + [ + "print.printer_Mozilla_Save_to_PDF.print_paper_height", + (paper.height * INCHES_PER_POINT).toString(), + ], + ], + }); +} + +add_task(async function testCustomMarginMaxAttrsSet() { + await PrintHelper.withTestPage(async helper => { + let paperList = [ + PrintHelper.createMockPaper({ + id: "unwriteableMargins", + name: "Unwriteable Margins", + // Numbers here demonstrate our truncating logic doesn't round up + unwriteableMargin: { + top: 18, + bottom: 19, + left: 18, + right: 19, + QueryInterface: ChromeUtils.generateQI([Ci.nsIPaperMargin]), + }, + }), + ]; + + let mockPrinterName = "Mock printer"; + helper.addMockPrinter({ name: mockPrinterName, paperList }); + Services.prefs.setStringPref("print_printer", mockPrinterName); + + await helper.startPrint(); + await helper.openMoreSettings(); + await changeDefaultToCustom(helper); + + let marginsSelect = helper.get("margins-select"); + is( + marginsSelect._maxHeight.toFixed(2), + "10.49", + "Max height would round up" + ); + is(marginsSelect._maxWidth.toFixed(2), "7.99", "Max width would round up"); + helper.assertSettingsMatch({ + marginTop: 0.5, + marginRight: 0.5, + marginBottom: 0.5, + marginLeft: 0.5, + }); + is( + helper.get("custom-margin-left").max, + "7.48", + "Left margin max attr is correct" + ); + is( + helper.get("custom-margin-right").max, + "7.48", + "Right margin max attr is correct" + ); + is( + helper.get("custom-margin-top").max, + "9.98", + "Top margin max attr is correct" + ); + is( + helper.get("custom-margin-bottom").max, + "9.98", + "Bottom margin max attr is correct" + ); + await helper.closeDialog(); + }); +}); + +add_task(async function testPresetMargins() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + await helper.openMoreSettings(); + + await helper.assertSettingsChanged( + { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 }, + { marginTop: 0.25, marginRight: 1, marginBottom: 2, marginLeft: 0.75 }, + async () => { + let marginSelect = helper.get("margins-picker"); + let customMargins = helper.get("custom-margins"); + + ok(customMargins.hidden, "Custom margins are hidden"); + is(marginSelect.value, "default", "Default margins set"); + helper.assertSettingsMatch({ honorPageRuleMargins: true }); + + await changeDefaultToCustom(helper); + + is(marginSelect.value, "custom", "Custom margins are now set"); + ok(!customMargins.hidden, "Custom margins are present"); + + // Check that values are initialized to correct values + is( + helper.get("custom-margin-top").value, + "0.50", + "Top margin placeholder is correct" + ); + is( + helper.get("custom-margin-right").value, + "0.50", + "Right margin placeholder is correct" + ); + is( + helper.get("custom-margin-bottom").value, + "0.50", + "Bottom margin placeholder is correct" + ); + is( + helper.get("custom-margin-left").value, + "0.50", + "Left margin placeholder is correct" + ); + + await helper.awaitAnimationFrame(); + + await helper.text(helper.get("custom-margin-top"), "0.25"); + await helper.text(helper.get("custom-margin-right"), "1"); + await helper.text(helper.get("custom-margin-bottom"), "2"); + await helper.text(helper.get("custom-margin-left"), "0.75"); + + assertPendingMarginsUpdate(helper); + + // Wait for the preview to update, the margin options delay updates by + // INPUT_DELAY_MS, which is 500ms. + await helper.waitForSettingsEvent(); + } + ); + await helper.closeDialog(); + }); +}); + +add_task(async function testHeightError() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + await helper.openMoreSettings(); + await changeDefaultToCustom(helper); + + await helper.assertSettingsNotChanged( + { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 }, + async () => { + let marginError = helper.get("error-invalid-margin"); + ok(marginError.hidden, "Margin error is hidden"); + + await helper.text(helper.get("custom-margin-top"), "20"); + await BrowserTestUtils.waitForAttributeRemoval("hidden", marginError); + + ok(!marginError.hidden, "Margin error is showing"); + assertNoPendingMarginsUpdate(helper); + } + ); + await helper.closeDialog(); + }); +}); + +add_task(async function testWidthError() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + await helper.openMoreSettings(); + await changeDefaultToCustom(helper); + + await helper.assertSettingsNotChanged( + { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 }, + async () => { + let marginError = helper.get("error-invalid-margin"); + ok(marginError.hidden, "Margin error is hidden"); + + await helper.text(helper.get("custom-margin-right"), "20"); + await BrowserTestUtils.waitForAttributeRemoval("hidden", marginError); + + ok(!marginError.hidden, "Margin error is showing"); + assertNoPendingMarginsUpdate(helper); + } + ); + await helper.closeDialog(); + }); +}); + +add_task(async function testInvalidMarginsReset() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + await helper.openMoreSettings(); + await changeDefaultToCustom(helper); + let marginError = helper.get("error-invalid-margin"); + + await helper.assertSettingsNotChanged( + { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 }, + async () => { + ok(marginError.hidden, "Margin error is hidden"); + + await helper.awaitAnimationFrame(); + await helper.text(helper.get("custom-margin-top"), "20"); + await helper.text(helper.get("custom-margin-right"), "20"); + assertNoPendingMarginsUpdate(helper); + await BrowserTestUtils.waitForAttributeRemoval("hidden", marginError); + } + ); + + await changeCustomToDefault(helper); + assertNoPendingMarginsUpdate(helper); + await BrowserTestUtils.waitForCondition( + () => marginError.hidden, + "Wait for margin error to be hidden" + ); + await changeDefaultToCustom(helper); + helper.assertSettingsMatch({ + marginTop: 0.5, + marginRight: 0.5, + marginBottom: 0.5, + marginLeft: 0.5, + }); + + is( + helper.get("margins-picker").value, + "custom", + "The custom option is selected" + ); + is( + helper.get("custom-margin-top").value, + "0.50", + "Top margin placeholder is correct" + ); + is( + helper.get("custom-margin-right").value, + "0.50", + "Right margin placeholder is correct" + ); + is( + helper.get("custom-margin-bottom").value, + "0.50", + "Bottom margin placeholder is correct" + ); + is( + helper.get("custom-margin-left").value, + "0.50", + "Left margin placeholder is correct" + ); + await BrowserTestUtils.waitForCondition( + () => marginError.hidden, + "Wait for margin error to be hidden" + ); + await helper.closeDialog(); + }); +}); + +add_task(async function testChangeInvalidToValidUpdate() { + await PrintHelper.withTestPage(async helper => { + await setupLetterPaper(); + await helper.startPrint(); + await helper.openMoreSettings(); + await changeDefaultToCustom(helper); + await helper.awaitAnimationFrame(); + let marginError = helper.get("error-invalid-margin"); + + await helper.text(helper.get("custom-margin-bottom"), "11"); + assertNoPendingMarginsUpdate(helper); + await BrowserTestUtils.waitForAttributeRemoval("hidden", marginError); + ok(!marginError.hidden, "Margin error is showing"); + helper.assertSettingsMatch({ + marginTop: 0.5, + marginRight: 0.5, + marginBottom: 0.5, + marginLeft: 0.5, + paperId: "na_letter", + }); + + await helper.text(helper.get("custom-margin-top"), "1"); + assertNoPendingMarginsUpdate(helper); + helper.assertSettingsMatch({ + marginTop: 0.5, + marginRight: 0.5, + marginBottom: 0.5, + marginLeft: 0.5, + }); + ok(!marginError.hidden, "Margin error is showing"); + + await helper.assertSettingsChanged( + { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 }, + { marginTop: 1, marginRight: 0.5, marginBottom: 1, marginLeft: 0.5 }, + async () => { + await helper.text(helper.get("custom-margin-bottom"), "1"); + + assertPendingMarginsUpdate(helper); + + // Wait for the preview to update, the margin options delay updates by + // INPUT_DELAY_MS, which is 500ms. + await helper.waitForSettingsEvent(); + ok(marginError.hidden, "Margin error is hidden"); + } + ); + }); +}); + +add_task(async function testChangeInvalidCanRevalidate() { + await PrintHelper.withTestPage(async helper => { + await setupLetterPaper(); + await helper.startPrint(); + await helper.openMoreSettings(); + await changeDefaultToCustom(helper); + await helper.awaitAnimationFrame(); + let marginError = helper.get("error-invalid-margin"); + + await helper.assertSettingsChanged( + { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 }, + { marginTop: 5, marginRight: 0.5, marginBottom: 3, marginLeft: 0.5 }, + async () => { + await helper.text(helper.get("custom-margin-top"), "5"); + await helper.text(helper.get("custom-margin-bottom"), "3"); + assertPendingMarginsUpdate(helper); + + // Wait for the preview to update, the margin options delay updates by + // INPUT_DELAY_MS, which is 500ms. + await helper.waitForSettingsEvent(); + ok(marginError.hidden, "Margin error is hidden"); + } + ); + + await helper.text(helper.get("custom-margin-top"), "9"); + assertNoPendingMarginsUpdate(helper); + await BrowserTestUtils.waitForAttributeRemoval("hidden", marginError); + ok(!marginError.hidden, "Margin error is showing"); + helper.assertSettingsMatch({ + marginTop: 5, + marginRight: 0.5, + marginBottom: 3, + marginLeft: 0.5, + paperId: "na_letter", + }); + + await helper.assertSettingsChanged( + { marginTop: 5, marginRight: 0.5, marginBottom: 3, marginLeft: 0.5 }, + { marginTop: 9, marginRight: 0.5, marginBottom: 2, marginLeft: 0.5 }, + async () => { + await helper.text(helper.get("custom-margin-bottom"), "2"); + assertPendingMarginsUpdate(helper); + + // Wait for the preview to update, the margin options delay updates by + // INPUT_DELAY_MS, which is 500ms. + await helper.waitForSettingsEvent(); + ok(marginError.hidden, "Margin error is hidden"); + } + ); + }); +}); + +add_task(async function testCustomMarginsPersist() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + await helper.openMoreSettings(); + + await helper.assertSettingsChanged( + { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 }, + { marginTop: 0.25, marginRight: 1, marginBottom: 2, marginLeft: 0 }, + async () => { + await changeDefaultToCustom(helper); + await helper.awaitAnimationFrame(); + + await helper.text(helper.get("custom-margin-top"), "0.25"); + await helper.text(helper.get("custom-margin-right"), "1"); + await helper.text(helper.get("custom-margin-bottom"), "2"); + await helper.text(helper.get("custom-margin-left"), "0"); + + assertPendingMarginsUpdate(helper); + + // Wait for the preview to update, the margin options delay updates by + // INPUT_DELAY_MS, which is 500ms. + await helper.waitForSettingsEvent(); + } + ); + + await helper.closeDialog(); + + await helper.startPrint(); + await helper.openMoreSettings(); + + helper.assertSettingsMatch({ + marginTop: 0.25, + marginRight: 1, + marginBottom: 2, + marginLeft: 0, + }); + + is( + helper.get("margins-picker").value, + "custom", + "The custom option is selected" + ); + is( + helper.get("custom-margin-top").value, + "0.25", + "Top margin placeholder is correct" + ); + is( + helper.get("custom-margin-right").value, + "1.00", + "Right margin placeholder is correct" + ); + is( + helper.get("custom-margin-bottom").value, + "2.00", + "Bottom margin placeholder is correct" + ); + is( + helper.get("custom-margin-left").value, + "0.00", + "Left margin placeholder is correct" + ); + await helper.assertSettingsChanged( + { marginTop: 0.25, marginRight: 1, marginBottom: 2, marginLeft: 0 }, + { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 }, + async () => { + await helper.awaitAnimationFrame(); + + await helper.text(helper.get("custom-margin-top"), "0.50"); + await helper.text(helper.get("custom-margin-right"), "0.50"); + await helper.text(helper.get("custom-margin-bottom"), "0.50"); + await helper.text(helper.get("custom-margin-left"), "0.50"); + + assertPendingMarginsUpdate(helper); + + // Wait for the preview to update, the margin options delay updates by + // INPUT_DELAY_MS, which is 500ms. + await helper.waitForSettingsEvent(); + } + ); + await helper.closeDialog(); + }); +}); + +add_task(async function testChangingBetweenMargins() { + await PrintHelper.withTestPage(async helper => { + await SpecialPowers.pushPrefEnv({ + set: [["print.printer_Mozilla_Save_to_PDF.print_margin_left", "1"]], + }); + + await helper.startPrint(); + await helper.openMoreSettings(); + + let marginsPicker = helper.get("margins-picker"); + is(marginsPicker.value, "custom", "First margin is custom"); + + helper.assertSettingsMatch({ + marginTop: 0.5, + marginBottom: 0.5, + marginLeft: 1, + marginRight: 0.5, + }); + + info("Switch to Default margins"); + await helper.assertSettingsChanged( + { marginLeft: 1 }, + { marginLeft: 0.5 }, + async () => { + let settingsChanged = helper.waitForSettingsEvent(); + await changeCustomToDefault(helper); + await settingsChanged; + } + ); + + is(marginsPicker.value, "default", "Default preset selected"); + + info("Switching back to Custom, should restore old margins"); + await helper.assertSettingsChanged( + { marginLeft: 0.5 }, + { marginLeft: 1 }, + async () => { + let settingsChanged = helper.waitForSettingsEvent(); + await changeDefaultToCustom(helper); + await settingsChanged; + } + ); + + is(marginsPicker.value, "custom", "Custom is now selected"); + + info("Switching back to Default, should restore 0.5"); + await helper.assertSettingsChanged( + { marginLeft: 1 }, + { marginLeft: 0.5 }, + async () => { + let settingsChanged = helper.waitForSettingsEvent(); + await changeCustomToDefault(helper); + await settingsChanged; + } + ); + + is(marginsPicker.value, "default", "Default preset is selected again"); + }); +}); + +add_task(async function testChangeHonoredInPrint() { + const mockPrinterName = "Fake Printer"; + await PrintHelper.withTestPage(async helper => { + helper.addMockPrinter(mockPrinterName); + await helper.startPrint(); + await helper.setupMockPrint(); + + helper.mockFilePicker("changedMargin.pdf"); + + await helper.openMoreSettings(); + helper.assertSettingsMatch({ marginRight: 0.5 }); + await changeDefaultToCustom(helper); + + await helper.withClosingFn(async () => { + await helper.text(helper.get("custom-margin-right"), "1"); + EventUtils.sendKey("return", helper.win); + helper.resolvePrint(); + }); + helper.assertPrintedWithSettings({ marginRight: 1 }); + }); +}); + +add_task(async function testInvalidPrefValueHeight() { + await PrintHelper.withTestPage(async helper => { + // Set some bad prefs + await SpecialPowers.pushPrefEnv({ + set: [["print.printer_Mozilla_Save_to_PDF.print_margin_top", "-1"]], + }); + await helper.startPrint(); + helper.assertSettingsMatch({ + marginTop: 0.5, + marginRight: 0.5, + marginBottom: 0.5, + marginLeft: 0.5, + }); + await helper.closeDialog(); + }); +}); + +add_task(async function testInvalidPrefValueWidth() { + await PrintHelper.withTestPage(async helper => { + // Set some bad prefs + await SpecialPowers.pushPrefEnv({ + set: [["print.printer_Mozilla_Save_to_PDF.print_margin_left", "-1"]], + }); + await helper.startPrint(); + helper.assertSettingsMatch({ + marginTop: 0.5, + marginRight: 0.5, + marginBottom: 0.5, + marginLeft: 0.5, + }); + await helper.closeDialog(); + }); +}); + +add_task(async function testInvalidMarginStartup() { + await PrintHelper.withTestPage(async helper => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["print.printer_Mozilla_Save_to_PDF.print_margin_right", "4"], + ["print.printer_Mozilla_Save_to_PDF.print_margin_left", "5"], + ], + }); + await setupLetterPaper(); + await helper.startPrint(); + helper.assertSettingsMatch({ + paperId: "na_letter", + marginLeft: 0.5, + marginRight: 0.5, + }); + helper.closeDialog(); + }); +}); + +add_task(async function testRevalidateSwitchToNone() { + await PrintHelper.withTestPage(async helper => { + await setupLetterPaper(); + await helper.startPrint(); + await helper.openMoreSettings(); + await changeDefaultToCustom(helper); + await helper.awaitAnimationFrame(); + + await helper.text(helper.get("custom-margin-bottom"), "6"); + await helper.text(helper.get("custom-margin-top"), "6"); + assertNoPendingMarginsUpdate(helper); + helper.assertSettingsMatch({ + marginTop: 0.5, + marginRight: 0.5, + marginBottom: 0.5, + marginLeft: 0.5, + paperId: "na_letter", + }); + + await helper.assertSettingsChanged( + { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 }, + { marginTop: 6, marginRight: 0.5, marginBottom: 3, marginLeft: 0.5 }, + async () => { + await helper.text(helper.get("custom-margin-bottom"), "3"); + assertPendingMarginsUpdate(helper); + + // Wait for the preview to update, the margin options delay updates by + // INPUT_DELAY_MS, which is 500ms. + await helper.waitForSettingsEvent(); + } + ); + + await helper.assertSettingsChanged( + { marginTop: 6, marginRight: 0.5, marginBottom: 3, marginLeft: 0.5 }, + { marginTop: 0, marginRight: 0, marginBottom: 0, marginLeft: 0 }, + async () => { + await changeCustomToNone(helper); + is( + helper.get("margins-picker").value, + "none", + "No margins are now set" + ); + + // Wait for the preview to update, the margin options delay updates by + // INPUT_DELAY_MS, which is 500ms. + await helper.waitForSettingsEvent(); + } + ); + }); +}); + +add_task(async function testInvalidMarginResetAfterDestinationChange() { + const mockPrinterName = "Fake Printer"; + await PrintHelper.withTestPage(async helper => { + helper.addMockPrinter(mockPrinterName); + await SpecialPowers.pushPrefEnv({ + set: [ + ["print.printer_Fake_Printer.print_paper_id", "na_letter"], + ["print.printer_Fake_Printer.print_paper_size_unit", 0], + ["print.printer_Fake_Printer.print_paper_width", "8.5"], + ["print.printer_Fake_Printer.print_paper_height", "11"], + ], + }); + await helper.startPrint(); + + let destinationPicker = helper.get("printer-picker"); + + await helper.openMoreSettings(); + await changeDefaultToCustom(helper); + await helper.awaitAnimationFrame(); + + let marginError = helper.get("error-invalid-margin"); + + await helper.assertSettingsNotChanged( + { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 }, + async () => { + ok(marginError.hidden, "Margin error is hidden"); + + await helper.text(helper.get("custom-margin-top"), "20"); + await BrowserTestUtils.waitForAttributeRemoval("hidden", marginError); + + ok(!marginError.hidden, "Margin error is showing"); + assertNoPendingMarginsUpdate(helper); + } + ); + + is(destinationPicker.disabled, false, "Destination picker is enabled"); + + helper.dispatchSettingsChange({ printerName: mockPrinterName }); + await BrowserTestUtils.waitForCondition( + () => marginError.hidden, + "Wait for margin error to be hidden" + ); + + helper.assertSettingsMatch({ + marginTop: 0.5, + marginRight: 0.5, + marginBottom: 0.5, + marginLeft: 0.5, + }); + + await helper.closeDialog(); + }); +}); + +add_task(async function testRevalidateCustomMarginsAfterPaperChanges() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + helper.dispatchSettingsChange({ paperId: "iso_a3" }); + await helper.openMoreSettings(); + await changeDefaultToCustom(helper); + await helper.awaitAnimationFrame(); + let marginError = helper.get("error-invalid-margin"); + + await helper.assertSettingsChanged( + { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 }, + { marginTop: 5, marginRight: 5, marginBottom: 5, marginLeft: 5 }, + async () => { + await helper.text(helper.get("custom-margin-top"), "5"); + await helper.text(helper.get("custom-margin-bottom"), "5"); + await helper.text(helper.get("custom-margin-right"), "5"); + await helper.text(helper.get("custom-margin-left"), "5"); + assertPendingMarginsUpdate(helper); + + // Wait for the preview to update, the margin options delay updates by + // INPUT_DELAY_MS, which is 500ms. + await helper.waitForSettingsEvent(); + ok(marginError.hidden, "Margin error is hidden"); + } + ); + + await helper.assertSettingsChanged( + { marginTop: 5, marginRight: 5, marginBottom: 5, marginLeft: 5 }, + { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 }, + async () => { + helper.dispatchSettingsChange({ paperId: "iso_a5" }); + + // Wait for the preview to update, the margin options delay updates by + // INPUT_DELAY_MS, which is 500ms. + await helper.waitForSettingsEvent(); + ok(marginError.hidden, "Margin error is hidden"); + } + ); + }); +}); + +add_task(async function testRevalidateCustomMarginsAfterOrientationChanges() { + await PrintHelper.withTestPage(async helper => { + await setupLetterPaper(); + await helper.startPrint(); + await helper.openMoreSettings(); + await changeDefaultToCustom(helper); + await helper.awaitAnimationFrame(); + let marginError = helper.get("error-invalid-margin"); + + await helper.assertSettingsChanged( + { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 }, + { marginTop: 5, marginRight: 0.5, marginBottom: 5, marginLeft: 0.5 }, + async () => { + await helper.text(helper.get("custom-margin-top"), "5"); + await helper.text(helper.get("custom-margin-bottom"), "5"); + assertPendingMarginsUpdate(helper); + + // Wait for the preview to update, the margin options delay updates by + // INPUT_DELAY_MS, which is 500ms. + await helper.waitForSettingsEvent(); + ok(marginError.hidden, "Margin error is hidden"); + } + ); + + await helper.assertSettingsChanged( + { marginTop: 5, marginRight: 0.5, marginBottom: 5, marginLeft: 0.5 }, + { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 }, + async () => { + helper.dispatchSettingsChange({ orientation: 1 }); + await helper.waitForSettingsEvent(); + ok(marginError.hidden, "Margin error is hidden"); + } + ); + }); +}); + +add_task(async function testResetMarginPersists() { + await PrintHelper.withTestPage(async helper => { + await setupLetterPaper(); + await helper.startPrint(); + + await helper.openMoreSettings(); + await changeDefaultToCustom(helper); + await helper.awaitAnimationFrame(); + let marginError = helper.get("error-invalid-margin"); + + await helper.assertSettingsChanged( + { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 }, + { marginTop: 0.5, marginRight: 4, marginBottom: 0.5, marginLeft: 4.5 }, + async () => { + await helper.text(helper.get("custom-margin-right"), "4"); + await helper.text(helper.get("custom-margin-left"), "4.5"); + + // Wait for the preview to update, the margin options delay updates by + // INPUT_DELAY_MS, which is 500ms. + await helper.waitForSettingsEvent(); + ok(marginError.hidden, "Margin error is hidden"); + } + ); + + await helper.assertSettingsChanged( + { marginTop: 0.5, marginRight: 4, marginBottom: 0.5, marginLeft: 4.5 }, + { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 }, + async () => { + helper.dispatchSettingsChange({ paperId: "iso_a4" }); + + // Wait for the preview to update, the margin options delay updates by + // INPUT_DELAY_MS, which is 500ms. + await helper.waitForSettingsEvent(); + ok(marginError.hidden, "Margin error is hidden"); + } + ); + + await helper.assertSettingsNotChanged( + { marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 }, + async () => { + helper.dispatchSettingsChange({ paperId: "iso_a5" }); + await helper.waitForSettingsEvent(); + ok(marginError.hidden, "Margin error is hidden"); + } + ); + + await helper.closeDialog(); + }); +}); + +add_task(async function testCustomMarginUnits() { + const mockPrinterName = "MetricPrinter"; + await PrintHelper.withTestPage(async helper => { + // Add a metric-unit printer we can test with + helper.addMockPrinter({ + name: mockPrinterName, + paperSizeUnit: Ci.nsIPrintSettings.kPaperSizeMillimeters, + paperList: [], + }); + + // settings are saved in inches + const persistedMargins = { + top: 0.5, + right: 5, + bottom: 0.5, + left: 1, + }; + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "print.printer_Mozilla_Save_to_PDF.print_margin_right", + persistedMargins.right.toString(), + ], + [ + "print.printer_Mozilla_Save_to_PDF.print_margin_left", + persistedMargins.left.toString(), + ], + [ + "print.printer_Mozilla_Save_to_PDF.print_margin_top", + persistedMargins.top.toString(), + ], + [ + "print.printer_Mozilla_Save_to_PDF.print_margin_bottom", + persistedMargins.bottom.toString(), + ], + ], + }); + await helper.startPrint(); + await helper.openMoreSettings(); + + helper.assertSettingsMatch({ + paperId: "na_letter", + marginTop: persistedMargins.top, + marginRight: persistedMargins.right, + marginBottom: persistedMargins.bottom, + marginLeft: persistedMargins.left, + }); + + is( + helper.settings.printerName, + DEFAULT_PRINTER_NAME, + "The PDF (inch-unit) printer is current" + ); + + is( + helper.get("margins-picker").value, + "custom", + "The margins picker has the expected value" + ); + is( + helper.get("margins-picker").selectedOptions[0].dataset.l10nId, + "printui-margins-custom-inches", + "The custom margins option has correct unit string id" + ); + // the unit value should be correct for inches + for (let edgeName of Object.keys(persistedMargins)) { + is( + helper.get(`custom-margin-${edgeName}`).value, + persistedMargins[edgeName].toFixed(2), + `Has the expected unit-converted ${edgeName}-margin value` + ); + } + + await helper.assertSettingsChanged( + { marginTop: persistedMargins.top }, + { marginTop: 1 }, + async () => { + // update the top margin to 1" + await helper.text(helper.get("custom-margin-top"), "1"); + assertPendingMarginsUpdate(helper); + + // Wait for the preview to update, the margin options delay updates by + // INPUT_DELAY_MS, which is 500ms. + await helper.waitForSettingsEvent(); + // ensure any round-trip correctly re-converts the setting value back to the displayed mm value + is( + helper.get("custom-margin-top").value, + "1", + "Converted custom margin value is expected value" + ); + } + ); + // put it back to how it was + await helper.text( + helper.get("custom-margin-top"), + persistedMargins.top.toString() + ); + await helper.waitForSettingsEvent(); + + // Now switch to the metric printer + await helper.dispatchSettingsChange({ printerName: mockPrinterName }); + await helper.waitForSettingsEvent(); + + is( + helper.settings.printerName, + mockPrinterName, + "The metric printer is current" + ); + is( + helper.get("margins-picker").value, + "custom", + "The margins picker has the expected value" + ); + is( + helper.get("margins-picker").selectedOptions[0].dataset.l10nId, + "printui-margins-custom-mm", + "The custom margins option has correct unit string id" + ); + // the unit value should be correct for mm + for (let edgeName of Object.keys(persistedMargins)) { + is( + helper.get(`custom-margin-${edgeName}`).value, + (persistedMargins[edgeName] * 25.4).toFixed(2), + `Has the expected unit-converted ${edgeName}-margin value` + ); + } + + await helper.assertSettingsChanged( + { marginTop: persistedMargins.top }, + { marginTop: 1 }, + async () => { + let marginError = helper.get("error-invalid-margin"); + ok(marginError.hidden, "Margin error is hidden"); + + // update the top margin to 1" in mm + await helper.text(helper.get("custom-margin-top"), "25.4"); + // Check the constraints validation is using the right max + // as 25" top margin would be an error, but 25mm is ok + ok(marginError.hidden, "Margin error is hidden"); + + assertPendingMarginsUpdate(helper); + // Wait for the preview to update, the margin options delay updates by INPUT_DELAY_MS + await helper.waitForSettingsEvent(); + // ensure any round-trip correctly re-converts the setting value back to the displayed mm value + is( + helper.get("custom-margin-top").value, + "25.4", + "Converted custom margin value is expected value" + ); + } + ); + + // check margin validation is actually working with unit-appropriate max + await helper.assertSettingsNotChanged({ marginTop: 1 }, async () => { + let marginError = helper.get("error-invalid-margin"); + ok(marginError.hidden, "Margin error is hidden"); + + await helper.text(helper.get("custom-margin-top"), "300"); + await BrowserTestUtils.waitForAttributeRemoval("hidden", marginError); + + ok(!marginError.hidden, "Margin error is showing"); + assertNoPendingMarginsUpdate(helper); + }); + + await SpecialPowers.popPrefEnv(); + await helper.closeDialog(); + }); +}); + +add_task(async function testHonorPageRuleMargins() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + await helper.openMoreSettings(); + let marginsPicker = helper.get("margins-picker"); + + is(marginsPicker.value, "default", "Started with default margins"); + helper.assertSettingsMatch({ honorPageRuleMargins: true }); + + await helper.waitForSettingsEvent(() => changeDefaultToCustom(helper)); + + is(marginsPicker.value, "custom", "Changed to custom margins"); + helper.assertSettingsMatch({ honorPageRuleMargins: false }); + + await helper.waitForSettingsEvent(() => changeCustomToNone(helper)); + + is(marginsPicker.value, "none", "Changed to no margins"); + helper.assertSettingsMatch({ honorPageRuleMargins: false }); + + await helper.waitForSettingsEvent(() => changeCustomToDefault(helper)); + + is(marginsPicker.value, "default", "Back to default margins"); + helper.assertSettingsMatch({ honorPageRuleMargins: true }); + }); +}); + +add_task(async function testIgnoreUnwriteableMargins() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + await helper.openMoreSettings(); + let marginsPicker = helper.get("margins-picker"); + + is(marginsPicker.value, "default", "Started with default margins"); + helper.assertSettingsMatch({ ignoreUnwriteableMargins: false }); + + await helper.waitForSettingsEvent(() => changeDefaultToCustom(helper)); + + is(marginsPicker.value, "custom", "Changed to custom margins"); + helper.assertSettingsMatch({ ignoreUnwriteableMargins: false }); + + await helper.waitForSettingsEvent(() => changeCustomToNone(helper)); + + is(marginsPicker.value, "none", "Changed to no margins"); + helper.assertSettingsMatch({ ignoreUnwriteableMargins: true }); + + await helper.waitForSettingsEvent(() => changeCustomToDefault(helper)); + + is(marginsPicker.value, "default", "Back to default margins"); + helper.assertSettingsMatch({ ignoreUnwriteableMargins: false }); + }); +}); + +add_task(async function testCustomMarginZeroRespectsUnwriteableMargins() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + await helper.openMoreSettings(); + let marginsPicker = helper.get("margins-picker"); + + await helper.waitForSettingsEvent(() => changeDefaultToCustom(helper)); + is(marginsPicker.value, "custom", "Changed to custom margins"); + + await helper.text(helper.get("custom-margin-top"), "0"); + await helper.text(helper.get("custom-margin-right"), "0"); + await helper.text(helper.get("custom-margin-bottom"), "0"); + await helper.text(helper.get("custom-margin-left"), "0"); + + assertPendingMarginsUpdate(helper); + await helper.waitForSettingsEvent(); + helper.assertSettingsMatch({ ignoreUnwriteableMargins: false }); + }); +}); + +add_task(async function testDefaultMarginsInvalidStartup() { + await PrintHelper.withTestPage(async helper => { + let paperList = [ + PrintHelper.createMockPaper({ + id: "smallestPaper", + name: "Default Margins Invalid", + width: 50, + height: 50, + unwriteableMargin: { + top: 10, + bottom: 10, + left: 10, + right: 10, + QueryInterface: ChromeUtils.generateQI([Ci.nsIPaperMargin]), + }, + }), + ]; + + let mockPrinterName = "Mock printer"; + helper.addMockPrinter({ name: mockPrinterName, paperList }); + Services.prefs.setStringPref("print_printer", mockPrinterName); + + await helper.startPrint(); + + helper.assertSettingsMatch({ + marginTop: 0, + marginRight: 0, + marginBottom: 0, + marginLeft: 0, + }); + + let marginSelect = helper.get("margins-picker"); + is(marginSelect.value, "none", "Margins picker set to 'None'"); + + let printForm = helper.get("print"); + ok(printForm.checkValidity(), "The print form is valid"); + + await helper.closeDialog(); + }); +}); diff --git a/toolkit/components/printing/tests/browser_print_page_range.js b/toolkit/components/printing/tests/browser_print_page_range.js new file mode 100644 index 0000000000..89ef80f3a5 --- /dev/null +++ b/toolkit/components/printing/tests/browser_print_page_range.js @@ -0,0 +1,540 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +async function changeRangeTo(helper, destination) { + info(`changeRangeTo(${destination})`); + let rangeSelect = helper.get("range-picker"); + let options = getRangeOptions(helper); + let numberMove = + options.indexOf(destination) - options.indexOf(rangeSelect.value); + let direction = numberMove > 0 ? "down" : "up"; + if (!numberMove) { + return; + } + + let input = BrowserTestUtils.waitForEvent(rangeSelect, "input"); + + let popupOpen = BrowserTestUtils.waitForSelectPopupShown(window); + + rangeSelect.focus(); + rangeSelect.scrollIntoView({ block: "center" }); + EventUtils.sendKey("space", helper.win); + + await popupOpen; + for (let i = Math.abs(numberMove); i > 0; i--) { + EventUtils.sendKey(direction, window); + } + EventUtils.sendKey("return", window); + + await input; +} + +function getRangeOptions(helper) { + let rangeSelect = helper.get("range-picker"); + let options = []; + for (let el of rangeSelect.options) { + if (!el.disabled) { + options.push(el.value); + } + } + return options; +} + +function getSheetCount(helper) { + return helper.doc.l10n.getAttributes(helper.get("sheet-count")).args + .sheetCount; +} + +add_task(async function testRangeResetAfterScale() { + const mockPrinterName = "Fake Printer"; + await PrintHelper.withTestPage(async helper => { + helper.addMockPrinter(mockPrinterName); + await helper.startPrint(); + await helper.setupMockPrint(); + + helper.mockFilePicker("changeRangeFromScale.pdf"); + await changeRangeTo(helper, "custom"); + + await helper.openMoreSettings(); + let scaleRadio = helper.get("percent-scale-choice"); + await helper.waitForPreview(() => helper.click(scaleRadio)); + let percentScale = helper.get("percent-scale"); + await helper.waitForPreview(() => helper.text(percentScale, "200")); + + let customRange = helper.get("custom-range"); + let rangeError = helper.get("error-invalid-range"); + await helper.waitForPreview(() => { + helper.text(customRange, "3"); + }); + + ok(rangeError.hidden, "Range error is hidden"); + + await helper.text(percentScale, "10"); + EventUtils.sendKey("return", helper.win); + + await BrowserTestUtils.waitForAttributeRemoval("hidden", rangeError); + ok(!rangeError.hidden, "Range error is showing"); + await helper.closeDialog(); + }); +}); + +add_task(async function testRangeResetAfterPaperSize() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + await helper.waitForPreview(() => + helper.dispatchSettingsChange({ paperId: "iso_a5" }) + ); + await helper.setupMockPrint(); + + await helper.openMoreSettings(); + let scaleRadio = helper.get("percent-scale-choice"); + await helper.waitForPreview(() => helper.click(scaleRadio)); + let percentScale = helper.get("percent-scale"); + await helper.waitForPreview(() => helper.text(percentScale, "200")); + + let customRange = helper.get("custom-range"); + await changeRangeTo(helper, "custom"); + await BrowserTestUtils.waitForAttributeRemoval("hidden", customRange); + + let rangeError = helper.get("error-invalid-range"); + await helper.waitForPreview(() => { + helper.text(customRange, "6"); + }); + + ok(rangeError.hidden, "Range error is hidden"); + + helper.dispatchSettingsChange({ paperId: "iso_a3" }); + await BrowserTestUtils.waitForCondition( + () => helper.get("paper-size-picker").value == "iso_a3", + "Wait for paper size select to update" + ); + EventUtils.sendKey("return", helper.win); + + await BrowserTestUtils.waitForAttributeRemoval("hidden", rangeError); + ok(!rangeError.hidden, "Range error is showing"); + await helper.closeDialog(); + }); +}); + +add_task(async function testInvalidRangeResetAfterDestinationChange() { + const mockPrinterName = "Fake Printer"; + await PrintHelper.withTestPage(async helper => { + helper.addMockPrinter(mockPrinterName); + await helper.startPrint(); + + let destinationPicker = helper.get("printer-picker"); + let customPageRange = helper.get("custom-range"); + + await helper.assertSettingsNotChanged({ pageRanges: [] }, async () => { + await changeRangeTo(helper, "custom"); + }); + let rangeError = helper.get("error-invalid-range"); + + await helper.assertSettingsNotChanged({ pageRanges: [] }, async () => { + ok(rangeError.hidden, "Range error is hidden"); + await helper.text(customPageRange, "9"); + await BrowserTestUtils.waitForAttributeRemoval("hidden", rangeError); + ok(!rangeError.hidden, "Range error is showing"); + }); + + is(destinationPicker.disabled, false, "Destination picker is enabled"); + + // Select a new printer + helper.dispatchSettingsChange({ printerName: mockPrinterName }); + await BrowserTestUtils.waitForCondition( + () => rangeError.hidden, + "Wait for range error to be hidden" + ); + is(customPageRange.value, "", "Page range has reset"); + await helper.closeDialog(); + }); +}); + +add_task(async function testPageRangeSets() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + + let customRange = helper.get("custom-range"); + let pageRangeInput = helper.get("page-range-input"); + let invalidError = helper.get("error-invalid-range"); + let invalidOverflowError = helper.get("error-invalid-start-range-overflow"); + + ok(customRange.hidden, "Custom range input is hidden"); + + await changeRangeTo(helper, "custom"); + await BrowserTestUtils.waitForAttributeRemoval("hidden", customRange); + + ok(!customRange.hidden, "Custom range is showing"); + is(helper.doc.activeElement, customRange, "Custom range field is focused"); + + // We need to set the input to something to ensure we do not return early + // out of our validation function + helper.text(helper.get("custom-range"), ","); + + let validStrings = { + 1: [1, 1], + "1,": [1, 1], + 2: [2, 2], + "1-2": [1, 2], + "1,2": [1, 2], + "1,2,": [1, 2], + "2,1": [1, 2], + "1,3": [1, 1, 3, 3], + "1-1,3": [1, 1, 3, 3], + "1,3-3": [1, 1, 3, 3], + "10-33": [10, 33], + "1-": [1, 50], + "-": [], + "-20": [1, 20], + "-1,1-": [1, 50], + "-1,1-2": [1, 2], + ",9": [9, 9], + ",": [], + "1,2,1,20,5": [1, 2, 5, 5, 20, 20], + "1-17,4,12-19": [1, 19], + "43-46,42,47-": [42, 50], + }; + + for (let [str, expected] of Object.entries(validStrings)) { + pageRangeInput._validateRangeInput(str, 50); + let pageRanges = pageRangeInput.formatPageRange(); + ok( + expected.length == pageRanges.length && + expected.every((page, index) => page === pageRanges[index]), + `Expected page range for "${str}" matches "${expected}"` + ); + + ok(invalidError.hidden, "Generic error message is hidden"); + ok(invalidOverflowError.hidden, "Start overflow error message is hidden"); + } + + let invalidStrings = ["51", "1,51", "1-51", "4-1", "--", "0", "-90"]; + + for (let str of invalidStrings) { + pageRangeInput._validateRangeInput(str, 50); + is(pageRangeInput._pagesSet.size, 0, `There are no pages in the set`); + ok(!pageRangeInput.validity, "Input is invalid"); + } + + await helper.closeDialog(); + }); +}); + +add_task(async function testPageRangeSelect() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + + let pageRangeInput = helper.get("page-range-input"); + + await changeRangeTo(helper, "all"); + let pageRanges = pageRangeInput.formatPageRange(); + ok(!pageRanges.length, "Page range for all should be []"); + + await changeRangeTo(helper, "current"); + pageRanges = pageRangeInput.formatPageRange(); + ok( + pageRanges.length == 2 && + [1, 1].every((page, index) => page === pageRanges[index]), + "The first page should be the current page" + ); + + pageRangeInput._validateRangeInput("9", 50); + pageRanges = pageRangeInput.formatPageRange(); + ok( + pageRanges.length == 2 && + [9, 9].every((page, index) => page === pageRanges[index]), + `Expected page range for "${pageRanges}" matches [9, 9]"` + ); + + await changeRangeTo(helper, "odd"); + pageRanges = pageRangeInput.formatPageRange(); + ok( + pageRanges.length == 4 && + [1, 1, 3, 3].every((page, index) => page === pageRanges[index]), + "Page range for odd should be [1, 1, 3, 3]" + ); + + await changeRangeTo(helper, "even"); + pageRanges = pageRangeInput.formatPageRange(); + ok( + pageRanges.length == 2 && + [2, 2].every((page, index) => page === pageRanges[index]), + "Page range for even should be [2, 2]" + ); + }, "longerArticle.html"); +}); + +add_task(async function testRangeError() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + + await changeRangeTo(helper, "custom"); + + let invalidError = helper.get("error-invalid-range"); + let invalidOverflowError = helper.get("error-invalid-start-range-overflow"); + + ok(invalidError.hidden, "Generic error message is hidden"); + ok(invalidOverflowError.hidden, "Start overflow error message is hidden"); + + helper.text(helper.get("custom-range"), "4"); + + await BrowserTestUtils.waitForAttributeRemoval("hidden", invalidError); + + ok(!invalidError.hidden, "Generic error message is showing"); + ok(invalidOverflowError.hidden, "Start overflow error message is hidden"); + + await helper.closeDialog(); + }); +}); + +add_task(async function testStartOverflowRangeError() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + + await changeRangeTo(helper, "custom"); + + await helper.openMoreSettings(); + let scaleRadio = helper.get("percent-scale-choice"); + await helper.waitForPreview(() => helper.click(scaleRadio)); + let percentScale = helper.get("percent-scale"); + await helper.waitForPreview(() => helper.text(percentScale, "200")); + + let invalidError = helper.get("error-invalid-range"); + let invalidOverflowError = helper.get("error-invalid-start-range-overflow"); + + ok(invalidError.hidden, "Generic error message is hidden"); + ok(invalidOverflowError.hidden, "Start overflow error message is hidden"); + + helper.text(helper.get("custom-range"), "2-1"); + + await BrowserTestUtils.waitForAttributeRemoval( + "hidden", + invalidOverflowError + ); + + ok(invalidError.hidden, "Generic error message is hidden"); + ok(!invalidOverflowError.hidden, "Start overflow error message is showing"); + + await helper.closeDialog(); + }); +}); + +add_task(async function testErrorClearedAfterSwitchingToAll() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + + await changeRangeTo(helper, "custom"); + + let customRange = helper.get("custom-range"); + let rangeError = helper.get("error-invalid-range"); + ok(rangeError.hidden, "Generic error message is hidden"); + + helper.text(customRange, "3"); + + await BrowserTestUtils.waitForAttributeRemoval("hidden", rangeError); + ok(!rangeError.hidden, "Generic error message is showing"); + + await changeRangeTo(helper, "all"); + + await BrowserTestUtils.waitForCondition( + () => rangeError.hidden, + "Wait for range error to be hidden" + ); + ok(customRange.hidden, "Custom range is hidden"); + is( + helper.doc.activeElement, + helper.get("range-picker"), + "Range picker remains focused" + ); + await helper.closeDialog(); + }); +}); + +add_task(async function testPageCountChangeNoRangeNoRerender() { + await PrintHelper.withTestPage(async helper => { + let customPrinter = "A printer"; + helper.addMockPrinter(customPrinter); + + await helper.startPrint(); + + await helper.assertSettingsChanged( + { printerName: "Mozilla Save to PDF" }, + { printerName: customPrinter }, + async () => { + let destinationPicker = helper.get("printer-picker"); + destinationPicker.focus(); + await Promise.all([ + helper.waitForPreview(() => + helper.dispatchSettingsChange({ printerName: customPrinter }) + ), + helper.waitForSettingsEvent(), + ]); + } + ); + + // Change a setting that will change the number of pages. Since pageRanges + // is set to "all" then there shouldn't be a re-render because of it. + let previewUpdateCount = 0; + ok(!helper.hasPendingPreview, "No preview is pending"); + helper.doc.addEventListener("preview-updated", () => previewUpdateCount++); + + // Ensure the sheet count will change. + let initialSheetCount = getSheetCount(helper); + + await helper.assertSettingsChanged( + { marginLeft: 0.5, marginRight: 0.5 }, + { marginLeft: 3, marginRight: 3 }, + async () => { + await Promise.all([ + helper.waitForPreview(() => + helper.dispatchSettingsChange({ marginLeft: 3, marginRight: 3 }) + ), + BrowserTestUtils.waitForEvent(helper.doc, "page-count"), + helper.waitForSettingsEvent(), + ]); + } + ); + + let newSheetCount = getSheetCount(helper); + ok( + initialSheetCount < newSheetCount, + `There are more sheets now ${initialSheetCount} < ${newSheetCount}` + ); + + ok(!helper.hasPendingPreview, "No preview is pending"); + is(previewUpdateCount, 1, "Only one preview update fired"); + + await helper.closeDialog(); + }); +}); + +add_task(async function testPageCountChangeRangeNoRerender() { + await PrintHelper.withTestPage(async helper => { + let customPrinter = "A printer"; + helper.addMockPrinter(customPrinter); + + await helper.startPrint(); + + await helper.assertSettingsChanged( + { printerName: "Mozilla Save to PDF", pageRanges: [] }, + { printerName: customPrinter, pageRanges: [1, 1] }, + async () => { + let destinationPicker = helper.get("printer-picker"); + destinationPicker.focus(); + await Promise.all([ + helper.waitForPreview(() => + helper.dispatchSettingsChange({ printerName: customPrinter }) + ), + helper.waitForSettingsEvent(), + ]); + + await helper.waitForPreview(async () => { + await changeRangeTo(helper, "custom"); + helper.text(helper.get("custom-range"), "1"); + }); + } + ); + + // Change a setting that will change the number of pages. Since pageRanges + // is set to a page that is in the new range, there shouldn't be a re-render. + let previewUpdateCount = 0; + ok(!helper.hasPendingPreview, "No preview is pending"); + helper.doc.addEventListener("preview-updated", () => previewUpdateCount++); + + await helper.assertSettingsChanged( + { marginLeft: 0.5, marginRight: 0.5 }, + { marginLeft: 3, marginRight: 3 }, + async () => { + await Promise.all([ + helper.waitForPreview(() => + helper.dispatchSettingsChange({ marginLeft: 3, marginRight: 3 }) + ), + BrowserTestUtils.waitForEvent(helper.doc, "page-count"), + helper.waitForSettingsEvent(), + ]); + } + ); + + let newSheetCount = getSheetCount(helper); + is(newSheetCount, 1, "There's still only one sheet"); + + ok(!helper.hasPendingPreview, "No preview is pending"); + is(previewUpdateCount, 1, "Only one preview update fired"); + + await helper.closeDialog(); + }); +}); + +add_task(async function testPageCountChangeRangeRerender() { + await PrintHelper.withTestPage(async helper => { + let customPrinter = "A printer"; + helper.addMockPrinter(customPrinter); + + await helper.startPrint(); + + await helper.assertSettingsChanged( + { printerName: "Mozilla Save to PDF", pageRanges: [] }, + { printerName: customPrinter, pageRanges: [1, 1] }, + async () => { + let destinationPicker = helper.get("printer-picker"); + destinationPicker.focus(); + await Promise.all([ + helper.waitForPreview(() => + helper.dispatchSettingsChange({ printerName: customPrinter }) + ), + helper.waitForSettingsEvent(), + ]); + + await helper.waitForPreview(async () => { + await changeRangeTo(helper, "custom"); + helper.text(helper.get("custom-range"), "1-"); + }); + } + ); + + // Change a setting that will change the number of pages. Since pageRanges + // is from 1-N the calculated page range will need to be updated. + let previewUpdateCount = 0; + ok(!helper.hasPendingPreview, "No preview is pending"); + helper.doc.addEventListener("preview-updated", () => previewUpdateCount++); + let renderedTwice = BrowserTestUtils.waitForCondition( + () => previewUpdateCount == 2 + ); + + // Ensure the sheet count will change. + let initialSheetCount = getSheetCount(helper); + + await helper.assertSettingsChanged( + { marginLeft: 0.5, marginRight: 0.5 }, + { marginLeft: 3, marginRight: 3 }, + async () => { + await Promise.all([ + helper.waitForPreview(() => + helper.dispatchSettingsChange({ marginLeft: 3, marginRight: 3 }) + ), + BrowserTestUtils.waitForEvent(helper.doc, "page-count"), + helper.waitForSettingsEvent(), + ]); + await renderedTwice; + } + ); + + let newSheetCount = getSheetCount(helper); + ok( + initialSheetCount < newSheetCount, + `There are more sheets now ${initialSheetCount} < ${newSheetCount}` + ); + Assert.deepEqual( + helper.viewSettings.pageRanges, + [1, newSheetCount], + "The new range is the updated full page range" + ); + + ok(!helper.hasPendingPreview, "No preview is pending"); + is(previewUpdateCount, 2, "Preview updated again to show new page range"); + + await helper.closeDialog(); + }); +}); diff --git a/toolkit/components/printing/tests/browser_print_page_size.js b/toolkit/components/printing/tests/browser_print_page_size.js new file mode 100644 index 0000000000..b9936f2150 --- /dev/null +++ b/toolkit/components/printing/tests/browser_print_page_size.js @@ -0,0 +1,269 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const pdfPrinterName = "Mozilla Save to PDF"; +const someOtherPrinter = "Other Printer"; + +async function changeDestination(helper, dir) { + let picker = helper.get("printer-picker"); + let changed = BrowserTestUtils.waitForEvent(picker, "change"); + + let pickerOpened = BrowserTestUtils.waitForSelectPopupShown(window); + picker.focus(); + EventUtils.sendKey("space", helper.win); + await pickerOpened; + EventUtils.sendKey(dir, window); + EventUtils.sendKey("return", window); + await changed; +} + +add_task(async function testShowAndHidePaperSizeSectionWithPageSize() { + await PrintHelper.withTestPage(async helper => { + await helper.addMockPrinter(someOtherPrinter); + await SpecialPowers.pushPrefEnv({ + set: [ + ["print.save_as_pdf.use_page_rule_size_as_paper_size.enabled", true], + ], + }); + await helper.startPrint(); + await helper.setupMockPrint(); + + helper.assertSettingsMatch({ + printerName: pdfPrinterName, + usePageRuleSizeAsPaperSize: true, + }); + + await helper.openMoreSettings(); + + let paperSize = helper.get("paper-size"); + + ok(BrowserTestUtils.is_hidden(paperSize), "Paper size section is hidden"); + + await helper.waitForSettingsEvent(async () => { + await changeDestination(helper, "down"); + }); + await helper.awaitAnimationFrame(); + + helper.assertSettingsMatch({ + printerName: someOtherPrinter, + usePageRuleSizeAsPaperSize: false, + }); + + ok(BrowserTestUtils.is_visible(paperSize), "Paper size section is shown"); + + await helper.closeDialog(); + }, "page_size.html"); +}); + +add_task(async function testShowPaperSizeSectionWithoutPageSize() { + await PrintHelper.withTestPage(async helper => { + await helper.addMockPrinter(someOtherPrinter); + await SpecialPowers.pushPrefEnv({ + set: [ + ["print.save_as_pdf.use_page_rule_size_as_paper_size.enabled", true], + ], + }); + await helper.startPrint(); + await helper.setupMockPrint(); + + helper.assertSettingsMatch({ + printerName: pdfPrinterName, + usePageRuleSizeAsPaperSize: true, + }); + + await helper.openMoreSettings(); + + let paperSize = helper.get("paper-size"); + + ok(BrowserTestUtils.is_visible(paperSize), "Paper size section is shown"); + + await helper.waitForSettingsEvent(async () => { + await changeDestination(helper, "down"); + }); + await helper.awaitAnimationFrame(); + + helper.assertSettingsMatch({ + printerName: someOtherPrinter, + usePageRuleSizeAsPaperSize: false, + }); + + ok( + BrowserTestUtils.is_visible(paperSize), + "Paper size section is still shown" + ); + + await helper.closeDialog(); + }); +}); + +add_task(async function testCustomPageSizePassedToPrinter() { + await PrintHelper.withTestPage(async helper => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["print.save_as_pdf.use_page_rule_size_as_paper_size.enabled", true], + ], + }); + await helper.startPrint(); + await helper.setupMockPrint(); + + is(helper.settings.paperWidth.toFixed(1), "4.0", "Check paperWidth"); + is(helper.settings.paperHeight.toFixed(1), "12.0", "Check paperHeight"); + is( + helper.settings.orientation, + Ci.nsIPrintSettings.kPortraitOrientation, + "Check orientation" + ); + is( + helper.settings.paperSizeUnit, + helper.settings.kPaperSizeInches, + "Check paperSizeUnit" + ); + + await helper.closeDialog(); + }, "page_size.html"); +}); + +add_task(async function testNamedPageSizePassedToPrinter() { + await PrintHelper.withTestPage(async helper => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["print.save_as_pdf.use_page_rule_size_as_paper_size.enabled", true], + ], + }); + await helper.startPrint(); + await helper.setupMockPrint(); + + helper.assertSettingsMatch({ + printerName: pdfPrinterName, + usePageRuleSizeAsPaperSize: true, + }); + + is(helper.settings.paperId, "iso_a5", "Check paper id is A5"); + is( + helper.settings.paperWidth.toFixed(1), + "5.8", + "Check paperWidth is ~148mm (in inches)" + ); + is( + helper.settings.paperHeight.toFixed(1), + "8.3", + "Check paperHeight is ~210mm (in inches)" + ); + is( + helper.settings.paperSizeUnit, + helper.settings.kPaperSizeInches, + "Check paperSizeUnit" + ); + is( + helper.settings.orientation, + Ci.nsIPrintSettings.kPortraitOrientation, + "Check orientation" + ); + await helper.closeDialog(); + }, "page_size_a5.html"); +}); + +add_task(async function testLandscapePageSizePassedToPrinter() { + await PrintHelper.withTestPage(async helper => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["print.save_as_pdf.use_page_rule_size_as_paper_size.enabled", true], + ], + }); + await helper.startPrint(); + await helper.setupMockPrint(); + + helper.assertSettingsMatch({ + printerName: pdfPrinterName, + usePageRuleSizeAsPaperSize: true, + }); + + is(helper.settings.paperId, "iso_a5", "Check paper id is A5"); + is( + helper.settings.orientation, + Ci.nsIPrintSettings.kLandscapeOrientation, + "Check orientation is landscape" + ); + is( + helper.settings.paperWidth.toFixed(1), + "5.8", + "Check paperWidth is ~148mm (in inches)" + ); + is( + helper.settings.paperHeight.toFixed(1), + "8.3", + "Check paperHeight is ~210mm (in inches)" + ); + is( + helper.settings.paperSizeUnit, + helper.settings.kPaperSizeInches, + "Check paperSizeUnit" + ); + + await helper.closeDialog(); + }, "page_size_a5_landscape.html"); +}); + +add_task(async function testZeroSizePassedToPrinter() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + await helper.setupMockPrint(); + + let pageSize = helper.get("paper-size"); + let orientation = helper.get("orientation"); + + ok(BrowserTestUtils.is_visible(pageSize), "Fallback to page size section"); + ok(BrowserTestUtils.is_visible(orientation), "Orientation picker is shown"); + + await helper.closeDialog(); + }); +}, "page_size_zero.html"); + +add_task(async function testZeroWidthPassedToPrinter() { + await PrintHelper.withTestPage(async helper => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["print.save_as_pdf.use_page_rule_size_as_paper_size.enabled", true], + ], + }); + await helper.startPrint(); + await helper.setupMockPrint(); + + let pageSize = helper.get("paper-size"); + let orientation = helper.get("orientation"); + + ok(BrowserTestUtils.is_visible(pageSize), "Fallback to page size section"); + ok(BrowserTestUtils.is_visible(orientation), "Orientation picker is shown"); + + await helper.closeDialog(); + }); +}, "page_size_zero_width.html"); + +add_task(async function testDefaultSizePassedToPrinter() { + await PrintHelper.withTestPage(async helper => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["print.save_as_pdf.use_page_rule_size_as_paper_size.enabled", true], + ], + }); + await helper.startPrint(); + await helper.setupMockPrint(); + + helper.assertSettingsMatch({ + printerName: pdfPrinterName, + usePageRuleSizeAsPaperSize: true, + }); + + is(helper.settings.paperWidth.toFixed(1), "8.5", "Check paperWidth"); + is(helper.settings.paperHeight.toFixed(1), "11.0", "Check paperHeight"); + is( + helper.settings.paperSizeUnit, + helper.settings.kPaperSizeInches, + "Check paperSizeUnit" + ); + + await helper.closeDialog(); + }); +}); diff --git a/toolkit/components/printing/tests/browser_print_paper_sizes.js b/toolkit/components/printing/tests/browser_print_paper_sizes.js new file mode 100644 index 0000000000..6f81a2a0b4 --- /dev/null +++ b/toolkit/components/printing/tests/browser_print_paper_sizes.js @@ -0,0 +1,120 @@ +"use strict"; + +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +async function selectPaperOptionWithValue(helper, value) { + let paperSelect = helper.get("paper-size-picker"); + paperSelect.dispatchSettingsChange({ + paperId: value, + }); + await helper.awaitAnimationFrame(); +} + +add_task(async function testBadPaperSizeUnitCorrection() { + await PrintHelper.withTestPage(async helper => { + // Set prefs to select a non-default paper size + await SpecialPowers.pushPrefEnv({ + set: [ + ["print.printer_Mozilla_Save_to_PDF.print_paper_id", "na_letter"], + // paperSizeUnit is a bogus value, but the dimensions are correct for inches + ["print.printer_Mozilla_Save_to_PDF.print_paper_size_unit", 99], + ["print.printer_Mozilla_Save_to_PDF.print_paper_height", "11.0"], + ["print.printer_Mozilla_Save_to_PDF.print_paper_width", "8.50"], + ], + }); + await helper.startPrint(); + + let paperSelect = helper.get("paper-size-picker"); + is(paperSelect.value, "na_letter", "The expected paper size is selected"); + is( + helper.viewSettings.paperId, + "na_letter", + "The settings have the expected paperId" + ); + is( + helper.viewSettings.paperSizeUnit, + helper.settings.kPaperSizeInches, + "Check paperSizeUnit" + ); + is(helper.viewSettings.paperWidth.toFixed(1), "8.5", "Check paperWidth"); + is(helper.viewSettings.paperHeight.toFixed(1), "11.0", "Check paperHeight"); + + await selectPaperOptionWithValue(helper, "iso_a3"); + is(paperSelect.value, "iso_a3", "The expected paper size is selected"); + is( + helper.viewSettings.paperId, + "iso_a3", + "The settings have the expected paperId" + ); + is( + helper.viewSettings.paperSizeUnit, + helper.settings.kPaperSizeInches, + "Check paperSizeUnit" + ); + is(helper.viewSettings.paperWidth.toFixed(1), "11.7", "Check paperWidth"); + is(helper.viewSettings.paperHeight.toFixed(1), "16.5", "Check paperHeight"); + + await SpecialPowers.popPrefEnv(); + await helper.closeDialog(); + }); +}); + +add_task(async function testMismatchedPaperSizeUnitCorrection() { + await PrintHelper.withTestPage(async helper => { + // Set prefs to select a non-default paper size + await SpecialPowers.pushPrefEnv({ + set: [ + ["print.printer_Mozilla_Save_to_PDF.print_paper_id", "na_ledger"], + // paperSizeUnit is millimeters, but the dimensions are correct for inches + ["print.printer_Mozilla_Save_to_PDF.print_paper_size_unit", 1], + ["print.printer_Mozilla_Save_to_PDF.print_paper_width", "11.0"], + ["print.printer_Mozilla_Save_to_PDF.print_paper_height", "17.0"], + ], + }); + await helper.startPrint(); + + let paperSelect = helper.get("paper-size-picker"); + is(paperSelect.value, "na_ledger", "The expected paper size is selected"); + + // We expect to honor the paperSizeUnit, and convert paperWidth/Height to that unit + is( + helper.viewSettings.paperId, + "na_ledger", + "The settings have the expected paperId" + ); + is( + helper.viewSettings.paperSizeUnit, + helper.settings.kPaperSizeMillimeters, + "Check paperSizeUnit" + ); + is(helper.viewSettings.paperWidth.toFixed(1), "279.4", "Check paperWidth"); + is( + helper.viewSettings.paperHeight.toFixed(1), + "431.8", + "Check paperHeight" + ); + + await selectPaperOptionWithValue(helper, "iso_a3"); + is(paperSelect.value, "iso_a3", "The expected paper size is selected"); + is( + helper.viewSettings.paperId, + "iso_a3", + "The settings have the expected paperId" + ); + is( + helper.viewSettings.paperSizeUnit, + helper.settings.kPaperSizeMillimeters, + "Check paperSizeUnit" + ); + is(helper.viewSettings.paperWidth.toFixed(1), "297.0", "Check paperWidth"); + is( + helper.viewSettings.paperHeight.toFixed(1), + "420.0", + "Check paperHeight" + ); + + await SpecialPowers.popPrefEnv(); + await helper.closeDialog(); + }); +}); diff --git a/toolkit/components/printing/tests/browser_print_pdf_on_frame_load.js b/toolkit/components/printing/tests/browser_print_pdf_on_frame_load.js new file mode 100644 index 0000000000..29864e430d --- /dev/null +++ b/toolkit/components/printing/tests/browser_print_pdf_on_frame_load.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +add_task(async function test_print_pdf_on_frame_load() { + is( + document.querySelector(".printPreviewBrowser"), + null, + "There shouldn't be any print preview browser" + ); + + await BrowserTestUtils.withNewTab( + `${TEST_PATH}file_print_pdf_on_frame_load.html`, + async function (browser) { + info( + "Waiting for window.print() to run and ensure we're showing the preview..." + ); + + let helper = new PrintHelper(browser); + await waitForPreviewVisible(); + + info("Preview shown, waiting for it to be updated..."); + + await TestUtils.waitForCondition(() => helper.sheetCount != 0); + + is(helper.sheetCount, 2, "Both pages should be loaded"); + + gBrowser.getTabDialogBox(browser).abortAllDialogs(); + } + ); +}); diff --git a/toolkit/components/printing/tests/browser_print_scaling.js b/toolkit/components/printing/tests/browser_print_scaling.js new file mode 100644 index 0000000000..85b8a68449 --- /dev/null +++ b/toolkit/components/printing/tests/browser_print_scaling.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function testInvalidScaleResetAfterDestinationChange() { + const mockPrinterName = "Fake Printer"; + await PrintHelper.withTestPage(async helper => { + helper.addMockPrinter(mockPrinterName); + await helper.startPrint(); + + let destinationPicker = helper.get("printer-picker"); + + await helper.openMoreSettings(); + let scaleRadio = helper.get("percent-scale-choice"); + await helper.assertSettingsChanged( + { shrinkToFit: true }, + { shrinkToFit: false }, + async () => { + await helper.waitForPreview(() => helper.click(scaleRadio)); + } + ); + let percentScale = helper.get("percent-scale"); + + let scaleError = helper.get("error-invalid-scale"); + + await helper.assertSettingsNotChanged({ scaling: 1 }, async () => { + ok(scaleError.hidden, "Scale error is hidden"); + await helper.text(percentScale, "9"); + await BrowserTestUtils.waitForAttributeRemoval("hidden", scaleError); + ok(!scaleError.hidden, "Scale error is showing"); + }); + + is(destinationPicker.disabled, false, "Destination picker is enabled"); + + // Select a new printer + await helper.dispatchSettingsChange({ printerName: mockPrinterName }); + await BrowserTestUtils.waitForCondition( + () => scaleError.hidden, + "Wait for scale error to be hidden" + ); + is(percentScale.value, "100", "Scale has reset to 100"); + + await helper.closeDialog(); + }); +}); diff --git a/toolkit/components/printing/tests/browser_print_selection.js b/toolkit/components/printing/tests/browser_print_selection.js new file mode 100644 index 0000000000..fc62da00e2 --- /dev/null +++ b/toolkit/components/printing/tests/browser_print_selection.js @@ -0,0 +1,244 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const frameSource = + "<a href='about:mozilla'>some text</a><a id='other' href='about:about'>other text</a>"; +const sources = [ + `<html><iframe id="f" srcdoc="${frameSource}"></iframe></html>`, + `<html><iframe id="f" src="https://example.com/document-builder.sjs?html=${frameSource}"></iframe></html>`, +]; + +async function getPreviewText(previewBrowser) { + return SpecialPowers.spawn(previewBrowser, [], function () { + return content.document.body.textContent; + }); +} + +add_task(async function print_selection() { + let i = 0; + for (let source of sources) { + is( + document.querySelector(".printPreviewBrowser"), + null, + "There shouldn't be any print preview browser" + ); + + await BrowserTestUtils.withNewTab( + "data:text/html," + source, + async function (browser) { + let frameBC = browser.browsingContext.children[0]; + await SpecialPowers.spawn(frameBC, [], () => { + let element = content.document.getElementById("other"); + content.focus(); + content.getSelection().selectAllChildren(element); + }); + + let helper = new PrintHelper(browser); + + // If you change this, change nsContextMenu.printSelection() too. + PrintUtils.startPrintWindow(frameBC, { + printSelectionOnly: true, + }); + + await waitForPreviewVisible(); + + let previewBrowser = document.querySelector( + ".printPreviewBrowser[previewtype='selection']" + ); + let previewText = () => getPreviewText(previewBrowser); + // The preview process is async, wait for it to not be empty. + let textContent = await TestUtils.waitForCondition(previewText); + is(textContent, "other text", "Correct content loaded"); + + let printSelect = document + .querySelector(".printSettingsBrowser") + .contentDocument.querySelector("#source-version-selection-radio"); + ok( + BrowserTestUtils.is_visible(printSelect), + "Print selection checkbox is shown" + ); + ok(printSelect.checked, "Print selection checkbox is checked"); + + let file = helper.mockFilePicker(`browser_print_selection-${i++}.pdf`); + await helper.assertPrintToFile(file, () => { + helper.click(helper.get("print-button")); + }); + PrintHelper.resetPrintPrefs(); + } + ); + } +}); + +add_task(async function print_selection_parent_process() { + is( + document.querySelector(".printPreviewBrowser"), + null, + "There shouldn't be any print preview browser" + ); + + await BrowserTestUtils.withNewTab("about:support", async function (browser) { + ok(!browser.isRemote, "Page loaded in parent process"); + let selectedText = await SpecialPowers.spawn( + browser.browsingContext, + [], + () => { + let element = content.document.querySelector("h1"); + content.focus(); + content.getSelection().selectAllChildren(element); + return element.textContent; + } + ); + ok(selectedText, "There is selected text"); + + let helper = new PrintHelper(browser); + + // If you change this, change nsContextMenu.printSelection() too. + PrintUtils.startPrintWindow(browser.browsingContext, { + printSelectionOnly: true, + }); + + await waitForPreviewVisible(); + + let previewBrowser = document.querySelector( + ".printPreviewBrowser[previewtype='selection']" + ); + let previewText = () => getPreviewText(previewBrowser); + // The preview process is async, wait for it to not be empty. + let textContent = await TestUtils.waitForCondition(previewText); + is(textContent, selectedText, "Correct content loaded"); + + let printSelect = document + .querySelector(".printSettingsBrowser") + .contentDocument.querySelector("#source-version-selection-radio"); + ok( + BrowserTestUtils.is_visible(printSelect), + "Print selection checkbox is shown" + ); + ok(printSelect.checked, "Print selection checkbox is checked"); + + let file = helper.mockFilePicker(`browser_print_selection_parent.pdf`); + await helper.assertPrintToFile(file, () => { + helper.click(helper.get("print-button")); + }); + PrintHelper.resetPrintPrefs(); + }); +}); + +add_task(async function no_print_selection() { + // Ensures the print selection checkbox is hidden if nothing is selected + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + await helper.openMoreSettings(); + + let printSelect = helper.get("source-version-selection"); + ok( + BrowserTestUtils.is_hidden(printSelect), + "Print selection checkbox is hidden" + ); + await helper.closeDialog(); + }); +}); + +add_task(async function print_selection_switch() { + await PrintHelper.withTestPage(async helper => { + await SpecialPowers.spawn(helper.sourceBrowser, [], async function () { + let element = content.document.querySelector("h1"); + content.window.getSelection().selectAllChildren(element); + }); + + await helper.startPrint(); + await helper.openMoreSettings(); + let printSource = helper.get("source-version-source-radio"); + ok(printSource.checked, "Print source radio is checked"); + let printSelect = helper.get("source-version-selection-radio"); + ok(!printSelect.checked, "Print selection radio is not checked"); + + function getCurrentBrowser(previewType) { + let browser = document.querySelector( + `.printPreviewBrowser[previewtype="${previewType}"]` + ); + is( + browser.parentElement.getAttribute("previewtype"), + previewType, + "Expected browser is showing" + ); + return browser; + } + + let selectedText = "Article title"; + let fullText = await getPreviewText(getCurrentBrowser("source")); + + helper.assertSettingsMatch({ + printSelectionOnly: false, + }); + + await helper.assertSettingsChanged( + { printSelectionOnly: false }, + { printSelectionOnly: true }, + async () => { + await helper.waitForPreview(() => helper.click(printSelect)); + let text = await getPreviewText(getCurrentBrowser("selection")); + is(text, selectedText, "Correct content loaded"); + } + ); + + await helper.assertSettingsChanged( + { printSelectionOnly: true }, + { printSelectionOnly: false }, + async () => { + await helper.waitForPreview(() => helper.click(printSource)); + let text = await getPreviewText(getCurrentBrowser("source")); + is(text, fullText, "Correct content loaded"); + } + ); + + await helper.closeDialog(); + }); +}); + +add_task(async function open_system_print_with_selection_and_pdf() { + await BrowserTestUtils.withNewTab( + "data:text/html," + sources[0], + async function (browser) { + let frameBC = browser.browsingContext.children[0]; + await SpecialPowers.spawn(frameBC, [], () => { + let element = content.document.getElementById("other"); + content.focus(); + content.getSelection().selectAllChildren(element); + }); + + let helper = new PrintHelper(browser); + + // Add another printer so the system dialog link is shown on Windows. + helper.addMockPrinter("A printer"); + + PrintUtils.startPrintWindow(frameBC, {}); + + await waitForPreviewVisible(); + + // Ensure that the PDF printer is selected since the way settings are + // cloned is different in this case. + is( + helper.settings.printerName, + "Mozilla Save to PDF", + "Mozilla Save to PDF is the current printer." + ); + + await helper.setupMockPrint(); + + helper.click(helper.get("open-dialog-link")); + await helper.withClosingFn(() => { + helper.resolveShowSystemDialog(); + helper.resolvePrint(); + }); + + ok( + helper.systemDialogOpenedWithSelection, + "Expect system print dialog to be notified of selection" + ); + + PrintHelper.resetPrintPrefs(); + } + ); +}); diff --git a/toolkit/components/printing/tests/browser_print_settings_fallback.js b/toolkit/components/printing/tests/browser_print_settings_fallback.js new file mode 100644 index 0000000000..6f30c87d8c --- /dev/null +++ b/toolkit/components/printing/tests/browser_print_settings_fallback.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let badPrinterName = "Bad"; +let otherPrinterName = "Fallback"; + +async function setupPrinters(helper) { + let badPrinter = helper.addMockPrinter({ + name: badPrinterName, + }); + + let badPrinterInfo = await badPrinter.printerInfo; + badPrinterInfo.defaultSettings.printerName = ""; + + helper.addMockPrinter(otherPrinterName); + + await SpecialPowers.pushPrefEnv({ + set: [["print_printer", badPrinterName]], + }); +} + +add_task(async function testBadPrinterSettings() { + await PrintHelper.withTestPage(async helper => { + await setupPrinters(helper); + await helper.startPrint(); + + let destinationPicker = helper.get("printer-picker"); + // Fallback can be any other printer, the fallback or save to pdf printer. + isnot( + destinationPicker.value, + badPrinterName, + "A fallback printer is selected" + ); + + await helper.closeDialog(); + }); +}); diff --git a/toolkit/components/printing/tests/browser_print_simplified_mode.js b/toolkit/components/printing/tests/browser_print_simplified_mode.js new file mode 100644 index 0000000000..8826f9071b --- /dev/null +++ b/toolkit/components/printing/tests/browser_print_simplified_mode.js @@ -0,0 +1,262 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function switch_print_preview_browsers() { + await PrintHelper.withTestPage(async helper => { + // Wait for the article state to be determined. + await helper.waitForReaderModeReady(); + + // Start print preview. + await helper.startPrint(); + let sourcePreviewBrowser = helper.currentPrintPreviewBrowser; + + { + // Assert that we are showing the initial content on default print preview browser + let [headerText, headingText] = await SpecialPowers.spawn( + sourcePreviewBrowser, + [], + async function () { + return [ + content.document.querySelector("header").textContent, + content.document.querySelector("h1").textContent, + ]; + } + ); + is(headerText, "Site header", "Should have initial content."); + is(headingText, "Article title", "Should have initial title."); + } + + // Here we call simplified mode + await helper.openMoreSettings(); + let simplifyRadio = helper.get("source-version-simplified-radio"); + ok(!simplifyRadio.checked, "Simplify page is not checked"); + ok(BrowserTestUtils.is_visible(simplifyRadio), "Simplify is shown"); + + await helper.waitForPreview(() => helper.click(simplifyRadio)); + let simplifiedPreviewBrowser = helper.currentPrintPreviewBrowser; + is( + simplifiedPreviewBrowser.getAttribute("previewtype"), + "simplified", + "Simplified browser was rendered" + ); + is( + simplifiedPreviewBrowser.closest("stack").getAttribute("previewtype"), + "simplified", + "Simplified browser is selected" + ); + ok( + BrowserTestUtils.is_visible(simplifiedPreviewBrowser), + "Simplified browser is visible" + ); + ok(simplifyRadio.checked, "Simplify page is checked"); + + { + // Assert that we are showing custom content on simplified print preview browser + let [hasHeader, headingText] = await SpecialPowers.spawn( + simplifiedPreviewBrowser, + [], + async function () { + return [ + !!content.document.querySelector("header"), + content.document.querySelector("h1").textContent, + ]; + } + ); + ok(!hasHeader, "The header was simplified out"); + is(headingText, "Article title", "The heading is still there"); + } + + // Switch back to default print preview content + let sourceRadio = helper.get("source-version-source-radio"); + ok(!sourceRadio.checked, "Source is not checked"); + await helper.waitForPreview(() => helper.click(sourceRadio)); + is( + helper.currentPrintPreviewBrowser, + sourcePreviewBrowser, + "Source browser was rendered" + ); + is( + sourcePreviewBrowser.getAttribute("previewtype"), + "source", + "Source browser was rendered" + ); + is( + sourcePreviewBrowser.closest("stack").getAttribute("previewtype"), + "source", + "Source browser is selected" + ); + ok( + BrowserTestUtils.is_visible(sourcePreviewBrowser), + "Source browser is visible" + ); + ok(sourceRadio.checked, "Source version is checked"); + + { + // Assert that we are showing the initial content on default print preview browser + let headerText = await SpecialPowers.spawn( + sourcePreviewBrowser, + [], + async function () { + return content.document.querySelector("header").textContent; + } + ); + is(headerText, "Site header", "Should have initial content."); + } + + await helper.closeDialog(); + }, "simplifyArticleSample.html"); +}); + +add_task(async function testPrintBackgroundsDisabledSimplified() { + await PrintHelper.withTestPage(async helper => { + // Wait for the article state to be determined. + await helper.waitForReaderModeReady(); + await helper.startPrint(); + + helper.assertPreviewedWithSettings({ + printBGImages: false, + printBGColors: false, + }); + + await helper.openMoreSettings(); + + let printBackgrounds = helper.get("backgrounds-enabled"); + ok(!printBackgrounds.checked, "Print backgrounds is not checked"); + ok(!printBackgrounds.disabled, "Print backgrounds in not disabled"); + + await helper.assertSettingsChanged( + { printBGImages: false, printBGColors: false }, + { printBGImages: true, printBGColors: true }, + async () => { + await helper.waitForPreview(() => helper.click(printBackgrounds)); + } + ); + + // Print backgrounds was enabled for preview. + ok(printBackgrounds.checked, "Print backgrounds is checked"); + ok(!printBackgrounds.disabled, "Print backgrounds is not disabled"); + helper.assertPreviewedWithSettings({ + printBGImages: true, + printBGColors: true, + }); + + let simplifyRadio = helper.get("source-version-simplified-radio"); + ok(!simplifyRadio.checked, "Simplify page is not checked"); + ok(BrowserTestUtils.is_visible(simplifyRadio), "Simplify is shown"); + + // Switch to simplified mode. + await helper.waitForPreview(() => helper.click(simplifyRadio)); + + // Print backgrounds should be disabled, it's incompatible with simplified. + ok(!printBackgrounds.checked, "Print backgrounds is now unchecked"); + ok(printBackgrounds.disabled, "Print backgrounds has been disabled"); + helper.assertPreviewedWithSettings({ + printBGImages: false, + printBGColors: false, + }); + + // Switch back to source, printBackgrounds is remembered. + let sourceRadio = helper.get("source-version-source-radio"); + ok(!sourceRadio.checked, "Source is not checked"); + ok(BrowserTestUtils.is_visible(sourceRadio), "Source is shown"); + + await helper.waitForPreview(() => helper.click(sourceRadio)); + + ok(printBackgrounds.checked, "Print backgrounds setting was remembered"); + ok(!printBackgrounds.disabled, "Print backgrounds can be changed again"); + helper.assertPreviewedWithSettings({ + printBGImages: true, + printBGColors: true, + }); + + await helper.closeDialog(); + }, "simplifyArticleSample.html"); +}); + +add_task(async function testSimplifyHiddenNonArticle() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + await helper.openMoreSettings(); + let sourceVersionSection = helper.get("source-version-section"); + ok( + BrowserTestUtils.is_hidden(sourceVersionSection), + "Source version is hidden" + ); + await helper.closeDialog(); + }, "simplifyNonArticleSample.html"); +}); + +add_task(async function testSimplifyNonArticleTabModal() { + await PrintHelper.withTestPage(async helper => { + let tab = gBrowser.selectedTab; + + // Trick browser to think loaded tab has isArticle property set as true + tab.linkedBrowser.isArticle = true; + + // Enter print preview + await helper.startPrint(); + + // Assert that we are showing the initial content on default print preview browser + await SpecialPowers.spawn( + helper.currentPrintPreviewBrowser, + [], + async () => { + is( + content.document.title, + "Non article title", + "Should have initial content." + ); + } + ); + + await helper.openMoreSettings(); + + // Simplify the page. + let simplifyRadio = helper.get("source-version-simplified-radio"); + ok(!simplifyRadio.checked, "Simplify is off"); + await helper.waitForPreview(() => helper.click(simplifyRadio)); + let simplifiedPreviewBrowser = helper.currentPrintPreviewBrowser; + is( + simplifiedPreviewBrowser.getAttribute("previewtype"), + "simplified", + "The simplified browser is shown" + ); + + // Assert that simplify page option is checked + ok(simplifyRadio.checked, "Should have simplify page option checked"); + + // Assert that we are showing recovery content on simplified print preview browser + await SpecialPowers.spawn(simplifiedPreviewBrowser, [], async () => { + await ContentTaskUtils.waitForCondition( + () => content.document.title === "Failed to load article from page", + "Simplified document title should be updated with recovery title." + ); + }); + + await helper.closeDialog(); + }, "simplifyNonArticleSample.html"); +}); + +add_task(async function testSimplifyHiddenReaderMode() { + await PrintHelper.withTestPage(async helper => { + let tab = gBrowser.selectedTab; + + // Trigger reader mode for the tab + let readerButton = document.getElementById("reader-mode-button"); + await TestUtils.waitForCondition(() => !readerButton.hidden); + readerButton.click(); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + // Print from reader mode + await helper.startPrint(); + await helper.openMoreSettings(); + let sourceVersionSection = helper.get("source-version-section"); + ok( + BrowserTestUtils.is_hidden(sourceVersionSection), + "Source version is hidden in reader mode" + ); + await helper.closeDialog(); + }, "simplifyArticleSample.html"); +}); diff --git a/toolkit/components/printing/tests/browser_print_stream.js b/toolkit/components/printing/tests/browser_print_stream.js new file mode 100644 index 0000000000..dff1ee8762 --- /dev/null +++ b/toolkit/components/printing/tests/browser_print_stream.js @@ -0,0 +1,102 @@ +//creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService( + Ci.nsIPrintSettingsService +); + +async function printToDestination(aBrowser, aDestination) { + let tmpDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + let fileName = `printDestinationTest-${aDestination}.pdf`; + let filePath = PathUtils.join(tmpDir.path, fileName); + + info(`Printing to ${filePath}`); + + let settings = PSSVC.createNewPrintSettings(); + settings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF; + settings.outputDestination = aDestination; + + settings.headerStrCenter = ""; + settings.headerStrLeft = ""; + settings.headerStrRight = ""; + settings.footerStrCenter = ""; + settings.footerStrLeft = ""; + settings.footerStrRight = ""; + + settings.unwriteableMarginTop = 1; /* Just to ensure settings are respected on both */ + let outStream = null; + if (aDestination == Ci.nsIPrintSettings.kOutputDestinationFile) { + settings.toFileName = PathUtils.join(tmpDir.path, fileName); + } else { + is(aDestination, Ci.nsIPrintSettings.kOutputDestinationStream); + outStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + let tmpFile = tmpDir.clone(); + tmpFile.append(fileName); + outStream.init(tmpFile, -1, 0o666, 0); + settings.outputStream = outStream; + } + + await aBrowser.browsingContext.print(settings); + + return filePath; +} + +add_task(async function testPrintToStream() { + await PrintHelper.withTestPage(async helper => { + let filePath = await printToDestination( + helper.sourceBrowser, + Ci.nsIPrintSettings.kOutputDestinationFile + ); + let streamPath = await printToDestination( + helper.sourceBrowser, + Ci.nsIPrintSettings.kOutputDestinationStream + ); + + // In Cocoa the CGContext adds a hash, plus there are other minor + // non-user-visible differences, so we need to be a bit more sloppy there. + // + // We see one byte difference in Windows and Linux on automation sometimes, + // though files are consistently the same locally, that needs + // investigation, but it's probably harmless. + const maxSizeDifference = AppConstants.platform == "macosx" ? 100 : 2; + + // Buffering shenanigans? Wait for sizes to match... There's no great + // IOUtils methods to force a flush without writing anything... + await TestUtils.waitForCondition(async function () { + let fileStat = await IOUtils.stat(filePath); + let streamStat = await IOUtils.stat(streamPath); + + ok(fileStat.size > 0, "File file should not be empty: " + fileStat.size); + ok( + streamStat.size > 0, + "Stream file should not be empty: " + streamStat.size + ); + return Math.abs(fileStat.size - streamStat.size) <= maxSizeDifference; + }, "Sizes should (almost) match"); + + if (false) { + // This doesn't work reliably on automation, but works locally, see + // above... + let fileData = await IOUtils.read(filePath); + let streamData = await IOUtils.read(streamPath); + ok(!!fileData.length, "File should not be empty"); + is(fileData.length, streamData.length, "File size should be equal"); + for (let i = 0; i < fileData.length; ++i) { + if (fileData[i] != streamData[i]) { + is( + fileData[i], + streamData[i], + `Files should be equal (byte ${i} different)` + ); + break; + } + } + } + + await IOUtils.remove(filePath); + await IOUtils.remove(streamPath); + }); +}); diff --git a/toolkit/components/printing/tests/browser_sheet_count.js b/toolkit/components/printing/tests/browser_sheet_count.js new file mode 100644 index 0000000000..d08020db18 --- /dev/null +++ b/toolkit/components/printing/tests/browser_sheet_count.js @@ -0,0 +1,356 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function testSheetCount() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + + let sheetCountElement = helper.get("sheet-count"); + let { id } = helper.doc.l10n.getAttributes(sheetCountElement); + is(id, "printui-sheets-count", "The l10n id is correct"); + let initialSheetCount = helper.sheetCount; + ok(initialSheetCount >= 1, "There is an initial sheet count"); + + await helper.openMoreSettings(); + + let scaleRadio = helper.get("percent-scale-choice"); + await helper.waitForPreview(() => helper.click(scaleRadio)); + + let percentScale = helper.get("percent-scale"); + await helper.waitForPreview(() => helper.text(percentScale, "200")); + + let zoomedSheetCount = helper.sheetCount; + ok(zoomedSheetCount > initialSheetCount, "The sheet count increased"); + + // Since we're using the Save to PDF printer, the numCopies element should + // be hidden and its value ignored. + let numCopies = helper.get("copies-count"); + ok(BrowserTestUtils.is_hidden(numCopies), "numCopies element is hidden"); + helper.dispatchSettingsChange({ + numCopies: 4, + }); + is( + helper.sheetCount, + zoomedSheetCount, + "numCopies is ignored for Save to PDF printer" + ); + + is(helper.viewSettings.numCopies, 1, "numCopies is 1 in viewSettings"); + + // We don't have any "real" printers set up for testing yet, so insert a modified + // copy of the PDF printer which pretends to be real, and switch to that + // to triggers the component to update. + let realPrinterName = "My real printer"; + let pdfPrinterInfo = + helper.win.PrintSettingsViewProxy.availablePrinters[ + PrintUtils.SAVE_TO_PDF_PRINTER + ]; + let mockPrinterInfo = Object.assign({}, pdfPrinterInfo, {}); + mockPrinterInfo.settings = pdfPrinterInfo.settings.clone(); + mockPrinterInfo.settings.outputFormat = + Ci.nsIPrintSettings.kOutputFormatNative; + mockPrinterInfo.settings.printerName = realPrinterName; + + helper.win.PrintSettingsViewProxy.availablePrinters[realPrinterName] = + mockPrinterInfo; + await helper.dispatchSettingsChange({ + printerName: realPrinterName, + }); + await helper.awaitAnimationFrame(); + + let { settings, viewSettings } = helper; + + is( + settings.printerName, + realPrinterName, + "Sanity check the current settings have the new printerName" + ); + is( + settings.outputFormat, + Ci.nsIPrintSettings.kOutputFormatNative, + "The new printer has the correct outputFormat" + ); + is(viewSettings.numCopies, 4, "numCopies is 4 in viewSettings"); + + // numCopies is now visible and sheetCount is multiplied by numCopies. + ok(BrowserTestUtils.is_visible(numCopies), "numCopies element is visible"); + is(numCopies.value, "4", "numCopies displays the correct value"); + is( + helper.sheetCount, + zoomedSheetCount * 4, + "numCopies is used when using a non-PDF printer" + ); + + await helper.closeDialog(); + }); +}); + +add_task(async function testSheetCountPageRange() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + await helper.waitForPreview(() => + helper.dispatchSettingsChange({ + shrinkToFit: false, + scaling: 2, + }) + ); + + await BrowserTestUtils.waitForCondition( + () => helper.sheetCount != 1, + "Wait for sheet count to update" + ); + let sheets = helper.sheetCount; + ok(sheets >= 3, "There are at least 3 pages"); + + // Set page range to 2-3, sheet count should be 2. + await helper.waitForPreview(() => + helper.dispatchSettingsChange({ + pageRanges: [2, 3], + }) + ); + + sheets = helper.sheetCount; + is(sheets, 2, "There are now only 2 pages shown"); + }); +}); + +// Test that enabling duplex printing updates the sheet count accordingly. +add_task(async function testSheetCountDuplex() { + await PrintHelper.withTestPage(async helper => { + const mockPrinterName = "DuplexCapablePrinter"; + const printer = helper.addMockPrinter(mockPrinterName); + printer.supportsDuplex = Promise.resolve(true); + + await helper.startPrint(); + await helper.dispatchSettingsChange({ printerName: mockPrinterName }); + + // Set scale and shinkToFit to make the document + // bigger so that it spans multiple pages. + await helper.waitForPreview(() => + helper.dispatchSettingsChange({ + shrinkToFit: false, + scaling: 4, + duplex: Ci.nsIPrintSettings.kDuplexNone, + }) + ); + await BrowserTestUtils.waitForCondition( + () => helper.sheetCount != 1, + "Wait for sheet count to update" + ); + let singleSidedSheets = helper.sheetCount; + ok(singleSidedSheets >= 2, "There are at least 2 pages"); + + // Turn on long-edge duplex printing and ensure the sheet count is halved. + await helper.waitForSettingsEvent(() => + helper.dispatchSettingsChange({ + duplex: Ci.nsIPrintSettings.kDuplexFlipOnLongEdge, + }) + ); + await BrowserTestUtils.waitForCondition( + () => helper.sheetCount != singleSidedSheets, + "Wait for sheet count to update" + ); + let duplexLongEdgeSheets = helper.sheetCount; + is( + duplexLongEdgeSheets, + Math.ceil(singleSidedSheets / 2), + "Long-edge duplex printing halved the sheet count" + ); + + // Turn off duplex printing + await helper.waitForSettingsEvent(() => + helper.dispatchSettingsChange({ + duplex: Ci.nsIPrintSettings.kDuplexNone, + }) + ); + await BrowserTestUtils.waitForCondition( + () => helper.sheetCount == singleSidedSheets, + "Wait for sheet count to update" + ); + + // Turn on short-edge duplex printing and ensure the + // sheet count matches the long-edge duplex sheet count. + await helper.waitForSettingsEvent(() => + helper.dispatchSettingsChange({ + duplex: Ci.nsIPrintSettings.kDuplexFlipOnShortEdge, + }) + ); + await BrowserTestUtils.waitForCondition( + () => helper.sheetCount != singleSidedSheets, + "Wait for sheet count to update" + ); + let duplexShortEdgeSheets = helper.sheetCount; + is( + duplexShortEdgeSheets, + duplexLongEdgeSheets, + "Short-edge duplex printing halved the sheet count" + ); + }); +}); + +// Test that enabling duplex printing with multiple copies updates the +// sheet count accordingly. +add_task(async function testSheetCountDuplexWithCopies() { + // Use different scale values to exercise printing of different page counts + for (let scale of [2, 3, 4, 5]) { + await TestDuplexNumCopiesAtScale(scale); + } +}); + +// Enable duplex and numCopies=2 with the provided scale value and check +// that the sheet count is correct. +async function TestDuplexNumCopiesAtScale(scale) { + await PrintHelper.withTestPage(async helper => { + const mockPrinterName = "DuplexCapablePrinter"; + const printer = helper.addMockPrinter(mockPrinterName); + printer.supportsDuplex = Promise.resolve(true); + + await helper.startPrint(); + await helper.dispatchSettingsChange({ printerName: mockPrinterName }); + + // Set scale and shinkToFit to make the document + // bigger so that it spans multiple pages. + await helper.waitForPreview(() => + helper.dispatchSettingsChange({ + shrinkToFit: false, + scaling: scale, + duplex: Ci.nsIPrintSettings.kDuplexNone, + }) + ); + await BrowserTestUtils.waitForCondition( + () => helper.sheetCount != 1, + "Wait for sheet count to update" + ); + let singleSidedSheets = helper.sheetCount; + + // Chnage to two copies + await helper.waitForSettingsEvent(() => + helper.dispatchSettingsChange({ + numCopies: 2, + }) + ); + await BrowserTestUtils.waitForCondition( + () => helper.sheetCount != singleSidedSheets, + "Wait for sheet count to update" + ); + let twoCopiesSheetCount = helper.sheetCount; + + // Turn on duplex printing. + await helper.waitForSettingsEvent(() => + helper.dispatchSettingsChange({ + duplex: Ci.nsIPrintSettings.kDuplexFlipOnLongEdge, + }) + ); + await BrowserTestUtils.waitForCondition( + () => helper.sheetCount != twoCopiesSheetCount, + "Wait for sheet count to update" + ); + let duplexTwoCopiesSheetCount = helper.sheetCount; + + // Check sheet count accounts for duplex and numCopies. + is( + duplexTwoCopiesSheetCount, + Math.ceil(singleSidedSheets / 2) * 2, + "Duplex with 2 copies sheet count is correct" + ); + + await helper.closeDialog(); + }); +} + +add_task(async function testPagesPerSheetCount() { + await PrintHelper.withTestPage(async helper => { + let mockPrinterName = "A real printer!"; + helper.addMockPrinter(mockPrinterName); + + await SpecialPowers.pushPrefEnv({ + set: [["print_printer", mockPrinterName]], + }); + + await helper.startPrint(); + + await helper.waitForPreview(() => + helper.dispatchSettingsChange({ + shrinkToFit: false, + scaling: 2, + }) + ); + + await BrowserTestUtils.waitForCondition( + () => helper.sheetCount != 1, + "Wait for sheet count to update" + ); + let sheets = helper.sheetCount; + + ok(sheets > 1, "There are multiple pages"); + + await helper.openMoreSettings(); + let pagesPerSheet = helper.get("pages-per-sheet-picker"); + ok(BrowserTestUtils.is_visible(pagesPerSheet), "Pages per sheet is shown"); + pagesPerSheet.focus(); + + let popupOpen = BrowserTestUtils.waitForSelectPopupShown(window); + + EventUtils.sendKey("space", helper.win); + + await popupOpen; + + let numberMove = + [...pagesPerSheet.options].map(o => o.value).indexOf("16") - + pagesPerSheet.selectedIndex; + + for (let i = 0; i < numberMove; i++) { + EventUtils.sendKey("down", window); + if (document.activeElement.value == 16) { + break; + } + } + + await helper.waitForPreview(() => EventUtils.sendKey("return", window)); + + sheets = helper.sheetCount; + is(sheets, 1, "There's only one sheet now"); + + await helper.waitForSettingsEvent(() => + helper.dispatchSettingsChange({ numCopies: 5 }) + ); + + sheets = helper.sheetCount; + is(sheets, 5, "Copies are handled with pages per sheet correctly"); + + await helper.closeDialog(); + }); +}); + +add_task(async function testUpdateCopiesNoPreviewUpdate() { + const mockPrinterName = "Fake Printer"; + await PrintHelper.withTestPage(async helper => { + helper.addMockPrinter(mockPrinterName); + await helper.startPrint(); + + await helper.waitForSettingsEvent(() => + helper.dispatchSettingsChange({ numCopies: 5 }) + ); + + ok( + !helper.win.PrintEventHandler._updatePrintPreviewTask.isArmed, + "Preview Task is not armed" + ); + + await helper.waitForPreview(() => + helper.dispatchSettingsChange({ printerName: mockPrinterName }) + ); + + await helper.waitForSettingsEvent(() => + helper.dispatchSettingsChange({ numCopies: 2 }) + ); + ok( + !helper.win.PrintEventHandler._updatePrintPreviewTask.isArmed, + "Preview Task is not armed" + ); + + await helper.closeDialog(); + }); +}); 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 0000000000..1f27916cc6 --- /dev/null +++ b/toolkit/components/printing/tests/browser_system_dialog_subdialog_hidden.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testSystemDialogLinkState() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + + is( + helper.get("printer-picker").options.length, + 1, + "Only the Save to PDF printer is available" + ); + + let systemLink = helper.get("open-dialog-link"); + if (AppConstants.platform == "win") { + ok( + BrowserTestUtils.is_hidden(systemLink), + "Link is hidden on Windows with no extra printers" + ); + } else { + ok( + BrowserTestUtils.is_visible(systemLink), + "Link is visible on Linux/macOS" + ); + } + }); +}); + +add_task(async function testModalPrintDialog() { + await PrintHelper.withTestPage(async helper => { + helper.addMockPrinter("A printer"); + await SpecialPowers.pushPrefEnv({ + set: [["print_printer", "A printer"]], + }); + + await helper.startPrint(); + + helper.assertDialogOpen(); + + helper.assertSettingsMatch({ printerName: "A printer" }); + await helper.setupMockPrint(); + + helper.click(helper.get("open-dialog-link")); + + helper.assertDialogHidden(); + + await helper.withClosingFn(() => { + helper.resolveShowSystemDialog(); + helper.resolvePrint(); + }); + + helper.assertDialogClosed(); + }); +}); + +add_task(async function testModalPrintDialogCancelled() { + await PrintHelper.withTestPage(async helper => { + helper.addMockPrinter("A printer"); + await SpecialPowers.pushPrefEnv({ + set: [["print_printer", "A printer"]], + }); + + await helper.startPrint(); + + helper.assertDialogOpen(); + + helper.assertSettingsMatch({ printerName: "A printer" }); + await helper.setupMockPrint(); + + helper.click(helper.get("open-dialog-link")); + + helper.assertDialogHidden(); + + await helper.withClosingFn(() => { + helper.resolveShowSystemDialog(false); + }); + + helper.assertDialogClosed(); + }); +}); + +add_task(async function testPrintDoesNotWaitForPreview() { + await PrintHelper.withTestPage(async helper => { + helper.addMockPrinter("A printer"); + await SpecialPowers.pushPrefEnv({ + set: [["print_printer", "A printer"]], + }); + + await helper.startPrint({ waitFor: "loadComplete" }); + await helper.awaitAnimationFrame(); + + helper.mockFilePicker("print_does_not_wait_for_preview.pdf"); + await helper.setupMockPrint(); + + let systemPrint = helper.get("system-print"); + await BrowserTestUtils.waitForCondition( + () => BrowserTestUtils.is_visible(systemPrint), + "Wait for the system-print to be visible" + ); + + helper.click(helper.get("open-dialog-link")); + + helper.assertDialogHidden(); + await helper.withClosingFn(() => { + helper.resolveShowSystemDialog(); + helper.resolvePrint(); + }); + + helper.assertDialogClosed(); + }); +}); diff --git a/toolkit/components/printing/tests/browser_toolbar_button_toggle.js b/toolkit/components/printing/tests/browser_toolbar_button_toggle.js new file mode 100644 index 0000000000..0a644a1e57 --- /dev/null +++ b/toolkit/components/printing/tests/browser_toolbar_button_toggle.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function testToggleToolbarButton() { + await PrintHelper.withTestPage(async helper => { + CustomizableUI.addWidgetToArea("print-button", CustomizableUI.AREA_NAVBAR); + + helper.assertDialogClosed(); + + // get the button from the toolbar + let button = document.getElementById("print-button"); + // click the toolbar button + EventUtils.synthesizeMouseAtCenter(button, {}); + + await helper.waitForDialog(); + + // ensure dialog box is open + helper.assertDialogOpen(); + + // click toolbar button again to close dialog box + EventUtils.synthesizeMouseAtCenter(button, {}); + + helper.assertDialogClosed(); + }); +}); diff --git a/toolkit/components/printing/tests/browser_ui_labels.js b/toolkit/components/printing/tests/browser_ui_labels.js new file mode 100644 index 0000000000..a107bac4d1 --- /dev/null +++ b/toolkit/components/printing/tests/browser_ui_labels.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_FormFieldLabels() { + await PrintHelper.withTestPage(async helper => { + await helper.startPrint(); + + let fields = Array.from(helper.get("print").elements); + for (let field of fields) { + if (field.localName == "button") { + continue; + } + ok( + field.labels.length || + field.hasAttribute("aria-label") || + field.hasAttribute("aria-labelledby"), + `Field ${field.localName}#${field.id} should be labelled` + ); + } + }); +}); diff --git a/toolkit/components/printing/tests/browser_window_print.js b/toolkit/components/printing/tests/browser_window_print.js new file mode 100644 index 0000000000..7d73c0616c --- /dev/null +++ b/toolkit/components/printing/tests/browser_window_print.js @@ -0,0 +1,336 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +const TEST_PATH_SITE = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://test1.example.com" +); + +add_task(async function test_print_blocks() { + is( + document.querySelector(".printPreviewBrowser"), + null, + "There shouldn't be any print preview browser" + ); + + await BrowserTestUtils.withNewTab( + `${TEST_PATH}file_window_print.html`, + async function (browser) { + info( + "Waiting for the first window.print() to run and ensure we're showing the preview..." + ); + + let helper = new PrintHelper(browser); + await helper.waitForDialog(); + + { + let [before, afterFirst] = await SpecialPowers.spawn( + browser, + [], + () => { + return [ + !!content.document.getElementById("before-print"), + !!content.document.getElementById("after-first-print"), + ]; + } + ); + + ok(before, "Content before printing should be in the DOM"); + ok(!afterFirst, "Shouldn't have returned yet from window.print()"); + } + + gBrowser.getTabDialogBox(browser).abortAllDialogs(); + + await helper.waitForDialog(); + + { + let [before, afterFirst, afterSecond] = await SpecialPowers.spawn( + browser, + [], + () => { + return [ + !!content.document.getElementById("before-print"), + !!content.document.getElementById("after-first-print"), + !!content.document.getElementById("after-second-print"), + ]; + } + ); + + ok(before, "Content before printing should be in the DOM"); + ok(afterFirst, "Should be in the second print already"); + ok(afterSecond, "Shouldn't have blocked if we have mozPrintCallbacks"); + } + } + ); +}); + +add_task(async function test_print_delayed_during_load() { + is( + document.querySelector(".printPreviewBrowser"), + null, + "There shouldn't be any print preview browser" + ); + + await BrowserTestUtils.withNewTab( + `${TEST_PATH}file_window_print_delayed_during_load.html`, + async function (browser) { + info( + "Waiting for the first window.print() to run and ensure we're showing the preview..." + ); + + let helper = new PrintHelper(browser); + await helper.waitForDialog(); + + // The print dialog is open, should be open after onload. + { + let duringLoad = await SpecialPowers.spawn(browser, [], () => { + return !!content.document.getElementById("added-during-load"); + }); + ok(duringLoad, "Print should've been delayed"); + } + + gBrowser.getTabDialogBox(browser).abortAllDialogs(); + + is(typeof browser.isConnected, "boolean"); + await BrowserTestUtils.waitForCondition(() => !browser.isConnected); + ok(true, "Tab should've been closed after printing"); + } + ); +}); + +add_task(async function test_print_on_sandboxed_frame() { + is( + document.querySelector(".printPreviewBrowser"), + null, + "There shouldn't be any print preview browser" + ); + + await BrowserTestUtils.withNewTab( + `${TEST_PATH}file_window_print_sandboxed_iframe.html`, + async function (browser) { + info( + "Waiting for the first window.print() to run and ensure we're showing the preview..." + ); + + let helper = new PrintHelper(browser); + await helper.waitForDialog(); + + isnot( + document.querySelector(".printPreviewBrowser"), + null, + "Should open the print preview correctly" + ); + gBrowser.getTabDialogBox(browser).abortAllDialogs(); + } + ); +}); + +add_task(async function test_print_another_iframe_and_remove() { + is( + document.querySelector(".printPreviewBrowser"), + null, + "There shouldn't be any print preview browser" + ); + + await BrowserTestUtils.withNewTab( + `${TEST_PATH}file_window_print_another_iframe_and_remove.html`, + async function (browser) { + let firstFrame = browser.browsingContext.children[0]; + info("Clicking on the button in the first iframe"); + BrowserTestUtils.synthesizeMouse("button", 0, 0, {}, firstFrame); + + await new PrintHelper(browser).waitForDialog(); + + isnot( + document.querySelector(".printPreviewBrowser"), + null, + "Should open the print preview correctly" + ); + gBrowser.getTabDialogBox(browser).abortAllDialogs(); + } + ); +}); + +add_task(async function test_window_print_coop_site() { + for (const base of [TEST_PATH, TEST_PATH_SITE]) { + const url = `${base}file_coop_header2.html`; + is( + document.querySelector(".printPreviewBrowser"), + null, + "There shouldn't be any print preview browser" + ); + await BrowserTestUtils.withNewTab(url, async function (browser) { + await new PrintHelper(browser).waitForDialog(); + + ok(true, "Shouldn't crash"); + gBrowser.getTabDialogBox(browser).abortAllDialogs(); + }); + } +}); + +add_task(async function test_window_print_iframe_remove_on_afterprint() { + ok( + !document.querySelector(".printPreviewBrowser"), + "There shouldn't be any print preview browser" + ); + await BrowserTestUtils.withNewTab( + `${TEST_PATH}file_window_print_iframe_remove_on_afterprint.html`, + async function (browser) { + await new PrintHelper(browser).waitForDialog(); + let modalBefore = await SpecialPowers.spawn(browser, [], () => { + return content.windowUtils.isInModalState(); + }); + + ok(modalBefore, "The tab should be in modal state"); + + // Clear the dialog. + gBrowser.getTabDialogBox(browser).abortAllDialogs(); + + let [modalAfter, hasIframe] = await SpecialPowers.spawn( + browser, + [], + () => { + return [ + content.windowUtils.isInModalState(), + !!content.document.querySelector("iframe"), + ]; + } + ); + + ok(!modalAfter, "Should've cleared the modal state properly"); + ok(!hasIframe, "Iframe should've been removed from the DOM"); + } + ); +}); + +// FIXME(emilio): This test doesn't use window.print(), why is it on this file? +add_task(async function test_focused_browsing_context() { + await BrowserTestUtils.openNewForegroundTab( + gBrowser, + `${TEST_PATH}longerArticle.html` + ); + + let tabCount = gBrowser.tabs.length; + document.getElementById("cmd_newNavigatorTab").doCommand(); + await TestUtils.waitForCondition(() => gBrowser.tabs.length == tabCount + 1); + let newTabBrowser = gBrowser.selectedBrowser; + is(newTabBrowser.documentURI.spec, "about:newtab", "newtab is loaded"); + + let menuButton = document.getElementById("PanelUI-menu-button"); + menuButton.click(); + await BrowserTestUtils.waitForEvent(window.PanelUI.mainView, "ViewShown"); + + let printButtonID = "appMenu-print-button2"; + + document.getElementById(printButtonID).click(); + + let dialog = await TestUtils.waitForCondition( + () => + gBrowser + .getTabDialogBox(newTabBrowser) + .getTabDialogManager() + ._dialogs.find(dlg => dlg._box.querySelector(".printSettingsBrowser")), + "Wait for dialog" + ); + await dialog._dialogReady; + ok(dialog, "Dialog is available"); + await dialog._frame.contentWindow._initialized; + await dialog.close(); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_print_with_oop_iframe() { + is( + document.querySelector(".printPreviewBrowser"), + null, + "There shouldn't be any print preview browser" + ); + + await BrowserTestUtils.withNewTab( + `${TEST_PATH}file_window_print_oop_iframe.html`, + async function (browser) { + info( + "Waiting for window.print() to run and ensure we're showing the preview..." + ); + + let helper = new PrintHelper(browser); + await helper.waitForDialog(); + + isnot( + document.querySelector(".printPreviewBrowser"), + null, + "Should open the print preview correctly" + ); + gBrowser.getTabDialogBox(browser).abortAllDialogs(); + } + ); +}); + +add_task(async function test_base_uri_srcdoc() { + is( + document.querySelector(".printPreviewBrowser"), + null, + "There shouldn't be any print preview browser" + ); + + const PARENT_URI = `${TEST_PATH}file_window_print_srcdoc_base_uri.html`; + await BrowserTestUtils.withNewTab(PARENT_URI, async function (browser) { + info( + "Waiting for window.print() to run and ensure we're showing the preview..." + ); + + let helper = new PrintHelper(browser); + await helper.waitForDialog(); + + let previewBrowser = document.querySelector(".printPreviewBrowser"); + isnot(previewBrowser, null, "Should open the print preview correctly"); + + let baseURI = await SpecialPowers.spawn(previewBrowser, [], () => { + return content.document.baseURI; + }); + + is(baseURI, PARENT_URI, "srcdoc print document base uri should be right"); + + gBrowser.getTabDialogBox(browser).abortAllDialogs(); + }); +}); + +add_task(async function test_print_reentrant() { + is( + document.querySelector(".printPreviewBrowser"), + null, + "There shouldn't be any print preview browser" + ); + + const URI = `${TEST_PATH}file_window_print_reentrant.html`; + await BrowserTestUtils.withNewTab(URI, async function (browser) { + info( + "Waiting for window.print() to run and ensure we're showing the preview..." + ); + + let helper = new PrintHelper(browser); + await helper.waitForDialog(); + + let previewBrowser = document.querySelector(".printPreviewBrowser"); + isnot(previewBrowser, null, "Should open the print preview correctly"); + + gBrowser.getTabDialogBox(browser).abortAllDialogs(); + + let count = await SpecialPowers.spawn(browser, [], () => { + return parseInt( + content.document.getElementById("before-print-count").innerText + ); + }); + + is(count, 1, "Should've fired beforeprint just once"); + }); +}); diff --git a/toolkit/components/printing/tests/file_coop_header.html b/toolkit/components/printing/tests/file_coop_header.html new file mode 100644 index 0000000000..4a724d5179 --- /dev/null +++ b/toolkit/components/printing/tests/file_coop_header.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<html> + <body> + <p>Hello world</p> + </body> +</html> diff --git a/toolkit/components/printing/tests/file_coop_header.html^headers^ b/toolkit/components/printing/tests/file_coop_header.html^headers^ new file mode 100644 index 0000000000..46ad58d83b --- /dev/null +++ b/toolkit/components/printing/tests/file_coop_header.html^headers^ @@ -0,0 +1 @@ +Cross-Origin-Opener-Policy: same-origin diff --git a/toolkit/components/printing/tests/file_coop_header2.html b/toolkit/components/printing/tests/file_coop_header2.html new file mode 100644 index 0000000000..37e2ec5051 --- /dev/null +++ b/toolkit/components/printing/tests/file_coop_header2.html @@ -0,0 +1,6 @@ +<!doctype html> +<meta charset="utf-8"> +<script> + if (self.crossOriginIsolated) + print(); +</script> diff --git a/toolkit/components/printing/tests/file_coop_header2.html^headers^ b/toolkit/components/printing/tests/file_coop_header2.html^headers^ new file mode 100644 index 0000000000..6cabc380d9 --- /dev/null +++ b/toolkit/components/printing/tests/file_coop_header2.html^headers^ @@ -0,0 +1,3 @@ +Cross-Origin-Embedder-Policy: require-corp +Cross-Origin-Resource-Policy: same-origin +Cross-Origin-Opener-Policy: same-origin diff --git a/toolkit/components/printing/tests/file_first_landscape.html b/toolkit/components/printing/tests/file_first_landscape.html new file mode 100644 index 0000000000..997d713861 --- /dev/null +++ b/toolkit/components/printing/tests/file_first_landscape.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <style> + @page a { size: landscape; } + </style> + </head> + <body> + <div style="page: a">Test</div> + </body> +</html> diff --git a/toolkit/components/printing/tests/file_first_portrait.html b/toolkit/components/printing/tests/file_first_portrait.html new file mode 100644 index 0000000000..720a99fb60 --- /dev/null +++ b/toolkit/components/printing/tests/file_first_portrait.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <style> + @page a { size: portrait; } + </style> + </head> + <body> + <div style="page: a">Test</div> + </body> +</html> diff --git a/toolkit/components/printing/tests/file_landscape.html b/toolkit/components/printing/tests/file_landscape.html new file mode 100644 index 0000000000..8372783174 --- /dev/null +++ b/toolkit/components/printing/tests/file_landscape.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <style> + @page { size: landscape; } + </style> + </head> + <body> + <div>Test</div> + </body> +</html> diff --git a/toolkit/components/printing/tests/file_link_modulepreload.html b/toolkit/components/printing/tests/file_link_modulepreload.html new file mode 100644 index 0000000000..ef8bc23c11 --- /dev/null +++ b/toolkit/components/printing/tests/file_link_modulepreload.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<html> + <head> + <link rel="modulepreload" href="test.js"> + </head> + <body> + <script type="importmap"> + { + "imports": { + "test": "./test.js" + } + } + </script> + <p>Hello world</p> + </body> +</html> diff --git a/toolkit/components/printing/tests/file_multi_page_pdf.pdf b/toolkit/components/printing/tests/file_multi_page_pdf.pdf new file mode 100644 index 0000000000..4ed88753d8 --- /dev/null +++ b/toolkit/components/printing/tests/file_multi_page_pdf.pdf @@ -0,0 +1,27 @@ +%PDF-1.0 +%âãÏÓ +2 0 obj +<< /Type /Catalog /Pages 3 0 R>> +endobj +3 0 obj +<< /Type /Pages /Kids [ 4 0 R 5 0 R ] /Count 2 >> +endobj +4 0 obj +<< /Type /Page /MediaBox [0 0 3 3 ] /TrimBox [0 0 3 3 ] /Rotate 0 >> +endobj +5 0 obj +<< /Type /Page /MediaBox [0 0 3 3 ] /TrimBox [0 0 3 3 ] /Rotate 0 >> +endobj +xref
+0 6
+0000000001 65535 f
+0000000000 00000 f
+0000000015 00000 n
+0000000063 00000 n
+0000000128 00000 n
+0000000216 00000 n
+trailer
+<</Size 4 /ID [(¿ßúê}¸uàÅ7!ýè) (¿ßúê}¸uàÅ7!ýè) ] /Root 2 0 R >>
+startxref
+304
+%%EOF
diff --git a/toolkit/components/printing/tests/file_pdf.pdf b/toolkit/components/printing/tests/file_pdf.pdf new file mode 100644 index 0000000000..593558f9a4 --- /dev/null +++ b/toolkit/components/printing/tests/file_pdf.pdf @@ -0,0 +1,12 @@ +%PDF-1.0
+1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 3 3]>>endobj
+xref
+0 4
+0000000000 65535 f
+0000000010 00000 n
+0000000053 00000 n
+0000000102 00000 n
+trailer<</Size 4/Root 1 0 R>>
+startxref
+149
+%EOF
\ No newline at end of file diff --git a/toolkit/components/printing/tests/file_portrait.html b/toolkit/components/printing/tests/file_portrait.html new file mode 100644 index 0000000000..12414a1970 --- /dev/null +++ b/toolkit/components/printing/tests/file_portrait.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <style> + @page { size: portrait; } + </style> + </head> + <body> + <div>Test</div> + </body> +</html> diff --git a/toolkit/components/printing/tests/file_print.html b/toolkit/components/printing/tests/file_print.html new file mode 100644 index 0000000000..31deabc252 --- /dev/null +++ b/toolkit/components/printing/tests/file_print.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script> + window.print(); +</script> +<p id="printed">I should be printed</p> diff --git a/toolkit/components/printing/tests/file_print_pdf_on_frame_load.html b/toolkit/components/printing/tests/file_print_pdf_on_frame_load.html new file mode 100644 index 0000000000..ac76a598f7 --- /dev/null +++ b/toolkit/components/printing/tests/file_print_pdf_on_frame_load.html @@ -0,0 +1,3 @@ +<!doctype html> +<meta charset="utf-8"> +<iframe src="file_multi_page_pdf.pdf" onload="this.contentWindow.print()"></iframe> diff --git a/toolkit/components/printing/tests/file_window_print.html b/toolkit/components/printing/tests/file_window_print.html new file mode 100644 index 0000000000..7a05cf4511 --- /dev/null +++ b/toolkit/components/printing/tests/file_window_print.html @@ -0,0 +1,30 @@ +<!doctype html> +<meta charset="utf-8"> +<div id="before-print">Before print</div> +<canvas id="canvas" width="100" height="100"></canvas> +<script> + onload = function() { + // window.print() is special until after the load event is finished firing. + setTimeout(function() { + // This fires a timer which would trigger a navigation and prevent the + // test from completing if it happens during window.print(). + let meta = document.createElement("meta"); + meta.setAttribute("http-equiv", "refresh"); + meta.setAttribute("content", "0; url=/unlikely-to-be-found"); + document.head.appendChild(meta); + // This one should block until we're done printing, and block the + // navigation too. + window.print(); + meta.remove(); + document.body.insertAdjacentHTML('beforeend', `<div id="after-first-print">After first print</div>`); + + let canvas = document.getElementById("canvas"); + canvas.mozPrintCallback = function() {}; + + // This one shouldn't, because the print callbacks need to run. + window.print(); + + document.body.insertAdjacentHTML('beforeend', `<div id="after-second-print">After second print</div>`); + }, 0); + } +</script> diff --git a/toolkit/components/printing/tests/file_window_print_another_iframe_and_remove.html b/toolkit/components/printing/tests/file_window_print_another_iframe_and_remove.html new file mode 100644 index 0000000000..c4d176f39e --- /dev/null +++ b/toolkit/components/printing/tests/file_window_print_another_iframe_and_remove.html @@ -0,0 +1,10 @@ +<!doctype html> +<meta charset="utf-8"> +<script> +function handlePrint() { + frames[0].frameElement.style.display = "none"; + frames[1].print(); +} +</script> +<iframe srcdoc='<button onclick="parent.handlePrint()">Print the other iframe, then hide me</button>'></iframe> +<iframe id="iframe2" srcdoc="Print me"></iframe> diff --git a/toolkit/components/printing/tests/file_window_print_delayed_during_load.html b/toolkit/components/printing/tests/file_window_print_delayed_during_load.html new file mode 100644 index 0000000000..839005370d --- /dev/null +++ b/toolkit/components/printing/tests/file_window_print_delayed_during_load.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset="utf-8"> +<body> +<script> + onload = function() { + let div = document.createElement("div"); + div.id = "added-during-load"; + div.innerHTML = "I should be printed"; + document.body.appendChild(div); + }; + + window.print(); // This should be delayed until after load. + window.close(); // So should this. +</script> diff --git a/toolkit/components/printing/tests/file_window_print_iframe_remove_on_afterprint.html b/toolkit/components/printing/tests/file_window_print_iframe_remove_on_afterprint.html new file mode 100644 index 0000000000..471f17d89f --- /dev/null +++ b/toolkit/components/printing/tests/file_window_print_iframe_remove_on_afterprint.html @@ -0,0 +1,31 @@ +<!doctype html> +<meta charset="utf-8"> +<script> +function closePrint() { + document.documentElement.removeChild(this.__container__); +} + +function setPrint() { + this.contentWindow.__container__ = this; + this.contentWindow.onbeforeunload = closePrint; + this.contentWindow.onafterprint = closePrint; + this.contentWindow.print(); +} + +function printPage(content) { + var frame = document.createElement("iframe"); + frame.onload = setPrint; + frame.style.position = "fixed"; + frame.style.right = "0"; + frame.style.bottom = "0"; + frame.style.width = "0"; + frame.style.height = "0"; + frame.style.border = "0"; + frame.srcdoc = content; + document.documentElement.appendChild(frame); +} + +onload = function() { + printPage("Something"); +} +</script> diff --git a/toolkit/components/printing/tests/file_window_print_oop_iframe.html b/toolkit/components/printing/tests/file_window_print_oop_iframe.html new file mode 100644 index 0000000000..7f028381c3 --- /dev/null +++ b/toolkit/components/printing/tests/file_window_print_oop_iframe.html @@ -0,0 +1,7 @@ +<!doctype html> +<iframe src="https://example.com/document-builder.sjs?html=PASS"></iframe> +<script> + onload = function() { + window.print(); + }; +</script> diff --git a/toolkit/components/printing/tests/file_window_print_reentrant.html b/toolkit/components/printing/tests/file_window_print_reentrant.html new file mode 100644 index 0000000000..7eebd8c2fe --- /dev/null +++ b/toolkit/components/printing/tests/file_window_print_reentrant.html @@ -0,0 +1,12 @@ +<!doctype html> +<div id="before-print-count"></div> +<script> +let count = 0; +onload = function() { + window.addEventListener('beforeprint', (event) => { + document.getElementById("before-print-count").innerText = ++count; + print() + }); + print() +}; +</script> diff --git a/toolkit/components/printing/tests/file_window_print_sandboxed_iframe.html b/toolkit/components/printing/tests/file_window_print_sandboxed_iframe.html new file mode 100644 index 0000000000..8eb60c18b6 --- /dev/null +++ b/toolkit/components/printing/tests/file_window_print_sandboxed_iframe.html @@ -0,0 +1,8 @@ +<!doctype html> +<meta charset="utf-8"> +<iframe sandbox="allow-same-origin allow-scripts allow-modals" src="about:blank" width="0" height="0"></iframe> +<script> + onload = function() { + document.querySelector("iframe").contentWindow.print(); + }; +</script> diff --git a/toolkit/components/printing/tests/file_window_print_srcdoc_base_uri.html b/toolkit/components/printing/tests/file_window_print_srcdoc_base_uri.html new file mode 100644 index 0000000000..162f273206 --- /dev/null +++ b/toolkit/components/printing/tests/file_window_print_srcdoc_base_uri.html @@ -0,0 +1,7 @@ +<!doctype html> +<iframe srcdoc="Some content"></iframe> +<script> +window.onload = function() { + document.querySelector("iframe").contentWindow.print(); +}; +</script> diff --git a/toolkit/components/printing/tests/head.js b/toolkit/components/printing/tests/head.js new file mode 100644 index 0000000000..72033c93cb --- /dev/null +++ b/toolkit/components/printing/tests/head.js @@ -0,0 +1,576 @@ +const PRINT_DOCUMENT_URI = "chrome://global/content/print.html"; +const DEFAULT_PRINTER_NAME = "Mozilla Save to PDF"; +const { MockFilePicker } = SpecialPowers; + +let pickerMocked = false; + +class PrintHelper { + static async withTestPage(testFn, pagePathname, useHTTPS = false) { + let pageUrl = ""; + if (pagePathname) { + pageUrl = useHTTPS + ? this.getTestPageUrlHTTPS(pagePathname) + : this.getTestPageUrl(pagePathname); + } else { + pageUrl = useHTTPS + ? this.defaultTestPageUrlHTTPS + : this.defaultTestPageUrl; + } + info("withTestPage: " + pageUrl); + let isPdf = pageUrl.endsWith(".pdf"); + + if (isPdf) { + await SpecialPowers.pushPrefEnv({ + set: [["pdfjs.eventBusDispatchToDOM", true]], + }); + } + + let taskReturn = await BrowserTestUtils.withNewTab( + isPdf ? "about:blank" : pageUrl, + async function (browser) { + if (isPdf) { + let loaded = BrowserTestUtils.waitForContentEvent( + browser, + "documentloaded", + false, + null, + true + ); + BrowserTestUtils.loadURIString(browser, pageUrl); + await loaded; + } + await testFn(new PrintHelper(browser)); + } + ); + + await SpecialPowers.popPrefEnv(); + if (isPdf) { + await SpecialPowers.popPrefEnv(); + } + + // Reset all of the other printing prefs to their default. + this.resetPrintPrefs(); + return taskReturn; + } + + static async withTestPageHTTPS(testFn, pagePathname) { + return this.withTestPage(testFn, pagePathname, /* useHttps */ true); + } + + static resetPrintPrefs() { + for (let name of Services.prefs.getChildList("print.")) { + Services.prefs.clearUserPref(name); + } + Services.prefs.clearUserPref("print_printer"); + Services.prefs.clearUserPref("print.more-settings.open"); + } + + static getTestPageUrl(pathName) { + const testPath = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" + ); + return testPath + pathName; + } + + static getTestPageUrlHTTPS(pathName) { + const testPath = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ); + return testPath + pathName; + } + + static get defaultTestPageUrl() { + return this.getTestPageUrl("simplifyArticleSample.html"); + } + + static get defaultTestPageUrlHTTPS() { + return this.getTestPageUrlHTTPS("simplifyArticleSample.html"); + } + + static createMockPaper(paperProperties = {}) { + return Object.assign( + { + id: "regular", + name: "Regular Size", + width: 612, + height: 792, + unwriteableMargin: Promise.resolve( + paperProperties.unwriteableMargin || { + top: 0.1, + bottom: 0.1, + left: 0.1, + right: 0.1, + QueryInterface: ChromeUtils.generateQI([Ci.nsIPaperMargin]), + } + ), + QueryInterface: ChromeUtils.generateQI([Ci.nsIPaper]), + }, + paperProperties + ); + } + + // This is used only for the old print preview. For tests + // involving the newer UI, use waitForPreview instead. + static waitForOldPrintPreview(expectedBrowser) { + const { PrintingParent } = ChromeUtils.importESModule( + "resource://gre/actors/PrintingParent.sys.mjs" + ); + + return new Promise(resolve => { + PrintingParent.setTestListener(browser => { + if (browser == expectedBrowser) { + PrintingParent.setTestListener(null); + resolve(); + } + }); + }); + } + + constructor(sourceBrowser) { + this.sourceBrowser = sourceBrowser; + } + + async startPrint(condition = {}) { + this.sourceBrowser.ownerGlobal.document + .getElementById("cmd_print") + .doCommand(); + return this.waitForDialog(condition); + } + + async waitForDialog(condition = {}) { + let dialog = await TestUtils.waitForCondition( + () => this.dialog, + "Wait for dialog" + ); + await dialog._dialogReady; + + if (Object.keys(condition).length === 0) { + await this.win._initialized; + // Wait a frame so the rendering spinner is hidden. + await new Promise(resolve => requestAnimationFrame(resolve)); + } else if (condition.waitFor == "loadComplete") { + await BrowserTestUtils.waitForAttributeRemoval("loading", this.doc.body); + } + } + + beforeInit(initFn) { + // Run a function when the print.html document is created, + // but before its init is called from the domcontentloaded handler + TestUtils.topicObserved("document-element-inserted", doc => { + return ( + doc.nodePrincipal.isSystemPrincipal && + doc.contentType == "text/html" && + doc.URL.startsWith("chrome://global/content/print.html") + ); + }).then(([doc]) => { + doc.addEventListener("DOMContentLoaded", () => { + initFn(doc.ownerGlobal); + }); + }); + } + + async withClosingFn(closeFn) { + let { dialog } = this; + await closeFn(); + if (this.dialog) { + await TestUtils.waitForCondition( + () => !this.dialog, + "Wait for dialog to close" + ); + } + await dialog._closingPromise; + } + + resetSettings() { + this.win.PrintEventHandler.settings = + this.win.PrintEventHandler.defaultSettings; + this.win.PrintEventHandler.saveSettingsToPrefs( + this.win.PrintEventHandler.kInitSaveAll + ); + } + + async closeDialog() { + this.resetSettings(); + await this.withClosingFn(() => this.dialog.close()); + } + + assertDialogClosed() { + is(this._dialogs.length, 0, "There are no print dialogs"); + } + + assertDialogOpen() { + is(this._dialogs.length, 1, "There is one print dialog"); + 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"); + ok( + this.dialog._box.getBoundingClientRect().width > 0, + "The dialog should still have boxes" + ); + } + + async assertPrintToFile(file, testFn) { + ok(!file.exists(), "File does not exist before printing"); + await this.withClosingFn(testFn); + await TestUtils.waitForCondition( + () => file.exists() && file.fileSize > 0, + "Wait for target file to get created", + 50 + ); + ok(file.exists(), "Created target file"); + + await TestUtils.waitForCondition( + () => file.fileSize > 0, + "Wait for the print progress to run", + 50 + ); + + ok(file.fileSize > 0, "Target file not empty"); + } + + setupMockPrint() { + if (this.resolveShowSystemDialog) { + throw new Error("Print already mocked"); + } + + // Create some Promises that we can resolve from the test. + let showSystemDialogPromise = new Promise(resolve => { + this.resolveShowSystemDialog = result => { + if (result !== undefined) { + resolve(result); + } else { + resolve(true); + } + }; + }); + let printPromise = new Promise((resolve, reject) => { + this.resolvePrint = resolve; + this.rejectPrint = reject; + }); + + // Mock PrintEventHandler with our Promises. + this.win.PrintEventHandler._showPrintDialog = ( + window, + haveSelection, + settings + ) => { + this.systemDialogOpenedWithSelection = haveSelection; + return showSystemDialogPromise; + }; + this.win.PrintEventHandler._doPrint = (bc, settings) => { + this._printedSettings = settings; + return printPromise; + }; + } + + addMockPrinter(opts = {}) { + if (typeof opts == "string") { + opts = { name: opts }; + } + let { + name = "Mock Printer", + paperList, + printerInfoPromise = Promise.resolve(), + paperSizeUnit = Ci.nsIPrintSettings.kPaperSizeInches, + paperId, + } = opts; + let PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService( + Ci.nsIPrintSettingsService + ); + // Use the fallbackPaperList as the default for mock printers + if (!paperList) { + info("addMockPrinter, using the fallbackPaperList"); + paperList = Cc["@mozilla.org/gfx/printerlist;1"].createInstance( + Ci.nsIPrinterList + ).fallbackPaperList; + } + + let defaultSettings = PSSVC.createNewPrintSettings(); + defaultSettings.printerName = name; + defaultSettings.toFileName = ""; + defaultSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatNative; + defaultSettings.outputDestination = + Ci.nsIPrintSettings.kOutputDestinationPrinter; + defaultSettings.paperSizeUnit = paperSizeUnit; + if (paperId) { + defaultSettings.paperId = paperId; + } + + if ( + defaultSettings.paperId && + Array.from(paperList).find(p => p.id == defaultSettings.paperId) + ) { + info( + `addMockPrinter, using paperId: ${defaultSettings.paperId} from the paperList` + ); + } else if (paperList.length) { + defaultSettings.paperId = paperList[0].id; + info( + `addMockPrinter, corrected default paperId setting value: ${defaultSettings.paperId}` + ); + } + + let printer = { + name, + supportsColor: Promise.resolve(true), + supportsMonochrome: Promise.resolve(true), + printerInfo: printerInfoPromise.then(() => ({ + paperList, + defaultSettings, + QueryInterface: ChromeUtils.generateQI([Ci.nsIPrinterInfo]), + })), + QueryInterface: ChromeUtils.generateQI([Ci.nsIPrinter]), + }; + + if (!this._mockPrinters) { + this._mockPrinters = [printer]; + this.beforeInit(win => (win._mockPrinters = this._mockPrinters)); + } else { + this._mockPrinters.push(printer); + } + return printer; + } + + get _tabDialogBox() { + return this.sourceBrowser.ownerGlobal.gBrowser.getTabDialogBox( + this.sourceBrowser + ); + } + + get _tabDialogBoxManager() { + return this._tabDialogBox.getTabDialogManager(); + } + + get _dialogs() { + return this._tabDialogBox.getTabDialogManager()._dialogs; + } + + get dialog() { + return this._dialogs.find(dlg => + dlg._box.querySelector(".printSettingsBrowser") + ); + } + + get paginationElem() { + return this.dialog._box.querySelector(".printPreviewNavigation"); + } + + get paginationSheetIndicator() { + return this.paginationElem.shadowRoot.querySelector("#sheetIndicator"); + } + + get currentPrintPreviewBrowser() { + return this.win.PrintEventHandler.printPreviewEl.lastPreviewBrowser; + } + + get _printBrowser() { + return this.dialog._frame; + } + + get doc() { + return this._printBrowser.contentDocument; + } + + get win() { + return this._printBrowser.contentWindow; + } + + get(id) { + return this.doc.getElementById(id); + } + + get sheetCount() { + return this.doc.l10n.getAttributes(this.get("sheet-count")).args.sheetCount; + } + + get sourceURI() { + return this.win.PrintEventHandler.activeCurrentURI; + } + + async waitForReaderModeReady() { + if (gBrowser.selectedBrowser.isArticle) { + return; + } + await new Promise(resolve => { + let onReaderModeChange = { + receiveMessage(message) { + if ( + message.data && + message.data.isArticle !== undefined && + gBrowser.selectedBrowser.isArticle + ) { + AboutReaderParent.removeMessageListener( + "Reader:UpdateReaderButton", + onReaderModeChange + ); + resolve(); + } + }, + }; + AboutReaderParent.addMessageListener( + "Reader:UpdateReaderButton", + onReaderModeChange + ); + }); + } + + async waitForPreview(changeFn) { + changeFn(); + await BrowserTestUtils.waitForEvent(this.doc, "preview-updated"); + } + + async waitForSettingsEvent(changeFn) { + let changed = BrowserTestUtils.waitForEvent(this.doc, "print-settings"); + await changeFn?.(); + await BrowserTestUtils.waitForCondition( + () => !this.win.PrintEventHandler._delayedSettingsChangeTask.isArmed, + "Wait for all delayed tasks to execute" + ); + await changed; + } + + click(el, { scroll = true } = {}) { + if (scroll) { + el.scrollIntoView(); + } + ok(BrowserTestUtils.is_visible(el), "Element must be visible to click"); + EventUtils.synthesizeMouseAtCenter(el, {}, this.win); + } + + text(el, text) { + this.click(el); + el.value = ""; + EventUtils.sendString(text, this.win); + } + + async openMoreSettings(options) { + let details = this.get("more-settings"); + if (!details.open) { + this.click(details.firstElementChild, options); + } + await this.awaitAnimationFrame(); + } + + dispatchSettingsChange(settings) { + this.doc.dispatchEvent( + new CustomEvent("update-print-settings", { + detail: settings, + }) + ); + } + + get settings() { + return this.win.PrintEventHandler.settings; + } + + get viewSettings() { + return this.win.PrintEventHandler.viewSettings; + } + + _assertMatches(a, b, msg) { + if (Array.isArray(a)) { + is(a.length, b.length, msg); + for (let i = 0; i < a.length; ++i) { + this._assertMatches(a[i], b[i], msg); + } + return; + } + is(a, b, msg); + } + + assertSettingsMatch(expected) { + let { settings } = this; + for (let [setting, value] of Object.entries(expected)) { + this._assertMatches(settings[setting], value, `${setting} matches`); + } + } + + assertPrintedWithSettings(expected) { + ok(this._printedSettings, "Printed settings have been recorded"); + for (let [setting, value] of Object.entries(expected)) { + this._assertMatches( + this._printedSettings[setting], + value, + `${setting} matches printed setting` + ); + } + } + + get _lastPrintPreviewSettings() { + return this.win.PrintEventHandler._lastPrintPreviewSettings; + } + + assertPreviewedWithSettings(expected) { + let settings = this._lastPrintPreviewSettings; + ok(settings, "Last preview settings are available"); + for (let [setting, value] of Object.entries(expected)) { + this._assertMatches( + settings[setting], + value, + `${setting} matches previewed setting` + ); + } + } + + async assertSettingsChanged(from, to, changeFn) { + is( + Object.keys(from).length, + Object.keys(to).length, + "Got the same number of settings to check" + ); + ok( + Object.keys(from).every(s => s in to), + "Checking the same setting names" + ); + this.assertSettingsMatch(from); + await changeFn(); + this.assertSettingsMatch(to); + } + + async assertSettingsNotChanged(settings, changeFn) { + await this.assertSettingsChanged(settings, settings, changeFn); + } + + awaitAnimationFrame() { + return new Promise(resolve => this.win.requestAnimationFrame(resolve)); + } + + mockFilePickerCancel() { + if (!pickerMocked) { + pickerMocked = true; + MockFilePicker.init(window); + registerCleanupFunction(() => MockFilePicker.cleanup()); + } + MockFilePicker.returnValue = MockFilePicker.returnCancel; + } + + mockFilePicker(filename) { + if (!pickerMocked) { + pickerMocked = true; + MockFilePicker.init(window); + registerCleanupFunction(() => MockFilePicker.cleanup()); + } + MockFilePicker.returnValue = MockFilePicker.returnOK; + let file = Services.dirsvc.get("TmpD", Ci.nsIFile); + file.append(filename); + registerCleanupFunction(() => { + if (file.exists()) { + file.remove(false); + } + }); + MockFilePicker.setFiles([file]); + return file; + } +} + +function waitForPreviewVisible() { + return BrowserTestUtils.waitForCondition(function () { + let preview = document.querySelector(".printPreviewBrowser"); + return preview && BrowserTestUtils.is_visible(preview); + }); +} diff --git a/toolkit/components/printing/tests/longerArticle.html b/toolkit/components/printing/tests/longerArticle.html new file mode 100644 index 0000000000..4109e55959 --- /dev/null +++ b/toolkit/components/printing/tests/longerArticle.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <style> + @media print { + #page-2 { + page-break-before: always; + } + #page-3 { + page-break-before: always; + } + } + </style> + </head> + <body> + <h1 id="page-1">Page 1</h1> + <h1 id="page-2">Page 2</h1> + <h1 id="page-3">Page 3</h1> + </body> +</html> diff --git a/toolkit/components/printing/tests/page_size.html b/toolkit/components/printing/tests/page_size.html new file mode 100644 index 0000000000..d55d9e2cff --- /dev/null +++ b/toolkit/components/printing/tests/page_size.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>At-Page Size</title> +<style> + body { + margin: 0; + } + .rect { + /* An outlined rect that is 1inch smaller in each dimension, + to help visualize if the output page is the right size or not: */ + width: calc(4in - 1in - 4px); + height: calc(12in - 1in - 4px); + border: 2px solid black; + } + @page { + size: 4in 12in; + margin: 0.5in; + } +</style> +</head> +<body> + <div class="rect"> + <h3>Expectation:</h3> + If you print-preview using the "Save to PDF" page target, you should see an + 4in-by-12in-size sheet of paper by default, just 1 inch wider (0.5in margins) than the + black-outlined box. + </div> +</body> +</html> diff --git a/toolkit/components/printing/tests/page_size_a5.html b/toolkit/components/printing/tests/page_size_a5.html new file mode 100644 index 0000000000..0f3326d691 --- /dev/null +++ b/toolkit/components/printing/tests/page_size_a5.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>At-Page Size: A5</title> +<style> + body { + margin: 0; + } + .rect { + /* An outlined rect that is 1inch smaller in each dimension, + to help visualize if the output page is the right size or not: */ + width: calc(148mm - 1in - 4px); + height: calc(210mm - 1in - 4px); + border: 2px solid black; + } + @page { + size: A5; /* 5-7/8 x 8-1/4 in or 148 x 210 mm */ + margin: 0.5in; + } +</style> +</head> +<body> + <div class="rect"> + <h3>Expectation:</h3> + If you print-preview using the "Save to PDF" page target, you should see an + A5-size sheet of paper by default, just 1 inch wider (0.5in margins) than the black-outlined + box. + </div> +</body> +</html> diff --git a/toolkit/components/printing/tests/page_size_a5_landscape.html b/toolkit/components/printing/tests/page_size_a5_landscape.html new file mode 100644 index 0000000000..bfbb05c69d --- /dev/null +++ b/toolkit/components/printing/tests/page_size_a5_landscape.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>At-Page Size: A5 Landscape</title> +<style> + body { + margin: 0; + } + .rect { + /* An outlined rect that is 1inch smaller in each dimension, + to help visualize if the output page is the right size or not: */ + width: calc(210mm - 1in - 4px); + height: calc(148mm - 1in - 4px); + border: 2px solid black; + } + @page { + size: A5 landscape; /* 8-1/4 x 5-7/8 in or 210 x 148 mm */ + margin: 0.5in; + } +</style> +</head> +<body> + <div class="rect"> + <h3>Expectation:</h3> + If you print-preview using the "Save to PDF" page target, you should see an + A5-size sheet of paper in landscape, just 1 inch wider (0.5in margins) than the black-outlined + box. + </div> +</body> +</html> diff --git a/toolkit/components/printing/tests/page_size_zero.html b/toolkit/components/printing/tests/page_size_zero.html new file mode 100644 index 0000000000..054f9ae0e3 --- /dev/null +++ b/toolkit/components/printing/tests/page_size_zero.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>At-Page Size: Zero</title> +<style> + @page { + size: 0; + } +</style> +<h3>Expectation:</h3> +A size of 0 should not be applied. diff --git a/toolkit/components/printing/tests/page_size_zero_width.html b/toolkit/components/printing/tests/page_size_zero_width.html new file mode 100644 index 0000000000..404edfbf06 --- /dev/null +++ b/toolkit/components/printing/tests/page_size_zero_width.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>At-Page Size: Zero Width</title> +<style> + @page { + size: 0 4in; + } +</style> +<h3>Expectation:</h3> +A size including 0 should not be applied, and the orientation picker should be visible. diff --git a/toolkit/components/printing/tests/simplifyArticleSample.html b/toolkit/components/printing/tests/simplifyArticleSample.html new file mode 100644 index 0000000000..feff9626ab --- /dev/null +++ b/toolkit/components/printing/tests/simplifyArticleSample.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>Article title</title> +<meta name="description" content="This is the article description." /> +</head> +<body> +<header>Site header</header> +<div> +<h1>Article title</h1> +<h2 class="author">by Jane Doe</h2> +<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetu</p> +<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetu</p> +<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetu</p> +</body> +</html> diff --git a/toolkit/components/printing/tests/simplifyNonArticleSample.html b/toolkit/components/printing/tests/simplifyNonArticleSample.html new file mode 100644 index 0000000000..e216af3c1f --- /dev/null +++ b/toolkit/components/printing/tests/simplifyNonArticleSample.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head> +<title>Non article title</title> +<meta name="description" content="This is the non-article description." /> +</head> +<body> +</body> +</html> diff --git a/toolkit/components/printingui/ipc/PPrintingTypes.ipdlh b/toolkit/components/printingui/ipc/PPrintingTypes.ipdlh new file mode 100644 index 0000000000..73abd250ab --- /dev/null +++ b/toolkit/components/printingui/ipc/PPrintingTypes.ipdlh @@ -0,0 +1,100 @@ +/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PRemotePrintJob; + +namespace mozilla { +namespace embedding { + +struct CStringKeyValue { + nsCString key; + nsCString value; +}; + +struct PrintData { + nullable PRemotePrintJob remotePrintJob; + double edgeTop; + double edgeLeft; + double edgeBottom; + double edgeRight; + double marginTop; + double marginLeft; + double marginBottom; + double marginRight; + double unwriteableMarginTop; + double unwriteableMarginLeft; + double unwriteableMarginBottom; + double unwriteableMarginRight; + double scaling; + bool printBGColors; + bool printBGImages; + bool honorPageRuleMargins; + bool usePageRuleSizeAsPaperSize; + bool ignoreUnwriteableMargins; + bool showMarginGuides; + bool printSelectionOnly; + int32_t[] pageRanges; + nsString title; + nsString docURL; + nsString headerStrLeft; + nsString headerStrCenter; + nsString headerStrRight; + nsString footerStrLeft; + nsString footerStrCenter; + nsString footerStrRight; + + bool printSilent; + bool shrinkToFit; + + nsString paperId; + double paperWidth; + double paperHeight; + short paperSizeUnit; + bool printReversed; + bool printInColor; + int32_t orientation; + int32_t numCopies; + int32_t numPagesPerSheet; + + // We do not transfer the following to the child process, since the child + // process should not need this information, and we don't want to leak it if + // the child process is compromised. + //nsString printerName; + //nsString toFileName; + + // TODO: Do we really need to deal with these in child processes? + short outputDestination; + short outputFormat; + + int32_t printPageDelay; + int32_t resolution; + int32_t duplex; + bool isInitializedFromPrinter; + bool isInitializedFromPrefs; + + /* Windows-specific things */ + nsString driverName; + nsString deviceName; + uint8_t[] devModeData; + + /** + * GTK-specific things. Some of these might look like dupes of the + * information we're already passing, but the generalized settings that + * we hold in nsIPrintSettings don't map perfectly to GTK's GtkPrintSettings, + * so there are some nuances. GtkPrintSettings, for example, stores both an + * internal name for paper size, as well as the display name. + */ + CStringKeyValue[] GTKPrintSettings; + + /* + * macOS-specific things: + * What the Cocoa print system is supposed to do with the job. + */ + nsString disposition; + uint16_t destination; +}; + +} // namespace embedding +} // namespace mozilla diff --git a/toolkit/components/printingui/ipc/moz.build b/toolkit/components/printingui/ipc/moz.build new file mode 100644 index 0000000000..99ff60c0e6 --- /dev/null +++ b/toolkit/components/printingui/ipc/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +IPDL_SOURCES += [ + "PPrintingTypes.ipdlh", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/toolkit/components/printingui/moz.build b/toolkit/components/printingui/moz.build new file mode 100644 index 0000000000..5a91ca28dc --- /dev/null +++ b/toolkit/components/printingui/moz.build @@ -0,0 +1,12 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "Printing") + +DIRS += ["ipc"] + +FINAL_LIBRARY = "xul" |