mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 01:57:12 -06:00
* Tests: UI tests wait for background work by time, not frame count (#4186) A frame count is not a time: on a busy runner the worker had not finished the open, or the page text had not arrived, within 30 frames or 50 hover rounds. support::step_until (#[must_use]) steps frames until a condition holds or 30 s pass, sleeping briefly between frames, and says whether it held; each test asserts on that with its own message. - control_open_replies_pending_while_the_worker_opens_and_focuses_once_it_has and mode_tabs_keep_the_hand_over_page_text_scrolled_under_them wait with it, and so does control.rs call() for its reply. - support::settle waits for every pending render and fails if the pages are not rendered within 30 s. It replaces all the render-pending settle loops (40 to 3000 frames) and the local settle/wait_for_pages wrappers, except text_select's double-click wait, which is held to 8 frames on purpose to stay inside egui's multi-click window, and view.rs's loops that already wait by a deadline. - wait_until is built on step_until, keeping its diagnostic message. - tabbing_back_to_the_same_field_then_saving_keeps_the_committed_value already waited 30 s for its file. It failed because its "same-frame" case shared its scratch directory with typing_and_saving_in_the_same_frame_saves_the_last_keystrokes, which runs in the same test process (sinceee1ee704, #1045) and deleted the directory mid-save. forms' and pickers' scratch() now give every call a directory of its own. Fixes #4186 * M14: update ctutils to 0.4.5 for RUSTSEC-2026-0385 The dependency-advisory gate rejects ctutils 0.4.2 (non-constant-time mask generation and u128 comparisons, fixed in 0.4.4). It comes in through crypto-bigint for the signing crates. Only its version and checksum change in Cargo.lock. (cherry picked from commit1f8811761a)
268 lines
11 KiB
Rust
268 lines
11 KiB
Rust
//! Redact a PDF in the real shell (egui_kittest): the Redact tool, Redact pages, the apply
|
|
//! confirmation and Clear all.
|
|
|
|
use egui::Pos2;
|
|
use egui_kittest::Harness;
|
|
use egui_kittest::kittest::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.set_option("zoom", "150").unwrap();
|
|
app
|
|
});
|
|
crate::support::settle(&mut h);
|
|
h
|
|
}
|
|
|
|
fn at(h: &Harness<'static, PdfCraftApp>, x: f32, y: f32) -> Pos2 {
|
|
let r = h.state().views[0].page_screen_rect(0).expect("on screen");
|
|
let k = r.width() / 300.0;
|
|
r.min + egui::vec2(x * k, y * k)
|
|
}
|
|
|
|
fn marks(h: &Harness<'static, PdfCraftApp>) -> usize {
|
|
let s = h.state();
|
|
s.session.get(s.views[0].id).unwrap().redaction_marks()
|
|
}
|
|
|
|
#[test]
|
|
fn marking_applying_and_clearing() {
|
|
let mut h = harness();
|
|
assert!(h.state_mut().execute("redact.mark"));
|
|
h.run_steps(2);
|
|
h.get_by_label("Redact a PDF");
|
|
// Draw a box in an empty part of the page.
|
|
let (a, b) = (at(&h, 40.0, 280.0), at(&h, 200.0, 320.0));
|
|
h.hover_at(a);
|
|
h.run_steps(1);
|
|
h.drag_at(a);
|
|
h.run_steps(1);
|
|
for k in 1..=4 {
|
|
h.hover_at(a + (b - a) * (k as f32 / 4.0));
|
|
h.run_steps(1);
|
|
}
|
|
h.drop_at(b);
|
|
h.run_steps(4);
|
|
assert_eq!(marks(&h), 1);
|
|
// Redact pages ▸ current page.
|
|
h.state_mut().execute("redact.pages");
|
|
h.run_steps(2);
|
|
h.get_by_label("Mark Page Range");
|
|
h.get_by_label("OK").click();
|
|
h.run_steps(3);
|
|
assert_eq!(marks(&h), 2);
|
|
// Clear all, then undo it.
|
|
h.get_by_label("Clear all").click();
|
|
h.run_steps(3);
|
|
assert_eq!(marks(&h), 0);
|
|
h.state_mut().execute("edit.undo");
|
|
h.run_steps(3);
|
|
assert_eq!(marks(&h), 2);
|
|
// Redact all asks first; Apply removes the marks (and the page's fields under them).
|
|
h.get_by_label("Redact all").click();
|
|
h.run_steps(3);
|
|
assert_eq!(h.state().dialog, Some(Dialog::RedactApply));
|
|
h.get_by_label("Apply").click();
|
|
h.run_steps(4);
|
|
let s = h.state();
|
|
let doc = s.session.get(s.views[0].id).unwrap();
|
|
assert_eq!((doc.redaction_marks(), doc.can_undo()), (0, Some("Apply redactions")));
|
|
assert!(doc.form.is_empty(), "the whole page was redacted, fields included");
|
|
}
|
|
|
|
#[test]
|
|
fn redaction_codes_become_the_overlay_text() {
|
|
let mut h = harness();
|
|
assert!(h.state_mut().execute("redact.properties"));
|
|
h.run_steps(2);
|
|
h.get_by_label("Redaction Tool Properties");
|
|
h.get_by_label("Use overlay text").click();
|
|
h.run_steps(2);
|
|
h.state_mut().redact_prefs.overlay = "CONFIDENTIAL".into();
|
|
h.get_by_label("Redaction code:").click();
|
|
h.run_steps(2);
|
|
h.get_by_label("(b)(7)(C)").click();
|
|
h.run_steps(2);
|
|
h.get_by_label("(b)(6)").click();
|
|
h.run_steps(2);
|
|
h.get_by_label("OK").click();
|
|
h.run_steps(2);
|
|
let prefs = &h.state().redact_prefs;
|
|
assert_eq!(prefs.overlay_text(), "(b)(6), (b)(7)(C)", "codes in the set's order");
|
|
let pdfcraft_engine::Edit::AddAnnotation(a) = prefs.mark(0, vec![pdfcraft_engine::rect_quad([10.0, 10.0, 50.0, 30.0])], "T") else {
|
|
panic!("a mark adds an annotation")
|
|
};
|
|
assert!(matches!(a.shape, pdfcraft_engine::Shape::Redact { ref overlay, .. } if overlay == "(b)(6), (b)(7)(C)"));
|
|
h.state_mut().redact_prefs.use_code = false;
|
|
assert_eq!(h.state().redact_prefs.overlay_text(), "CONFIDENTIAL", "custom text is kept while codes are used");
|
|
}
|
|
|
|
/// The Redaction Tool Properties are the next run's too, like every other tool's defaults
|
|
/// (Acrobat's "Make Properties Default" without the box): fill, overlay text or codes, the
|
|
/// text's look, and the mark's outline colour and opacity all come back from the settings.
|
|
#[test]
|
|
fn redaction_properties_survive_a_restart() {
|
|
let mut h = harness();
|
|
{
|
|
let p = &mut h.state_mut().redact_prefs;
|
|
p.fill = None;
|
|
p.use_overlay = true;
|
|
p.use_code = true;
|
|
p.code_set = pdfcraft_engine::REDACTION_CODE_SETS[0];
|
|
p.codes = vec!["(b)(6)", "(b)(7)(C)"];
|
|
p.overlay = "CONFIDENTIAL".into();
|
|
p.look.font = pdfcraft_engine::OverlayFont::Courier;
|
|
p.look.size = 12.0;
|
|
p.look.color = [0.0, 0.0, 1.0];
|
|
p.look.align = 2;
|
|
p.look.repeat = true;
|
|
p.outline = [0.0, 0.5, 0.0];
|
|
p.hover_fill = [1.0, 1.0, 0.0];
|
|
p.opacity = 0.4;
|
|
}
|
|
// The defaults are what OK with "Make properties default" ticked leaves: go through the
|
|
// dialog.
|
|
assert!(h.state_mut().execute("redact.properties"));
|
|
h.run_steps(6);
|
|
h.get_by_label("Make properties default");
|
|
h.get_by_label("OK").click();
|
|
h.run_steps(2);
|
|
let json = h.state().persist();
|
|
let mut fresh = PdfCraftApp::new();
|
|
fresh.restore(&json);
|
|
let p = &fresh.redact_prefs;
|
|
assert_eq!(p.fill, None, "no colour is a saved choice, not a missing one");
|
|
assert_eq!(p.overlay_text(), "(b)(6), (b)(7)(C)");
|
|
assert_eq!(p.overlay, "CONFIDENTIAL");
|
|
assert_eq!(p.look.font, pdfcraft_engine::OverlayFont::Courier);
|
|
assert_eq!((p.look.size, p.look.color, p.look.align, p.look.repeat), (12.0, [0.0, 0.0, 1.0], 2, true));
|
|
assert_eq!((p.outline, p.hover_fill, p.opacity), ([0.0, 0.5, 0.0], [1.0, 1.0, 0.0], 0.4));
|
|
// The next mark takes the outline and opacity.
|
|
let pdfcraft_engine::Edit::AddAnnotation(a) = p.mark(0, vec![pdfcraft_engine::rect_quad([10.0, 10.0, 50.0, 30.0])], "T") else {
|
|
panic!("a mark adds an annotation")
|
|
};
|
|
assert_eq!((a.style.color, a.style.opacity, a.style.fill), ([0.0, 0.5, 0.0], 0.4, None));
|
|
// Settings are untrusted: a bad colour, an unknown code set and a code outside the set are
|
|
// dropped, the rest kept.
|
|
let mut odd = PdfCraftApp::new();
|
|
odd.restore(
|
|
r#"{"redact_prefs":{"outline":[2,0,0],"opacity":0.7,"code_set":"nope","codes":["(b)(6)","bogus"],"use_overlay":true,"use_code":true}}"#,
|
|
);
|
|
let p = &odd.redact_prefs;
|
|
assert_eq!((p.outline, p.opacity), ([0.89, 0.13, 0.13], 0.7));
|
|
assert_eq!(p.code_set.id, pdfcraft_engine::REDACTION_CODE_SETS[0].id);
|
|
assert_eq!(p.overlay_text(), "(b)(6)");
|
|
}
|
|
|
|
/// "Make properties default" off: OK keeps the values for this run only, the saved defaults
|
|
/// stay what they were; Cancel puts back what the dialog opened with.
|
|
#[test]
|
|
fn redaction_properties_for_this_run_only_and_cancel() {
|
|
let mut h = harness();
|
|
assert!(h.state_mut().execute("redact.properties"));
|
|
h.run_steps(6);
|
|
h.get_by_label("Redaction mark appearance");
|
|
h.get_by_label("Outline colour:");
|
|
h.get_by_label("Fill colour:");
|
|
h.get_by_label("Opacity:");
|
|
h.get_by_label("Make properties default").click();
|
|
h.run_steps(3);
|
|
assert!(!h.state().redact_prefs.remember);
|
|
h.state_mut().redact_prefs.outline = [0.0, 0.0, 1.0];
|
|
h.get_by_label("OK").click();
|
|
h.run_steps(2);
|
|
assert_eq!(h.state().redact_prefs.outline, [0.0, 0.0, 1.0], "used for this run");
|
|
assert_eq!(h.state().redact_defaults.outline, [0.89, 0.13, 0.13], "not made the default");
|
|
let mut fresh = PdfCraftApp::new();
|
|
fresh.restore(&h.state().persist());
|
|
assert_eq!(fresh.redact_prefs.outline, [0.89, 0.13, 0.13], "the next run starts from the saved defaults");
|
|
// Cancel discards the edits made while the dialog was open.
|
|
assert!(h.state_mut().execute("redact.properties"));
|
|
h.run_steps(2);
|
|
h.state_mut().redact_prefs.opacity = 0.2;
|
|
h.get_by_label("Cancel").click();
|
|
h.run_steps(2);
|
|
assert_eq!(h.state().redact_prefs.opacity, 1.0, "cancelled");
|
|
assert_eq!(h.state().redact_prefs.outline, [0.0, 0.0, 1.0], "the run's values stay");
|
|
}
|
|
|
|
#[test]
|
|
fn redacting_an_imported_word_list() {
|
|
let memo = pdfcraft_engine::Session::new().create_from_text("memo", "Call Ada today\nAda is private\nPublic line").unwrap().to_vec();
|
|
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("memo.pdf", None, memo.clone()).unwrap();
|
|
app
|
|
});
|
|
h.run_steps(4);
|
|
h.state_mut().execute("redact.search");
|
|
h.run_steps(2);
|
|
h.get_by_label("Multiple words or phrases");
|
|
let list = b" Ada \n\nprivate\nAda\nabsent\n".to_vec();
|
|
h.state_mut().use_files(pdfcraft_ui_egui::FilePurpose::RedactWords, vec![("names.txt".into(), list)]);
|
|
let d = &h.state().redact_search;
|
|
assert_eq!((d.mode, d.words.as_str()), (pdfcraft_ui_egui::RedactSearchMode::Words, "Ada\nprivate\nabsent"));
|
|
h.run_steps(2);
|
|
h.get_by_label("3 word(s) or phrase(s)");
|
|
h.get_by_label("Mark all").click();
|
|
h.run_steps(3);
|
|
assert_eq!((marks(&h), h.state().redact_search.found), (3, Some(3)));
|
|
let too_big = vec![b'a'; 2 << 20];
|
|
h.state_mut().use_files(pdfcraft_ui_egui::FilePurpose::RedactWords, vec![("huge.txt".into(), too_big)]);
|
|
assert_eq!(h.state().redact_search.words, "Ada\nprivate\nabsent", "an oversized list changes nothing");
|
|
}
|
|
|
|
#[test]
|
|
fn redaction_search_can_match_case_and_whole_words() {
|
|
let memo = pdfcraft_engine::Session::new().create_from_text("memo", "Ada ada Adaline\nAda beta betamax").unwrap().to_vec();
|
|
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("memo.pdf", None, memo.clone()).unwrap();
|
|
app
|
|
});
|
|
h.run_steps(4);
|
|
h.state_mut().execute("redact.search");
|
|
h.state_mut().redact_search.text = "Ada".into();
|
|
h.run_steps(2);
|
|
h.get_by_label("Whole words only").click();
|
|
h.get_by_label("Case-sensitive").click();
|
|
h.run_steps(2);
|
|
assert!(h.state().redact_search.whole_words && h.state().redact_search.case_sensitive);
|
|
h.get_by_label("Mark all").click();
|
|
h.run_steps(3);
|
|
assert_eq!((marks(&h), h.state().redact_search.found), (2, Some(2)));
|
|
}
|
|
|
|
#[test]
|
|
fn removing_hidden_information_and_sanitizing() {
|
|
let mut h = harness();
|
|
h.state_mut().execute("protect.remove_hidden");
|
|
h.run_steps(2);
|
|
h.get_by_label("Remove hidden information");
|
|
{
|
|
let d = &h.state().hidden_draft;
|
|
let fields = d.found.iter().find(|f| f.0 == pdfcraft_engine::Hidden::FormFields).unwrap();
|
|
assert_eq!((fields.1, fields.2), (5, true), "five fields, checked");
|
|
}
|
|
h.get_by_label("Remove").click();
|
|
h.run_steps(3);
|
|
{
|
|
let s = h.state();
|
|
let doc = s.session.get(s.views[0].id).unwrap();
|
|
assert!(doc.form.is_empty());
|
|
assert_eq!(doc.can_undo(), Some("Remove hidden information"));
|
|
}
|
|
h.state_mut().execute("redact.sanitize");
|
|
h.run_steps(2);
|
|
h.get_by_label("Sanitize").click();
|
|
h.run_steps(3);
|
|
let s = h.state();
|
|
assert_eq!(s.session.get(s.views[0].id).unwrap().can_undo(), Some("Sanitize document"));
|
|
}
|