mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-25 10:39:01 -06:00
* Fix main UI regressions in paging, fonts and test expectations * Wait for asynchronous dropped-document preparation in regression --------- Co-authored-by: Codex <codex@openai.com>
429 lines
20 KiB
Rust
429 lines
20 KiB
Rust
//! The Print dialog in the real shell (egui_kittest): settings, preview sheets, Save as PDF.
|
||
|
||
use egui::accesskit::Role;
|
||
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 current_print_page_saves_the_viewed_page_when_labels_restart() {
|
||
// Five distinct painted pages, labelled i, ii, iii, 1, 2.
|
||
let mut objects = vec![
|
||
"<< /Type /Catalog /Pages 2 0 R /PageLabels << /Nums [0 << /S /r >> 3 << /S /D /St 1 >>] >> >>".to_owned(),
|
||
"<< /Type /Pages /Kids [3 0 R 5 0 R 7 0 R 9 0 R 11 0 R] /Count 5 /MediaBox [0 0 200 300] >>".to_owned(),
|
||
];
|
||
for (i, color) in ["1 0 0", "0 1 0", "0 0 1", "1 1 0", "1 0 1"].iter().enumerate() {
|
||
objects.push(format!("<< /Type /Page /Parent 2 0 R /Resources << >> /Contents {} 0 R >>", 4 + i * 2));
|
||
let content = format!("{color} rg 0 0 200 300 re f");
|
||
objects.push(format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()));
|
||
}
|
||
let mut bytes = b"%PDF-1.7\n".to_vec();
|
||
let mut offsets = Vec::new();
|
||
for (i, object) in objects.iter().enumerate() {
|
||
offsets.push(bytes.len());
|
||
bytes.extend_from_slice(format!("{} 0 obj\n{object}\nendobj\n", i + 1).as_bytes());
|
||
}
|
||
let xref = bytes.len();
|
||
bytes.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).as_bytes());
|
||
for offset in offsets {
|
||
bytes.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
|
||
}
|
||
bytes.extend_from_slice(format!("trailer << /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objects.len() + 1).as_bytes());
|
||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_| {
|
||
let mut app = PdfCraftApp::new();
|
||
app.set_option("language", "en").unwrap();
|
||
app.open_bytes("labelled.pdf", None, bytes).unwrap();
|
||
app
|
||
});
|
||
h.run_steps(4);
|
||
let id = h.state().views[0].id;
|
||
let labels: Vec<_> = h.state().session.get(id).unwrap().info.pages.iter().map(|p| p.label.as_str()).collect();
|
||
assert_eq!(labels, ["i", "ii", "iii", "1", "2"]);
|
||
for (current, expected) in [[255, 0, 0], [0, 255, 0]].into_iter().enumerate() {
|
||
h.state_mut().set_option("page", &(current + 1).to_string()).unwrap();
|
||
h.run_steps(3);
|
||
assert!(h.state_mut().execute("print.dialog"));
|
||
h.run_steps(3);
|
||
h.get_by_label("Current page").click();
|
||
h.run_steps(3);
|
||
assert_eq!(h.state().print_draft.current_page, current);
|
||
h.get_by_label("Sheet 1 of 1");
|
||
let out = std::env::temp_dir().join(format!("pdfcraft-print-current-{}-{current}.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::sync::Arc::new(std::fs::read(&out).unwrap());
|
||
std::fs::remove_file(out).unwrap();
|
||
let doc = pdfcraft_cos::Document::open(saved.clone()).unwrap();
|
||
assert_eq!(pdfcraft_model::pages(&doc).len(), 1);
|
||
let mut renderer = pdfcraft_render::PageRenderer::new(saved, Default::default());
|
||
let shot = renderer.render(pdfcraft_render::RenderRequest { page: 0, scale: 1.0, ..Default::default() });
|
||
assert!(shot.error.is_none(), "{:?}", shot.error);
|
||
let center = ((shot.height / 2 * shot.width + shot.width / 2) * 4) as usize;
|
||
assert_eq!(&shot.rgba[center..center + 3], &expected, "saved the wrong physical page for {current}");
|
||
}
|
||
}
|
||
|
||
#[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");
|
||
// The selected sheet's media is always sent, independently of changed driver properties.
|
||
let media = vec![("media".to_string(), "Letter".to_string())];
|
||
assert_eq!(h.state().print_draft.job("form.pdf").options, [("InputSlot".to_string(), "Tray2".to_string()), media[0].clone()]);
|
||
h.get_by_label("Reset to printer defaults").click();
|
||
h.run_steps(2);
|
||
assert_eq!(h.state().print_draft.job("form.pdf").options, media, "driver defaults are not sent; selected paper is retained");
|
||
// 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_eq!(h.state().print_draft.job("form.pdf").options, media, "another queue must not inherit driver options");
|
||
}
|
||
|
||
/// 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);
|
||
// Nothing is known yet (#160): the dialog says it is looking, and Print waits instead of
|
||
// saving a PDF.
|
||
h.get_by_value("Looking for printers…");
|
||
assert!(h.get_by_role_and_label(Role::Button, "Print").accesskit_node().is_disabled(), "Print waits for the list");
|
||
assert!(h.query_by_label("Save as PDF").is_none(), "not offered as the choice while looking");
|
||
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);
|
||
}
|
||
|
||
/// Print as image (Acrobat's Advanced Print Setup) is a choice in the Print dialog's Advanced
|
||
/// section: the saved print-ready PDF carries the page as a picture, no text.
|
||
#[test]
|
||
fn print_as_image_is_an_advanced_choice_that_saves_pictures() {
|
||
let mut h = harness();
|
||
assert!(h.state_mut().execute("print.dialog"));
|
||
h.run_steps(3);
|
||
h.get_by_label("Advanced".to_uppercase().as_str()).click();
|
||
h.run_steps(2);
|
||
h.get_by_label("Print as image").click();
|
||
h.run_steps(2);
|
||
assert!(h.state().print_draft.as_image);
|
||
let out = std::env::temp_dir().join(format!("pdfcraft-print-image-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 pictures =
|
||
doc.scan_objects().filter(|(_, o)| matches!(&**o, pdfcraft_cos::Object::Stream(st) if st.dict.name(b"Subtype") == Some(b"Image"))).count();
|
||
assert_eq!(pictures, 1, "the page is one picture");
|
||
let _ = std::fs::remove_file(out);
|
||
}
|
||
|
||
/// The spooler answering with no printers at all says so (#160), and Save as PDF still works.
|
||
#[test]
|
||
fn an_empty_printer_list_says_none_were_found() {
|
||
use std::sync::Arc;
|
||
let mut h = harness();
|
||
h.state_mut().printer_source = Some(Arc::new(Vec::new));
|
||
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_loading && std::time::Instant::now() < deadline {
|
||
h.run_steps(1);
|
||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||
}
|
||
h.run_steps(2);
|
||
h.get_by_label_contains("No printers were found");
|
||
assert!(!h.get_by_role_and_label(Role::Button, "Save as PDF").accesskit_node().is_disabled());
|
||
}
|