pdfcraft/crates/ui-egui/tests/print.rs

314 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! The Print dialog in the real shell (egui_kittest): settings, preview sheets, Save as PDF.
use egui_kittest::Harness;
use egui_kittest::kittest::{NodeT, Queryable};
use pdfcraft_ui_egui::{Dialog, PdfCraftApp};
fn harness() -> Harness<'static, PdfCraftApp> {
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| {
let mut app = PdfCraftApp::new();
app.set_option("language", "en").unwrap();
app.open_bytes("form.pdf", None, include_bytes!("data/form.pdf").to_vec()).unwrap();
app
});
h.run_steps(4);
h
}
#[test]
fn print_dialog_lays_out_sheets_and_saves_a_pdf() {
let mut h = harness();
assert!(h.state_mut().execute("print.dialog"));
h.run_steps(3);
assert_eq!(h.state().dialog, Some(Dialog::Print));
h.get_by_label("Pages to Print".to_uppercase().as_str());
h.get_by_label("Sheet 1 of 1");
// Two copies of the one page, as a 2-up poster-free layout: Multiple.
h.get_by_label("Multiple").click();
h.run_steps(2);
h.get_by_label("Sheet 1 of 1");
// No printer on a test machine: Save as PDF.
let out = std::env::temp_dir().join(format!("pdfcraft-print-test-{}.pdf", std::process::id()));
h.state_mut().save_override = Some(out.to_string_lossy().into_owned());
h.state_mut().print_draft.printer = None;
h.run_steps(1);
h.get_by_label("Save as PDF").click();
h.run_steps(3);
assert_eq!(h.state().dialog, None);
let bytes = std::fs::read(&out).expect("saved");
let doc = pdfcraft_cos::Document::open(std::sync::Arc::new(bytes)).unwrap();
assert_eq!(pdfcraft_model::pages(&doc).len(), 1);
let _ = std::fs::remove_file(out);
}
/// Pages picked beforehand are what the dialog offers to print (Acrobat's "Selected pages").
#[test]
fn print_dialog_prints_the_selected_pages() {
use pdfcraft_ui_egui::PrintWhich;
let mut h = harness();
let bytes = h.state().session.create_blank(200.0, 300.0, 5).unwrap();
h.state_mut().open_bytes("five.pdf", None, bytes.as_ref().clone()).unwrap();
h.run_steps(3);
let tab = h.state().views.len() - 1;
// Nothing picked: the whole document, and no "Selected pages" choice.
assert!(h.state_mut().execute("print.dialog"));
h.run_steps(3);
assert_eq!(h.state().print_draft.which, PrintWhich::All);
assert!(h.query_by_label_contains("Selected pages").is_none());
h.get_by_label("Sheet 1 of 5");
h.state_mut().dialog = None;
h.run_steps(2);
// Pages 2 and 4 picked.
h.state_mut().views[tab].select_pages(&[1, 3]);
assert!(h.state_mut().execute("print.dialog"));
h.run_steps(3);
assert_eq!(h.state().print_draft.which, PrintWhich::Selected);
h.get_by_label("Selected pages (2)");
h.get_by_label("Sheet 1 of 2");
let out = std::env::temp_dir().join(format!("pdfcraft-print-selected-{}.pdf", std::process::id()));
h.state_mut().save_override = Some(out.to_string_lossy().into_owned());
h.state_mut().print_draft.printer = None;
h.run_steps(1);
h.get_by_label("Save as PDF").click();
h.run_steps(3);
assert_eq!(h.state().dialog, None);
let saved = std::fs::read(&out).expect("saved");
let doc = pdfcraft_cos::Document::open(std::sync::Arc::new(saved)).unwrap();
assert_eq!(pdfcraft_model::pages(&doc).len(), 2);
let _ = std::fs::remove_file(out);
// The selection gone, the remembered choice falls back to the whole document.
h.state_mut().views[tab].select_pages(&[]);
assert!(h.state_mut().execute("print.dialog"));
h.run_steps(3);
assert_eq!(h.state().print_draft.which, PrintWhich::All);
assert!(h.state().print_draft.selected.is_empty());
}
#[test]
fn invalid_ranges_are_explained_in_the_preview() {
let mut h = harness();
h.state_mut().execute("print.dialog");
h.run_steps(2);
h.state_mut().print_draft.which = pdfcraft_ui_egui::PrintWhich::Range;
h.state_mut().print_draft.range = "7".into();
h.run_steps(2);
h.get_by_label_contains("out of range");
}
#[test]
fn cut_stack_dialog_previews_saves_and_refuses_duplex() {
use pdfcraft_engine::print::{PageOrder, spool::Duplex};
let mut h = harness();
// Ten source pages, generated using the headless engine.
let bytes = h.state().session.create_blank(200.0, 300.0, 10).unwrap();
h.state_mut().open_bytes("numbered.pdf", None, bytes.as_ref().clone()).unwrap();
h.state_mut().execute("print.dialog");
h.run_steps(3);
h.get_by_label("Multiple").click();
h.run_steps(2);
// Pick the new order through the actual widgets.
h.get_by_value("Horizontal").click();
h.run_steps(2);
h.get_by_label("Cut and stack").click();
h.run_steps(2);
assert_eq!(h.state().print_draft.order, PageOrder::CutStack);
h.state_mut().print_draft.per_sheet = 4;
h.run_steps(2);
h.get_by_label("Sheet 1 of 3");
h.get_by_label_contains("Keep the sheets in order");
h.get_by_label("›").click();
h.run_steps(2);
h.get_by_label("Sheet 2 of 3");
h.state_mut().print_draft.duplex = Duplex::LongEdge;
h.run_steps(2);
h.get_by_label_contains("Cut and stack needs Two-sided: Off");
assert!(!h.state_mut().print_now());
h.state_mut().print_draft.duplex = Duplex::Off;
h.state_mut().print_draft.printer = None;
let out = std::env::temp_dir().join(format!("pdfcraft-cut-stack-ui-{}.pdf", std::process::id()));
h.state_mut().save_override = Some(out.to_string_lossy().into_owned());
h.run_steps(2);
h.get_by_label("Save as PDF").click();
h.run_steps(3);
assert_eq!(h.state().dialog, None);
let printed = pdfcraft_cos::Document::open(std::sync::Arc::new(std::fs::read(&out).unwrap())).unwrap();
assert_eq!(pdfcraft_model::pages(&printed).len(), 3);
let _ = std::fs::remove_file(out);
}
#[test]
fn a_failed_save_as_pdf_keeps_the_dialog_open() {
let mut h = harness();
assert!(h.state_mut().execute("print.dialog"));
h.run_steps(3);
h.state_mut().print_draft.printer = None;
// A folder that doesn't exist: the write fails.
let out = std::env::temp_dir().join(format!("pdfcraft-missing-{}", std::process::id())).join("x.pdf");
h.state_mut().save_override = Some(out.to_string_lossy().into_owned());
h.run_steps(2);
h.get_by_label("Save as PDF").click();
h.run_steps(3);
assert_eq!(h.state().dialog, Some(Dialog::Print), "the dialog stays open, with its settings");
let toast = h.state().toast.as_ref().map(|(m, _)| m.clone()).unwrap_or_default();
assert!(toast.contains("Could not save"), "the user is told why: {toast:?}");
}
/// Properties… shows the printer driver's own options (CUPS) and sends only those changed.
#[test]
fn print_dialog_shows_the_printer_driver_options() {
use pdfcraft_engine::print::spool;
let mut h = harness();
assert!(h.state_mut().execute("print.dialog"));
h.run_steps(3);
h.state_mut().print_draft.printer = None;
h.run_steps(1);
assert!(h.query_by(|n| n.label().as_deref() == Some("Properties…") && n.is_disabled()).is_some(), "Save as PDF has no driver");
let tray = spool::PrinterOption {
key: "InputSlot".into(),
label: "Paper tray".into(),
group: "Media".into(),
choices: vec![("AutoSelect".into(), "Auto tray select".into()), ("Tray2".into(), "Tray 2".into())],
default: "AutoSelect".into(),
};
{
let d = &mut h.state_mut().print_draft;
d.printer = Some("Test_Queue".into());
d.driver_options = Some(("Test_Queue".into(), vec![tray]));
d.driver_choices.insert("InputSlot".into(), "Tray2".into());
d.show_driver_options = true;
}
h.run_steps(2);
h.get_by_label("Printer properties");
h.get_by_label("Media");
h.get_by_label("Paper tray");
assert_eq!(h.state().print_draft.job("form.pdf").options, [("InputSlot".to_string(), "Tray2".to_string())]);
h.get_by_label("Reset to printer defaults").click();
h.run_steps(2);
assert!(h.state().print_draft.job("form.pdf").options.is_empty(), "defaults are not sent");
// Options read for another printer never go with this one's job.
h.state_mut().print_draft.driver_choices.insert("InputSlot".into(), "Tray2".into());
h.state_mut().print_draft.printer = Some("Other_Queue".into());
assert!(h.state().print_draft.job("form.pdf").options.is_empty());
}
/// The desktop app lists printers in the background (PowerShell on Windows takes a second or
/// more): the dialog opens at once, then preselects the default when the list arrives — unless
/// the user has already picked a printer.
#[test]
fn printers_listed_in_the_background_preselect_the_default_unless_one_was_picked() {
use pdfcraft_engine::print::spool::Printer;
use std::sync::{Arc, Mutex};
let mut h = harness();
// The source waits until the test lets it answer, so the dialog is seen while it looks.
let gate = Arc::new(Mutex::new(()));
let hold = gate.lock().unwrap();
let wait = gate.clone();
h.state_mut().printer_source = Some(Arc::new(move || {
let _open = wait.lock();
vec![
Printer { name: "Office".into(), default: false, two_sided: None },
Printer { name: "Front Desk".into(), default: true, two_sided: None },
]
}));
assert!(h.state_mut().execute("print.dialog"));
h.run_steps(2);
assert_eq!(h.state().dialog, Some(Dialog::Print), "the dialog doesn't wait for the printers");
assert!(h.state().print_draft.printers_loading);
assert_eq!(h.state().print_draft.printer, None);
drop(hold);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
while h.state().print_draft.printers_loading && std::time::Instant::now() < deadline {
h.run_steps(1);
std::thread::sleep(std::time::Duration::from_millis(5));
}
assert!(!h.state().print_draft.printers_loading);
assert_eq!(h.state().print_draft.printers.len(), 2);
assert_eq!(h.state().print_draft.printer.as_deref(), Some("Front Desk"));
// Opened again: last time's list shows at once; a choice made before the fresh list
// arrives is kept.
h.state_mut().dialog = None;
let hold = gate.lock().unwrap();
assert!(h.state_mut().execute("print.dialog"));
h.run_steps(2);
assert_eq!(h.state().print_draft.printers.len(), 2, "the last list shows while looking");
{
let d = &mut h.state_mut().print_draft;
d.printer = None;
d.printer_picked = true;
}
drop(hold);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
while h.state().print_draft.printers_loading && std::time::Instant::now() < deadline {
h.run_steps(1);
std::thread::sleep(std::time::Duration::from_millis(5));
}
assert!(!h.state().print_draft.printers_loading);
assert_eq!(h.state().print_draft.printer, None, "Save as PDF, as picked");
}
#[test]
fn architectural_page_can_be_matched_at_100_percent() {
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 1000.0)).build_eframe(|_cc| {
let mut app = PdfCraftApp::new();
app.set_option("language", "en").unwrap();
let bytes = app.session.create_blank(2592.0, 1728.0, 1).unwrap();
app.open_bytes("architectural.pdf", None, bytes.as_ref().clone()).unwrap();
app
});
h.run_steps(4);
assert!(h.state_mut().execute("print.dialog"));
h.run_steps(3);
h.get_by_label("PDF page: 36.00 × 24.00 in");
h.get_by_label("Match page at 100%").click();
h.run_steps(3);
h.get_by_value("ARCH D (24 x 36 in)");
h.get_by_label("PDF scale: 100.00%");
h.get_by_label("36.00 × 24.00 in");
h.get_by_label("Printer paper and scaling may differ. Check driver settings.");
let settings = h.state().print_draft.settings(1, &[]).unwrap();
assert_eq!(settings.paper, (1728.0, 2592.0));
assert_eq!(settings.layout, pdfcraft_engine::print::Layout::Size(pdfcraft_engine::print::SizeMode::Actual));
}
#[test]
fn custom_page_cannot_silently_match_a_standard_paper() {
let mut h = harness();
assert!(h.state_mut().execute("print.dialog"));
h.run_steps(3);
// The 300 x 400 pt fixture is not a standard sheet.
assert!(pdfcraft_engine::print::matching_paper((300.0, 400.0)).is_none());
let before = h.state().print_draft.clone();
assert!(h.get_by_label("Match page at 100%").accesskit_node().is_disabled());
h.get_by_label("Match page at 100%").click();
h.run_steps(3);
assert_eq!(h.state().print_draft.paper, before.paper);
assert_eq!(h.state().print_draft.size, before.size);
}
/// A driver Windows reports without two-sided printing keeps its own setting whatever Two-sided
/// says (some print both sides with it Off), so the dialog says where to change it.
#[test]
fn a_printer_without_two_sided_printing_points_to_its_properties() {
use pdfcraft_engine::print::spool::Printer;
let mut h = harness();
h.state_mut().printer_source = Some(std::sync::Arc::new(|| {
vec![
Printer { name: "Office PCL".into(), default: true, two_sided: Some(true) },
Printer { name: "Office PS".into(), default: false, two_sided: Some(false) },
]
}));
assert!(h.state_mut().execute("print.dialog"));
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
while h.state().print_draft.printers.is_empty() && std::time::Instant::now() < deadline {
h.run_steps(1);
std::thread::sleep(std::time::Duration::from_millis(5));
}
h.run_steps(2);
let note = "doesn't take two-sided settings";
assert!(h.query_by_label_contains(note).is_none(), "a driver that prints two-sided as asked");
h.state_mut().print_draft.printer = Some("Office PS".into());
h.run_steps(2);
h.get_by_label_contains(note);
}