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)).
This commit is contained in:
skylar taylor-barrick
2026-10-23 10:19:13 -04:00
committed by GitHub
parent 8f292c66ae
commit f2e33db113
3 changed files with 74 additions and 0 deletions
+22
View File
@@ -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")
);
}
}
+13
View File
@@ -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> =
+39
View File
@@ -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);