mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-02 22:41:47 -06:00
215 lines
7.7 KiB
Rust
215 lines
7.7 KiB
Rust
//! Headless UI tests (egui_kittest + AccessKit). They drive the real app shell without a window.
|
|
|
|
use egui_kittest::Harness;
|
|
use egui_kittest::kittest::Queryable;
|
|
use printcraft_ui_egui::PrintCraftApp;
|
|
|
|
/// A tiny PDF with two pages, two bookmarks and one sticky note.
|
|
const FIXTURE: &[u8] = b"%PDF-1.7
|
|
1 0 obj << /Type /Catalog /Pages 2 0 R /Outlines 6 0 R >> endobj
|
|
2 0 obj << /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >> endobj
|
|
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 300 400] /Annots [9 0 R] >> endobj
|
|
4 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 300 400] >> endobj
|
|
6 0 obj << /Type /Outlines /First 7 0 R /Last 8 0 R /Count 2 >> endobj
|
|
7 0 obj << /Title (Alpha section) /Parent 6 0 R /Next 8 0 R /Dest [3 0 R /Fit] >> endobj
|
|
8 0 obj << /Title (Beta section) /Parent 6 0 R /Prev 7 0 R /Dest [4 0 R /Fit] >> endobj
|
|
9 0 obj << /Type /Annot /Subtype /Text /Rect [10 370 30 390] /T (Tester) /Contents (Check the numbers) >> endobj
|
|
trailer << /Root 1 0 R >>
|
|
%%EOF";
|
|
|
|
fn harness(setup: impl FnOnce(&mut PrintCraftApp) + 'static) -> Harness<'static, PrintCraftApp> {
|
|
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| {
|
|
let mut app = PrintCraftApp::new();
|
|
setup(&mut app);
|
|
app
|
|
});
|
|
// Fonts install on frame 1 and apply on frame 2.
|
|
h.run_steps(4);
|
|
h
|
|
}
|
|
|
|
#[test]
|
|
fn home_shows_welcome_and_tools() {
|
|
let h = harness(|_| {});
|
|
h.get_by_label_contains("Welcome to PrintCraft");
|
|
assert!(h.query_all_by_label("Organize pages").count() >= 2, "tool list + home card");
|
|
h.get_by_label("Open file");
|
|
}
|
|
|
|
#[test]
|
|
fn every_catalog_tool_is_listed_after_view_more() {
|
|
let mut h = harness(|_| {});
|
|
h.get_by_label("View more").click();
|
|
h.run_steps(3);
|
|
for g in printcraft_engine::catalog::TOOL_GROUPS {
|
|
assert!(h.query_all_by_label(g.label).count() >= 1, "tool {} missing from All tools", g.label);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn opening_a_pdf_shows_comments_and_bookmarks() {
|
|
let mut h = harness(|app| app.open_bytes("fixture.pdf", None, FIXTURE.to_vec()).expect("fixture opens"));
|
|
// Documents with comments open on the Comments panel, like Acrobat.
|
|
h.get_by_label_contains("Check the numbers");
|
|
h.get_by_label("Bookmarks").click();
|
|
h.run_steps(3);
|
|
h.get_by_label("Alpha section");
|
|
h.get_by_label("Beta section");
|
|
}
|
|
|
|
#[test]
|
|
fn garbage_input_is_rejected_without_panicking() {
|
|
let mut app = PrintCraftApp::new();
|
|
assert!(app.open_bytes("junk.pdf", None, b"this is not a pdf".to_vec()).is_err());
|
|
assert!(app.open_bytes("empty.pdf", None, Vec::new()).is_err());
|
|
let mut truncated = FIXTURE.to_vec();
|
|
truncated.truncate(FIXTURE.len() / 3);
|
|
// A truncated file either repairs or fails cleanly; it must not panic.
|
|
let _ = app.open_bytes("truncated.pdf", None, truncated);
|
|
}
|
|
|
|
#[test]
|
|
fn theme_and_view_options_apply() {
|
|
let mut h = harness(|app| {
|
|
app.open_bytes("fixture.pdf", None, FIXTURE.to_vec()).expect("fixture opens");
|
|
app.set_option("theme", "dark").unwrap();
|
|
app.set_option("panel", "pages").unwrap();
|
|
app.set_option("page", "2").unwrap();
|
|
});
|
|
h.run_steps(3);
|
|
h.get_by_label("Page 2");
|
|
assert!(h.state_mut().set_option("panel", "nope").is_err());
|
|
}
|
|
|
|
/// Two pages of real text.
|
|
const TEXT_FIXTURE: &[u8] = b"%PDF-1.7
|
|
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
|
|
2 0 obj << /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >> endobj
|
|
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 300 200] /Contents 5 0 R /Resources << /Font << /F1 7 0 R >> >> >> endobj
|
|
4 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 300 200] /Contents 6 0 R /Resources << /Font << /F1 7 0 R >> >> >> endobj
|
|
5 0 obj << /Length 55 >> stream
|
|
BT /F1 14 Tf 20 150 Td (The quick brown fox) Tj ET
|
|
endstream endobj
|
|
6 0 obj << /Length 58 >> stream
|
|
BT /F1 14 Tf 20 150 Td (A second brown animal) Tj ET
|
|
endstream endobj
|
|
7 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> endobj
|
|
trailer << /Root 1 0 R >>
|
|
%%EOF";
|
|
|
|
fn settle(h: &mut Harness<'static, PrintCraftApp>) {
|
|
for _ in 0..200 {
|
|
h.run_steps(2);
|
|
if !h.state().render_pending() {
|
|
break;
|
|
}
|
|
std::thread::sleep(std::time::Duration::from_millis(5));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn find_counts_matches_across_pages() {
|
|
let mut h = harness(|app| {
|
|
app.open_bytes("text.pdf", None, TEXT_FIXTURE.to_vec()).expect("opens");
|
|
app.set_option("find", "brown").unwrap();
|
|
});
|
|
settle(&mut h);
|
|
h.get_by_label_contains("of 2");
|
|
let v = &h.state().views[0];
|
|
let f = v.find.as_ref().expect("find open");
|
|
assert_eq!(f.matches.iter().map(|(p, _)| *p).collect::<Vec<_>>(), vec![0, 1]);
|
|
}
|
|
|
|
#[test]
|
|
fn find_reports_no_matches() {
|
|
let mut h = harness(|app| {
|
|
app.open_bytes("text.pdf", None, TEXT_FIXTURE.to_vec()).expect("opens");
|
|
app.set_option("find", "zebra").unwrap();
|
|
});
|
|
settle(&mut h);
|
|
h.get_by_label("No matches");
|
|
}
|
|
|
|
#[test]
|
|
fn drag_selects_text_and_copy_returns_it() {
|
|
let mut h = harness(|app| {
|
|
app.open_bytes("text.pdf", None, TEXT_FIXTURE.to_vec()).expect("opens");
|
|
app.set_option("left", "closed").unwrap();
|
|
});
|
|
settle(&mut h);
|
|
// "The quick brown fox": glyph 4 = 'q', glyph 14 = 'n' of "brown".
|
|
let (a, b) = {
|
|
let v = &h.state().views[0];
|
|
(v.glyph_screen_pos(0, 4).expect("text layer loaded"), v.glyph_screen_pos(0, 14).expect("glyph"))
|
|
};
|
|
h.hover_at(a);
|
|
h.run_steps(1);
|
|
h.drag_at(a);
|
|
h.run_steps(1);
|
|
h.hover_at(a + (b - a) * 0.5);
|
|
h.run_steps(1);
|
|
h.hover_at(b);
|
|
h.run_steps(2);
|
|
h.drop_at(b);
|
|
h.run_steps(2);
|
|
assert_eq!(h.state().views[0].selected_text().as_deref(), Some("quick brown"));
|
|
}
|
|
|
|
#[test]
|
|
fn persistence_round_trips_and_tolerates_garbage() {
|
|
let mut a = PrintCraftApp::new();
|
|
a.theme = printcraft_ui_egui::theme::ThemeKind::Dark;
|
|
let json = a.persist();
|
|
let mut b = PrintCraftApp::new();
|
|
b.restore(&json);
|
|
assert_eq!(b.theme, printcraft_ui_egui::theme::ThemeKind::Dark);
|
|
b.restore("{not json");
|
|
b.restore("{\"recent\": 5, \"theme\": \"Purple\"}");
|
|
assert_eq!(b.theme, printcraft_ui_egui::theme::ThemeKind::Dark);
|
|
}
|
|
|
|
#[test]
|
|
fn zoom_keeps_the_point_under_the_cursor_still() {
|
|
let mut h = harness(|app| {
|
|
app.open_bytes("text.pdf", None, TEXT_FIXTURE.to_vec()).expect("opens");
|
|
app.set_option("left", "closed").unwrap();
|
|
app.set_option("panel", "none").unwrap();
|
|
});
|
|
settle(&mut h);
|
|
let before = h.state().views[0].glyph_screen_pos(0, 4).expect("glyph on screen");
|
|
let z = h.state().views[0].zoom;
|
|
h.state_mut().views[0].zoom_at(z * 2.0, before);
|
|
h.run_steps(4);
|
|
let after = h.state().views[0].glyph_screen_pos(0, 4).expect("glyph still on screen");
|
|
assert!((after - before).length() < 2.0, "moved from {before:?} to {after:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn selection_works_in_every_view_rotation() {
|
|
for deg in ["90", "180", "270"] {
|
|
let mut h = harness(move |app| {
|
|
app.open_bytes("text.pdf", None, TEXT_FIXTURE.to_vec()).expect("opens");
|
|
app.set_option("left", "closed").unwrap();
|
|
app.set_option("rotate", deg).unwrap();
|
|
// Keep the whole rotated page on screen.
|
|
app.set_option("zoom", "60").unwrap();
|
|
});
|
|
settle(&mut h);
|
|
let (a, b) = {
|
|
let v = &h.state().views[0];
|
|
(v.glyph_screen_pos(0, 4).expect("glyph"), v.glyph_screen_pos(0, 14).expect("glyph"))
|
|
};
|
|
h.hover_at(a);
|
|
h.run_steps(1);
|
|
h.drag_at(a);
|
|
h.run_steps(1);
|
|
h.hover_at(a + (b - a) * 0.5);
|
|
h.run_steps(1);
|
|
h.hover_at(b);
|
|
h.run_steps(2);
|
|
h.drop_at(b);
|
|
h.run_steps(2);
|
|
assert_eq!(h.state().views[0].selected_text().as_deref(), Some("quick brown"), "rotation {deg}");
|
|
}
|
|
}
|