From f2e33db1137fc26b8473ef74ec232a1434c6a836 Mon Sep 17 00:00:00 2001 From: skylar taylor-barrick Date: Fri, 23 Oct 2026 08:19:13 -0600 Subject: [PATCH] Regression tests for #246, #843 and #2028 (#4143) * #2028: test that Edit text & images is the first Modify page item Edit text & images was listed under Add content, though it changes what is already on the page. The test checks that the Edit a PDF catalogue lists it first under Modify page and not under Add content. With the item back under Add content it fails on the section assertion (left: Add content, right: Modify page). * #246: test that start-up and the panic hook never use eprintln eprintln! panics when standard error is a broken pipe, and a panic inside the panic hook aborts the process. The test scans the product code of the desktop main.rs (everything before the test module) and fails on any eprintln! outside a comment. With the old eprintln! calls restored in the panic hook and the log-file warning, it fails listing main.rs:188 and main.rs:227. * #843: test that text typed into an open paragraph counts as unsaved work An open paragraph with a draft that was not applied yet was not counted as unsaved work, so closing the tab or quitting skipped the save prompt. The test opens a paragraph, types into it, and checks that first_dirty() reports the tab (and not before typing, nor after the original text is typed back). Without the line_editor check in has_unsaved_work, first_dirty() stays None and the assertion fails (left: None, right: Some(0)). --- apps/pdfcraft/src/main.rs | 22 +++++++++++++++++ crates/engine/src/catalog.rs | 13 ++++++++++ crates/ui-egui/tests/it/editing.rs | 39 ++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+) diff --git a/apps/pdfcraft/src/main.rs b/apps/pdfcraft/src/main.rs index 1a36d64bfd..4206839424 100644 --- a/apps/pdfcraft/src/main.rs +++ b/apps/pdfcraft/src/main.rs @@ -1414,4 +1414,26 @@ mod tests { let class = entry.lines().find_map(|line| line.strip_prefix("StartupWMClass=")); assert_eq!(class, Some(super::APP_ID)); } + + // Regression test for #246: `eprintln!` panics when standard error is a broken pipe (a GUI + // launch on Windows), and a panic inside the panic hook aborts the process. The start-up + // path and the hook must write to standard error without it. + #[test] + fn start_up_and_the_panic_hook_never_use_eprintln() { + let source = include_str!("main.rs"); + // Everything before the top-level test module is product code. + let product = source.split("\n#[cfg(test)]\nmod tests {").next().unwrap_or(source); + assert!(product.len() < source.len(), "found the end of the product code"); + let offenders: Vec<_> = product + .lines() + .enumerate() + .filter(|(_, line)| !line.trim_start().starts_with("//") && line.contains("eprintln!")) + .map(|(n, line)| format!("main.rs:{}: {}", n + 1, line.trim())) + .collect(); + assert!( + offenders.is_empty(), + "`eprintln!` panics on a broken standard error pipe; use `log::warn!` or `write_fmt` on `stderr()` and ignore the error:\n{}", + offenders.join("\n") + ); + } } diff --git a/crates/engine/src/catalog.rs b/crates/engine/src/catalog.rs index c19e1e42c8..cfee2e3baa 100644 --- a/crates/engine/src/catalog.rs +++ b/crates/engine/src/catalog.rs @@ -596,6 +596,19 @@ mod tests { assert!(menus >= 3); } + // Regression test for #2028: Edit text & images sat under "Add content", though it changes + // what is already on the page; it belongs first under "Modify page". + #[test] + fn edit_text_and_images_is_the_first_item_under_modify_page() { + let edit = group("edit").expect("the Edit a PDF tool"); + let section_of = |command: &str| edit.sections.iter().find(|s| s.items.iter().any(|i| i.command == command)).map(|s| s.title); + assert_eq!(section_of("edit.edit_text"), Some("Modify page"), "Edit text & images is a Modify page tool"); + let modify = edit.sections.iter().find(|s| s.title == "Modify page").expect("a Modify page section"); + assert_eq!(modify.items.first().map(|i| i.command), Some("edit.edit_text"), "and the first one, so it is not below the fold"); + let add = edit.sections.iter().find(|s| s.title == "Add content").expect("an Add content section"); + assert!(add.items.iter().all(|i| i.command != "edit.edit_text"), "it adds nothing, so it is not listed under Add content"); + } + #[test] fn every_update_and_remove_command_is_reachable_from_a_tool_entry() { let reachable: HashSet<&str> = diff --git a/crates/ui-egui/tests/it/editing.rs b/crates/ui-egui/tests/it/editing.rs index 71ba9ccdac..73464d33e7 100644 --- a/crates/ui-egui/tests/it/editing.rs +++ b/crates/ui-egui/tests/it/editing.rs @@ -3090,6 +3090,45 @@ fn switching_straight_to_another_box_applies_the_open_paragraph() { assert_eq!(ed.text, "Bottom"); } +// Regression test for #811: text typed into an open paragraph was not counted as unsaved work, so +// closing the tab or quitting discarded it without the save prompt. +#[test] +fn text_typed_into_an_open_paragraph_counts_as_unsaved_work() { + let bytes = source_font_fixture_with_text("BT /F1 24 Tf 20 220 Td (Top) Tj ET"); + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { + let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); + app.open_bytes("box.pdf", None, bytes).expect("opens"); + app + }); + h.run_steps(4); + assert!(h.state_mut().execute("edit.edit_text")); + h.run_steps(2); + let r = h.state().views[0].page_screen_rect(0).expect("on screen"); + let k = r.width() / 300.0; + let block = { + let s = h.state(); + s.session.get(s.views[0].id).unwrap().text_blocks(0).into_iter().next().expect("one editable box") + }; + let center = egui::pos2( + r.left() + ((block.rect[0] + block.rect[2]) / 2.0) as f32 * k, + r.top() + (300.0 - (block.rect[1] + block.rect[3]) / 2.0) as f32 * k, + ); + click_at(&mut h, center); + assert_eq!(h.state().views[0].line_editor.as_ref().map(|e| e.text.as_str()), Some("Top"), "the paragraph is open for typing"); + assert_eq!(h.state().first_dirty(), None, "opening a paragraph changes nothing"); + + h.state_mut().views[0].line_editor.as_mut().unwrap().text = "Top, and a draft not applied yet".into(); + h.run_steps(1); + assert!(!h.state().session.get(h.state().views[0].id).unwrap().dirty, "the document itself is untouched until the draft is applied"); + assert_eq!(h.state().first_dirty(), Some(0), "the draft in the open paragraph is unsaved work"); + + // Typing the original text back leaves nothing to lose. + h.state_mut().views[0].line_editor.as_mut().unwrap().text = "Top".into(); + h.run_steps(1); + assert_eq!(h.state().first_dirty(), None, "a draft equal to the page text is not unsaved work"); +} + /// Dark pixels in `r`: glyph ink on a white page. fn ink(pixels: &image::RgbaImage, r: egui::Rect) -> usize { let (x0, y0, x1, y1) = (r.left().max(0.0) as u32, r.top().max(0.0) as u32, r.right() as u32, r.bottom() as u32);