//! Native print onboarding preserves engine PDF generation and submits the selected settings. use serde_json::json; use std::{cell::RefCell, rc::Rc}; use wordcraft_engine::Session; use wordcraft_ui_egui::printing::{Duplex, Feedback, Printer}; use wordcraft_ui_egui::{Services, WordApp}; #[test] fn control_print_discovery_reports_metadata_without_host_side_effects() { use std::cell::Cell; use wordcraft_ui_egui::control::{ControlRequest, Outcome, handle}; let discoveries = Rc::new(Cell::new(0)); let submissions = Rc::new(Cell::new(0)); let mut app = WordApp::new(Session::new(wordcraft_doc::Document::new()), Services::default()); let observed = discoveries.clone(); app.services.printers = Some(Box::new(move || { observed.set(observed.get() + 1); let (_, receiver) = std::sync::mpsc::channel(); receiver })); let observed = submissions.clone(); app.services.print_native = Some(Box::new(move |_, _, _| { observed.set(observed.get() + 1); let (_, receiver) = std::sync::mpsc::channel(); receiver })); let ctx = egui::Context::default(); let (request, _) = ControlRequest::new("engine.commands", json!({"search":"print"})); let Outcome::Done(reply) = handle(&mut app, &ctx, &request) else { panic!("metadata replies immediately") }; assert_eq!(reply["ok"], true); let commands = reply["result"].as_array().unwrap(); assert!(commands.iter().any(|command| command["id"] == "file.print"), "engine print commands remain visible"); let settings = commands.iter().find(|command| command["id"] == "ui.printSettings").unwrap(); assert_eq!(settings["enabled"], true, "settings/status inspection is available"); for field in ["printer", "copies", "duplex", "grayscale", "refresh", "status", "detail"] { assert!(settings["params"].as_str().unwrap().contains(field), "documented setting: {field}"); } for id in ["ui.printSubmit", "ui.print"] { let command = commands.iter().find(|command| command["id"] == id).unwrap(); assert_eq!(command["enabled"], false, "a printer must be selected before submission"); for field in ["range", "pages", "parity", "copies", "collate"] { assert!(command["params"].as_str().unwrap().contains(field), "{id}: {field}"); } } app.printing.printers.push(Printer { name: "Queue".into(), default: true }); app.run("ui.printSettings", json!({"printer":"Queue"})).unwrap(); let Outcome::Done(reply) = handle(&mut app, &ctx, &request) else { panic!("metadata replies immediately") }; for id in ["ui.printSubmit", "ui.print"] { let command = reply["result"].as_array().unwrap().iter().find(|command| command["id"] == id).unwrap(); assert_eq!(command["enabled"], true, "the selected queue enables {id}"); } assert_eq!(discoveries.get(), 0, "metadata never starts printer discovery"); assert_eq!(submissions.get(), 0, "metadata never submits"); assert!(app.dialog.is_none() && !app.ui.backstage, "metadata never opens a dialog or Print page"); assert!(!app.printing.busy() && !app.session.dirty); } #[test] fn native_print_uses_selected_settings_and_reports_spool_errors() { let mut app = WordApp::new(Session::new(wordcraft_doc::Document::new()), Services::default()); app.run("text.insert", json!({"text": "A document to print"})).unwrap(); let jobs = Rc::new(RefCell::new(Vec::new())); let observed = jobs.clone(); app.services.print_native = Some(Box::new(move |bytes, settings, collate| { observed.borrow_mut().push((bytes, settings, collate)); let (sender, receiver) = std::sync::mpsc::channel(); sender.send(Err("printer is offline".into())).unwrap(); receiver })); assert!(app.run("ui.print", json!({})).is_err()); assert!(jobs.borrow().is_empty(), "opening settings/exporting does not spool"); app.printing.printers.push(Printer { name: "Chosen queue".into(), default: false }); let configured = app.run("ui.printSettings", json!({"printer":"Chosen queue", "copies":3,"duplex":"shortEdge","grayscale":true})).unwrap(); assert_eq!(configured["ready"], true); assert_eq!(configured["printers"][0]["name"], "Chosen queue"); app.run("ui.print", json!({})).unwrap(); assert!(app.printing.busy()); assert!(!app.close_requested(), "a pending print job cannot be interrupted by normal close"); let jobs = jobs.borrow(); assert_eq!(jobs.len(), 1); assert!(jobs[0].0.starts_with(b"%PDF")); assert_eq!(jobs[0].1.printer, "Chosen queue"); assert_eq!(jobs[0].1.copies, 3); assert_eq!(jobs[0].1.duplex, Some(Duplex::ShortEdge)); assert!(jobs[0].1.grayscale); assert!(jobs[0].2, "copies are collated by default"); app.poll_printing(&egui::Context::default()); assert_eq!(app.printing.message, Some(Feedback::Failed("printer is offline".into()))); assert!(app.status_msg.as_ref().unwrap().0.contains("printer is offline")); } #[test] fn print_settings_are_visible_in_a_narrow_window() { use egui_kittest::kittest::{NodeT, Queryable}; let mut app = WordApp::new(Session::new(wordcraft_doc::Document::new()), Services::default()); app.run("text.insert", json!({"text": "A document to print"})).unwrap(); app.services.printers = Some(Box::new(|| { let (sender, receiver) = std::sync::mpsc::channel(); sender.send(vec![Printer { name: "Test queue".into(), default: true }]).unwrap(); receiver })); app.services.print_native = Some(Box::new(|_, _, _| { let (sender, receiver) = std::sync::mpsc::channel(); sender.send(Ok("queued".into())).unwrap(); receiver })); app.ui.backstage = true; app.ui.backstage_page = "print".into(); let mut harness = egui_kittest::Harness::builder().with_size(egui::vec2(800.0, 700.0)).build_ui_state( |ui, app: &mut WordApp| { app.logic(ui.ctx()); app.ui(ui); }, app, ); harness.run_steps(5); assert!(!harness.get_by_label("Refresh printers").accesskit_node().is_disabled()); assert!(!harness.get_by_label("Grayscale").accesskit_node().is_disabled()); harness.get_by_label("Grayscale").click(); harness.run_steps(2); assert!(harness.state().printing.settings.grayscale); assert!(harness.state().printing.ready()); } #[test] fn invalid_print_settings_are_rejected_without_partial_changes() { let mut app = WordApp::new(Session::new(wordcraft_doc::Document::new()), Services::default()); app.printing.printers.push(Printer { name: "Queue".into(), default: true }); let before = app.run("ui.printSettings", json!({})).unwrap(); for params in [ json!({"printer":"Missing"}), json!({"copies":0}), json!({"copies":1000}), json!({"copies":1.5}), json!({"copies":-1}), json!({"copies":"3"}), json!({"duplex":"maybe"}), json!({"grayscale":"yes"}), json!({"refresh":"yes"}), json!({"copies":3,"duplex":"invalid"}), json!([]), ] { assert!(app.run("ui.printSettings", params.clone()).is_err(), "{params}"); assert_eq!(app.run("ui.printSettings", json!({})).unwrap(), before, "no partial update: {params}"); } } #[test] fn repeated_fast_jobs_each_surface_an_identical_receipt() { let mut app = WordApp::new(Session::new(wordcraft_doc::Document::new()), Services::default()); app.printing.printers.push(Printer { name: "Queue".into(), default: true }); app.run("ui.printSettings", json!({"printer":"Queue"})).unwrap(); app.services.print_native = Some(Box::new(|_, _, _| { let (sender, receiver) = std::sync::mpsc::channel(); sender.send(Ok("same receipt".into())).unwrap(); receiver })); for _ in 0..2 { app.status_msg = None; app.run("ui.printSubmit", json!({})).unwrap(); // Completion may be consumed by the shared widget before the app's logic pass. app.printing.poll(&egui::Context::default()); app.poll_printing(&egui::Context::default()); assert!(app.status_msg.as_ref().unwrap().0.contains("same receipt")); app.status_msg = None; app.poll_printing(&egui::Context::default()); assert!(app.status_msg.is_none()); } } #[test] fn native_ranges_and_parity_do_not_duplicate_spooler_copies() { let mut app = WordApp::new(Session::new(wordcraft_doc::Document::new()), Services::default()); for page in 1..=3 { app.run("text.insert", json!({"text":format!("Distinct page {page}")})).unwrap(); if page < 3 { app.run("insert.pageBreak", json!({})).unwrap(); } } assert_eq!(app.session.export_layout().pages.len(), 3); app.printing.printers.push(Printer { name: "Queue".into(), default: true }); app.run("ui.printSettings", json!({"printer":"Queue","copies":3})).unwrap(); let jobs = Rc::new(RefCell::new(Vec::new())); let observed = jobs.clone(); app.services.print_native = Some(Box::new(move |bytes, settings, collate| { observed.borrow_mut().push((bytes, settings, collate)); let (sender, receiver) = std::sync::mpsc::channel(); sender.send(Ok("queued".into())).unwrap(); receiver })); for bad in [json!({"pages":"8"}), json!({"parity":"bad"}), json!({"copies":0}), json!({"collate":"yes"})] { assert!(app.run("ui.printSubmit", bad).is_err()); } assert!(jobs.borrow().is_empty(), "invalid settings do not send any job"); let params = json!({"pages":"1-3","parity":"odd","collate":false}); let (expected, plan) = app.session.print_pdf(¶ms).unwrap(); assert_eq!(plan.pages, vec![0, 2]); app.run("ui.printSubmit", params).unwrap(); let jobs = jobs.borrow(); assert_eq!(jobs[0].0, expected, "the PDF contains only the two selected pages, once each"); assert_eq!(jobs[0].1.copies, 3, "copies go to the spooler, not the PDF"); assert!(!jobs[0].2, "uncollated copies reach the spooler"); }