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)
This commit is contained in:
@@ -55,21 +55,10 @@ fn harness(rotate: u16) -> Harness<'static, PdfCraftApp> {
|
||||
app
|
||||
});
|
||||
h.run_steps(4);
|
||||
settle(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
h
|
||||
}
|
||||
|
||||
/// Run frames until rendering has finished and the layout holds still.
|
||||
fn settle(h: &mut Harness<'static, PdfCraftApp>) {
|
||||
for _ in 0..200 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
|
||||
/// Make a bookmark with ⌘B, as the Bookmarks button does, and say what it goes to.
|
||||
fn new_bookmark(h: &mut Harness<'static, PdfCraftApp>) -> (Option<usize>, DestView) {
|
||||
h.key_press_modifiers(Modifiers::COMMAND, Key::B);
|
||||
@@ -103,7 +92,7 @@ fn a_new_bookmark_goes_to_the_top_of_the_window_not_the_top_of_the_page() {
|
||||
h.state_mut().apply_edit(Edit::AddBookmark { parent: vec![], index: 0, title: "Mid".into(), page: 1, left: None, top: Some(250.0) });
|
||||
h.run_steps(3);
|
||||
h.get_by_label("Mid").click();
|
||||
settle(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
let (page, view) = new_bookmark(&mut h);
|
||||
assert_eq!(page, Some(1), "the page at the top of the window");
|
||||
let (left, top) = xyz(view);
|
||||
@@ -151,7 +140,7 @@ fn a_bookmark_made_on_a_turned_page_pins_the_axis_the_window_scrolls_along() {
|
||||
fn rotate_view(h: &mut Harness<'static, PdfCraftApp>, degrees: u16) {
|
||||
h.state_mut().views[0].rotation = degrees;
|
||||
h.run_steps(3);
|
||||
settle(h);
|
||||
crate::support::settle(h);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -216,12 +205,12 @@ fn a_bookmark_in_a_spread_of_unequal_pages_goes_to_the_page_crossing_the_top_of_
|
||||
});
|
||||
h.state_mut().views[0].layout = PageLayout::TwoUp;
|
||||
h.run_steps(4);
|
||||
settle(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
// 20 pt below page 2's top is at the window's top, where page 1 has not started.
|
||||
h.state_mut().apply_edit(Edit::AddBookmark { parent: vec![], index: 0, title: "Right".into(), page: 1, left: None, top: Some(380.0) });
|
||||
h.run_steps(3);
|
||||
h.get_by_label("Right").click();
|
||||
settle(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
let (page, view) = new_bookmark(&mut h);
|
||||
assert_eq!(page, Some(1), "page 2 crosses the top of the window; page 1 starts below it");
|
||||
close(xyz(view).1, 380.0, "top");
|
||||
|
||||
@@ -40,20 +40,10 @@ fn harness_with_bytes(bytes: Vec<u8>, setup: impl FnOnce(&mut PdfCraftApp) + 'st
|
||||
app
|
||||
});
|
||||
h.run_steps(4);
|
||||
settle(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
h
|
||||
}
|
||||
|
||||
fn settle(h: &mut Harness<'static, PdfCraftApp>) {
|
||||
for _ in 0..200 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
|
||||
/// A point on page 1 in PDF user space → screen (no rotation; MediaBox 300×200).
|
||||
fn at(h: &Harness<'static, PdfCraftApp>, x: f32, y: f32) -> Pos2 {
|
||||
let r = h.state().views[0].page_screen_rect(0).expect("page 1 on screen");
|
||||
@@ -146,7 +136,7 @@ fn sidebar_comment_centres_the_annotation_at_high_zoom() {
|
||||
&& let Ok(dir) = std::env::var("PDFCRAFT_SHOTS")
|
||||
{
|
||||
let _gpu = crate::gpu();
|
||||
settle(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
h.render().unwrap().save(format!("{dir}/comment-centred.png")).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -754,7 +744,7 @@ fn a_dragged_text_box_follows_a_rotated_view() {
|
||||
}
|
||||
h.state_mut().set_option("zoom", "150").unwrap();
|
||||
h.run_steps(3);
|
||||
settle(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
let screen = |h: &Harness<'static, PdfCraftApp>, x: f32, y: f32| {
|
||||
let r = h.state().views[0].page_screen_rect(0).expect("page 1 on screen");
|
||||
let (u, v) = (x / 300.0, (200.0 - y) / 200.0);
|
||||
@@ -1726,7 +1716,7 @@ fn attachment_harness(setup: impl FnOnce(&mut PdfCraftApp) + 'static) -> Harness
|
||||
app
|
||||
});
|
||||
h.run_steps(4);
|
||||
settle(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
h
|
||||
}
|
||||
|
||||
@@ -1915,7 +1905,7 @@ fn hovering_a_comment_shows_its_author_and_text() {
|
||||
app
|
||||
});
|
||||
h.run_steps(4);
|
||||
settle(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
assert!(h.query_by_label_contains("Check this figure").is_none(), "no popup before hovering");
|
||||
let p = at(&h, 100.0, 85.0);
|
||||
h.hover_at(p);
|
||||
@@ -1945,7 +1935,7 @@ fn preference_hides_author_only_highlight_hover_previews() {
|
||||
app
|
||||
});
|
||||
h.run_steps(4);
|
||||
settle(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
|
||||
let p = at(&h, 100.0, 85.0);
|
||||
// By default, author-only hover is shown.
|
||||
@@ -1992,7 +1982,7 @@ fn markup_hover_harness(pdf: Vec<u8>, view_rotation: u16) -> Harness<'static, Pd
|
||||
app
|
||||
});
|
||||
h.run_steps(4);
|
||||
settle(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
h
|
||||
}
|
||||
|
||||
|
||||
@@ -28,13 +28,7 @@ fn harness_bytes_with_step_dt(bytes: Vec<u8>, step_dt: f32) -> Harness<'static,
|
||||
app.set_option("zoom", "150").unwrap();
|
||||
app
|
||||
});
|
||||
for _ in 0..60 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
crate::support::settle(&mut h);
|
||||
h
|
||||
}
|
||||
|
||||
@@ -161,13 +155,7 @@ fn text_tool_deleting_a_draft_hides_the_old_added_glyphs_and_cancel_restores_the
|
||||
click(&mut h, point);
|
||||
h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A);
|
||||
h.key_press(egui::Key::Backspace);
|
||||
for _ in 0..100 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
crate::support::settle(&mut h);
|
||||
assert!(h.state().views[0].content.draft.as_ref().unwrap().text.is_empty());
|
||||
let shot = h.render().unwrap();
|
||||
for x in 105..140 {
|
||||
@@ -795,18 +783,6 @@ fn red(pixels: &image::RgbaImage, r: egui::Rect) -> usize {
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Steps until no render is pending.
|
||||
fn settle(h: &mut Harness<'static, PdfCraftApp>) {
|
||||
for _ in 0..200 {
|
||||
h.run_steps(1);
|
||||
if !h.state().render_pending() {
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
panic!("the page did not finish rendering");
|
||||
}
|
||||
|
||||
/// An added picture resized by a corner is drawn at its new size while dragged and after the
|
||||
/// release until the page is rendered again. It used to stay at its old size under the outline for
|
||||
/// the whole drag, and to show there again after the release.
|
||||
@@ -826,7 +802,7 @@ fn a_resized_added_picture_shows_only_at_its_new_size() {
|
||||
h.state_mut().add_image("red.png".into(), png);
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().views[0].content.selected, Some((0, 0)), "the picture is selected");
|
||||
settle(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
let rect = added(&h)[0].content.rect();
|
||||
let r = item_on_screen(&h, rect);
|
||||
// The lower right corner to the middle: half the size, the upper left corner kept.
|
||||
@@ -841,7 +817,7 @@ fn a_resized_added_picture_shows_only_at_its_new_size() {
|
||||
h.hover_at(grip + (to - grip) * (k as f32 / 4.0));
|
||||
h.run_steps(1);
|
||||
}
|
||||
settle(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
let pixels = h.render().unwrap();
|
||||
assert!(red(&pixels, kept) > 100, "while dragged, the picture is drawn at its new size");
|
||||
assert_eq!(red(&pixels, gone), 0, "while dragged, the picture shows at its old size");
|
||||
@@ -856,7 +832,7 @@ fn a_resized_added_picture_shows_only_at_its_new_size() {
|
||||
}
|
||||
assert_ne!(added(&h)[0].content.rect(), rect, "the resize is applied");
|
||||
h.state_mut().views[0].hold_rasters = false;
|
||||
settle(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
h.run_steps(2);
|
||||
let pixels = h.render().unwrap();
|
||||
assert!(red(&pixels, kept) > 100 && red(&pixels, gone) == 0, "the page raster has the picture at its new size");
|
||||
|
||||
@@ -73,13 +73,15 @@ fn harness_pages_at(pages: usize, width: f32, scale: f32) -> (Harness<'static, P
|
||||
/// Send a request and run frames until it is answered.
|
||||
fn call(h: &mut Harness<'static, PdfCraftApp>, c: &ControlClient, method: &str, params: Value) -> Reply {
|
||||
let rx = c.send(method, params);
|
||||
for _ in 0..30 {
|
||||
h.step();
|
||||
if let Ok(r) = rx.try_recv() {
|
||||
return r;
|
||||
}
|
||||
let mut reply = None;
|
||||
let answered = crate::support::step_until(h, |_| {
|
||||
reply = rx.try_recv().ok();
|
||||
reply.is_some()
|
||||
});
|
||||
match reply {
|
||||
Some(r) if answered => r,
|
||||
_ => panic!("{method}: no reply in 30 s"),
|
||||
}
|
||||
panic!("{method}: no reply after 30 frames");
|
||||
}
|
||||
|
||||
fn ok(h: &mut Harness<'static, PdfCraftApp>, c: &ControlClient, method: &str, params: Value) -> Value {
|
||||
@@ -1674,15 +1676,12 @@ fn control_open_replies_pending_while_the_worker_opens_and_focuses_once_it_has()
|
||||
// The worker completes the open on later frames; the tab shows, and only then does a
|
||||
// repeated open focus it instead of reporting another pending open.
|
||||
let mut documents = 0;
|
||||
for _ in 0..30 {
|
||||
h.run_steps(1);
|
||||
let state = ok(&mut h, &c, "ui.state", json!({}));
|
||||
let opened = crate::support::step_until(&mut h, |h| {
|
||||
let state = ok(h, &c, "ui.state", json!({}));
|
||||
documents = state["documents"].as_array().map(|d| d.len()).unwrap_or_default();
|
||||
if documents == 1 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(documents, 1, "the pending open completed into a tab");
|
||||
documents == 1
|
||||
});
|
||||
assert!(opened, "the pending open completed into a tab: {documents} tabs");
|
||||
|
||||
let again = ok(&mut h, &c, "ui.open", json!({ "path": a }));
|
||||
assert_eq!(again["focused"], true, "{again}");
|
||||
|
||||
@@ -2826,13 +2826,7 @@ fn deleting_a_draft_does_not_show_the_original_text_underneath() {
|
||||
h.drop_at(at);
|
||||
h.run_steps(3);
|
||||
h.state_mut().views[0].line_editor.as_mut().unwrap().text.clear();
|
||||
for _ in 0..100 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
crate::support::settle(&mut h);
|
||||
let shot = h.render().unwrap();
|
||||
if let Ok(path) = std::env::var("PDFCRAFT_TEXT_PREVIEW_SHOT") {
|
||||
shot.save(path).unwrap();
|
||||
@@ -2931,13 +2925,7 @@ fn opening_a_paragraph_leaves_the_page_as_it_is() {
|
||||
});
|
||||
h.run_steps(4);
|
||||
assert!(h.state_mut().execute("edit.edit_text"));
|
||||
for _ in 0..100 {
|
||||
h.run_steps(1);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
crate::support::settle(&mut h);
|
||||
let page = h.state().views[0].page_screen_rect(0).expect("on screen");
|
||||
let k = page.width() / 300.0;
|
||||
let block = h.state().session.get(h.state().views[0].id).unwrap().text_blocks(0)[0].rect;
|
||||
@@ -3008,13 +2996,7 @@ fn a_quick_click_into_another_paragraph_keeps_both_in_place() {
|
||||
};
|
||||
let (top, bottom) = (screen(&blocks[0]), screen(&blocks[1]));
|
||||
click_at(&mut h, top.center());
|
||||
for _ in 0..100 {
|
||||
h.run_steps(1);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
crate::support::settle(&mut h);
|
||||
assert_eq!(h.state().views[0].line_editor.as_ref().map(|e| e.text.as_str()), Some("Top"));
|
||||
let ink = |pixels: &image::RgbaImage, r: egui::Rect| {
|
||||
(r.top() as u32..r.bottom() as u32)
|
||||
@@ -4013,14 +3995,13 @@ fn save_paragraph_drag(h: &mut Harness<'static, PdfCraftApp>, name: &str, origin
|
||||
|
||||
fn paragraph_drag_shot(h: &mut Harness<'static, PdfCraftApp>, name: &str) {
|
||||
if let Ok(directory) = std::env::var("PDFCRAFT_PARAGRAPH_DRAG_SHOTS") {
|
||||
for _ in 0..60 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
assert!(!h.state().render_pending(), "the screenshot must show the current document generation");
|
||||
assert!(
|
||||
crate::support::step_until(h, |h| {
|
||||
h.run_steps(1);
|
||||
!h.state().render_pending()
|
||||
}),
|
||||
"the screenshot must show the current document generation"
|
||||
);
|
||||
let directory = std::path::Path::new(&directory);
|
||||
std::fs::create_dir_all(directory).unwrap();
|
||||
h.render().unwrap().save(directory.join(name)).unwrap();
|
||||
@@ -5170,14 +5151,13 @@ fn image_pixels_follow_the_frame_while_a_corner_is_dragged() {
|
||||
h.run_steps(4);
|
||||
// The dragged pixels are the page raster's: wait for it, as a slow runner may not have it yet
|
||||
// (the drag then drew no picture at all).
|
||||
for _ in 0..400 {
|
||||
h.run_steps(1);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
assert!(!h.state().render_pending(), "the page raster arrives");
|
||||
assert!(
|
||||
crate::support::step_until(&mut h, |h| {
|
||||
h.run_steps(1);
|
||||
!h.state().render_pending()
|
||||
}),
|
||||
"the page raster arrives"
|
||||
);
|
||||
let page = h.state().views[0].page_screen_rect(0).unwrap();
|
||||
let at = |x: f32, y: f32| egui::pos2(page.left() + x / 200.0 * page.width(), page.top() + (300.0 - y) / 300.0 * page.height());
|
||||
let p = at(95.0, 195.0);
|
||||
@@ -5876,13 +5856,7 @@ fn a_paragraph_running_off_its_page_edits_without_covering_the_next_page() {
|
||||
h.drag_at(p);
|
||||
h.run_steps(1);
|
||||
h.drop_at(p);
|
||||
for _ in 0..60 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
crate::support::settle(&mut h);
|
||||
assert!(
|
||||
h.state().views[0].line_editor.as_ref().is_some_and(|e| e.page == 0 && e.text.contains("line 50")),
|
||||
"the paragraph's off-page lines are in it"
|
||||
@@ -5971,15 +5945,13 @@ fn mode_tabs_keep_the_hand_over_page_text_scrolled_under_them() {
|
||||
h.run_steps(4);
|
||||
let top = h.state().views[0].page_screen_rect(0).expect("on screen").top();
|
||||
let mut cursor = egui::CursorIcon::Default;
|
||||
for _ in 0..50 {
|
||||
let text_cursor = crate::support::step_until(&mut h, |h| {
|
||||
h.hover_at(text.center() + egui::vec2(0.0, top));
|
||||
h.run_steps(2);
|
||||
cursor = h.output().platform_output.cursor_icon;
|
||||
if cursor == egui::CursorIcon::Text {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(cursor, egui::CursorIcon::Text);
|
||||
cursor == egui::CursorIcon::Text
|
||||
});
|
||||
assert!(text_cursor, "the I-beam over page text, not {cursor:?}");
|
||||
assert_eq!(mode_tab_cursors(&mut h, text), HAND_ON_EVERY_TAB);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,13 +14,7 @@ fn harness() -> Harness<'static, PdfCraftApp> {
|
||||
app.set_option("zoom", "150").unwrap();
|
||||
app
|
||||
});
|
||||
for _ in 0..60 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
crate::support::settle(&mut h);
|
||||
h
|
||||
}
|
||||
|
||||
@@ -500,8 +494,13 @@ fn wait_for_file(h: &mut Harness<'static, PdfCraftApp>, path: &std::path::Path)
|
||||
h.run_steps(1);
|
||||
}
|
||||
|
||||
/// A fresh directory of its own for each call. The tests share one process and run in parallel,
|
||||
/// so a name alone is not enough: two tests that both used "same-frame" deleted each other's
|
||||
/// directory mid-save, and the save then never wrote its file (#4186).
|
||||
fn scratch(name: &str) -> std::path::PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!("pdfcraft-forms-{name}-{}", std::process::id()));
|
||||
static NEXT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
|
||||
let n = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let dir = std::env::temp_dir().join(format!("pdfcraft-forms-{name}-{}-{n}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
@@ -1029,13 +1028,7 @@ fn calculated_field_and_script_side_effect_on_another_page_re_render() {
|
||||
event: "calculate".into(),
|
||||
script: Some("event.value = 'from_' + getField('name').value;".into()),
|
||||
}));
|
||||
for _ in 0..60 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
crate::support::settle(&mut h);
|
||||
click_field(&mut h, "name", 0);
|
||||
h.event(egui::Event::Text("hello".into()));
|
||||
h.run_steps(2);
|
||||
@@ -1043,13 +1036,7 @@ fn calculated_field_and_script_side_effect_on_another_page_re_render() {
|
||||
h.run_steps(2);
|
||||
assert_eq!(value(&h, "page2_field"), ["from_hello"]);
|
||||
// Wait for all re-renders to complete.
|
||||
for _ in 0..60 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
crate::support::settle(&mut h);
|
||||
assert_eq!(value(&h, "name"), ["hello"]);
|
||||
assert_eq!(value(&h, "page2_field"), ["from_hello"]);
|
||||
}
|
||||
@@ -1065,13 +1052,7 @@ fn overlay_clears_after_fresh_tiles_zoom_then_edit_and_rejected_edit() {
|
||||
h.key_press(egui::Key::Tab);
|
||||
h.run_steps(2);
|
||||
assert_eq!(value(&h, "name"), ["GoodVal"]);
|
||||
for _ in 0..60 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
crate::support::settle(&mut h);
|
||||
assert!(!h.state().views[0].forms.unbaked.contains(&("name".to_string(), 0)));
|
||||
|
||||
// 2. Rejected edit: typing rejected by validate script leaves unbaked clean.
|
||||
@@ -1091,13 +1072,7 @@ fn overlay_clears_after_fresh_tiles_zoom_then_edit_and_rejected_edit() {
|
||||
|
||||
// 3. Zoom then edit: zoom to another scale, let tiles settle, then edit.
|
||||
h.state_mut().views[0].zoom_step(true);
|
||||
for _ in 0..60 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
crate::support::settle(&mut h);
|
||||
click_field(&mut h, "name", 0);
|
||||
h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A);
|
||||
h.event(egui::Event::Text("ZoomedVal".into()));
|
||||
@@ -1105,13 +1080,7 @@ fn overlay_clears_after_fresh_tiles_zoom_then_edit_and_rejected_edit() {
|
||||
h.key_press(egui::Key::Tab);
|
||||
h.run_steps(2);
|
||||
assert_eq!(value(&h, "name"), ["ZoomedVal"]);
|
||||
for _ in 0..60 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
crate::support::settle(&mut h);
|
||||
assert!(!h.state().views[0].forms.unbaked.contains(&("name".to_string(), 0)));
|
||||
}
|
||||
|
||||
|
||||
@@ -73,13 +73,7 @@ fn harness_document(clipping: bool, background: bool) -> Harness<'static, PdfCra
|
||||
assert!(app.execute("edit.edit_text"));
|
||||
app
|
||||
});
|
||||
for _ in 0..60 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
crate::support::settle(&mut h);
|
||||
h
|
||||
}
|
||||
|
||||
|
||||
@@ -133,13 +133,7 @@ fn an_ocr_word_opens_for_editing_and_stays_open() {
|
||||
assert!(app.execute("edit.edit_text"));
|
||||
app
|
||||
});
|
||||
for _ in 0..60 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
crate::support::settle(&mut h);
|
||||
let (r, k) = {
|
||||
let app = h.state();
|
||||
let d = app.session.get(app.views[0].id).unwrap();
|
||||
|
||||
@@ -115,9 +115,12 @@ fn a_pick_for_a_document_that_is_no_longer_active_is_dropped() {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// A scratch folder for one test.
|
||||
/// A scratch folder for one test, of its own for each call: the tests share one process and run
|
||||
/// in parallel, so two that picked the same name would delete each other's folder (#4186).
|
||||
fn scratch(name: &str) -> std::path::PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!("pdfcraft-pickers-{name}-{}", std::process::id()));
|
||||
static NEXT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
|
||||
let n = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let dir = std::env::temp_dir().join(format!("pdfcraft-pickers-{name}-{}-{n}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
|
||||
@@ -17,13 +17,7 @@ fn harness() -> Harness<'static, PdfCraftApp> {
|
||||
app.set_option("zoom", "150").unwrap();
|
||||
app
|
||||
});
|
||||
for _ in 0..60 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
crate::support::settle(&mut h);
|
||||
h
|
||||
}
|
||||
|
||||
|
||||
@@ -14,13 +14,7 @@ fn harness() -> Harness<'static, PdfCraftApp> {
|
||||
app.set_option("zoom", "150").unwrap();
|
||||
app
|
||||
});
|
||||
for _ in 0..60 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
crate::support::settle(&mut h);
|
||||
h
|
||||
}
|
||||
|
||||
|
||||
@@ -2,21 +2,43 @@
|
||||
use egui_kittest::Harness;
|
||||
use pdfcraft_ui_egui::PdfCraftApp;
|
||||
|
||||
pub fn wait_until(h: &mut Harness<'static, PdfCraftApp>, what: &str, ready: impl Fn(&PdfCraftApp) -> bool) {
|
||||
/// Step frames until `ready` holds or 30 seconds pass, and say whether it held. For background work
|
||||
/// (a worker-thread open, page text arriving) that a test then asserts on itself: a frame count is
|
||||
/// not a time, and a busy runner may not have finished the work within one. Sleeps briefly between
|
||||
/// frames so the worker threads get the CPU.
|
||||
#[must_use]
|
||||
pub fn step_until(h: &mut Harness<'static, PdfCraftApp>, mut ready: impl FnMut(&mut Harness<'static, PdfCraftApp>) -> bool) -> bool {
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
|
||||
loop {
|
||||
h.step();
|
||||
if ready(h.state()) {
|
||||
return;
|
||||
if ready(h) {
|
||||
return true;
|
||||
}
|
||||
if std::time::Instant::now() >= deadline {
|
||||
return false;
|
||||
}
|
||||
assert!(
|
||||
std::time::Instant::now() < deadline,
|
||||
"timed out waiting for {what}: views={}, active={:?}, password={:?}, toast={:?}",
|
||||
h.state().views.len(),
|
||||
h.state().active,
|
||||
h.state().password_prompt.as_ref().map(|p| (&p.name, &p.error)),
|
||||
h.state().toast
|
||||
);
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
|
||||
/// Step frames until nothing is waiting to be rendered, and fail the test if that takes over 30
|
||||
/// seconds: what a test checks next is on the rendered page.
|
||||
pub fn settle(h: &mut Harness<'static, PdfCraftApp>) {
|
||||
let settled = step_until(h, |h| {
|
||||
h.run_steps(1);
|
||||
!h.state().render_pending()
|
||||
});
|
||||
assert!(settled, "pages rendered");
|
||||
}
|
||||
|
||||
pub fn wait_until(h: &mut Harness<'static, PdfCraftApp>, what: &str, ready: impl Fn(&PdfCraftApp) -> bool) {
|
||||
let held = step_until(h, |h| ready(h.state()));
|
||||
assert!(
|
||||
held,
|
||||
"timed out waiting for {what}: views={}, active={:?}, password={:?}, toast={:?}",
|
||||
h.state().views.len(),
|
||||
h.state().active,
|
||||
h.state().password_prompt.as_ref().map(|p| (&p.name, &p.error)),
|
||||
h.state().toast
|
||||
);
|
||||
}
|
||||
|
||||
@@ -451,7 +451,7 @@ fn overflowing_tabs_can_be_reached_by_scrolling() {
|
||||
assert!(h.state().active.is_some_and(|i| i < 19));
|
||||
if let Ok(dir) = std::env::var("PDFCRAFT_TAB_SHOTS") {
|
||||
let _gpu = crate::gpu();
|
||||
settle(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
h.render().unwrap().save(format!("{dir}/overflow-tabs.png")).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -565,7 +565,7 @@ fn narrow_tab_strips_accept_vertical_wheel_scrolling() {
|
||||
assert!(h.get_by_label_contains("Display theme:").rect().right() <= width);
|
||||
if let Ok(dir) = std::env::var("PDFCRAFT_TAB_SHOTS") {
|
||||
let _gpu = crate::gpu();
|
||||
settle(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
h.render().unwrap().save(format!("{dir}/tabs-{width}.png")).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -612,23 +612,13 @@ endstream endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF";
|
||||
|
||||
fn settle(h: &mut Harness<'static, PdfCraftApp>) {
|
||||
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);
|
||||
crate::support::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");
|
||||
@@ -641,7 +631,7 @@ fn find_reports_no_matches() {
|
||||
app.open_bytes("text.pdf", None, TEXT_FIXTURE.to_vec()).expect("opens");
|
||||
app.set_option("find", "zebra").unwrap();
|
||||
});
|
||||
settle(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
h.get_by_label("No matches");
|
||||
}
|
||||
|
||||
@@ -651,7 +641,7 @@ fn drag_selects_text_and_copy_returns_it() {
|
||||
app.open_bytes("text.pdf", None, TEXT_FIXTURE.to_vec()).expect("opens");
|
||||
app.set_option("left", "closed").unwrap();
|
||||
});
|
||||
settle(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
// "The quick brown fox": glyph 4 = 'q', glyph 14 = 'n' of "brown".
|
||||
let (a, b) = {
|
||||
let v = &h.state().views[0];
|
||||
@@ -738,7 +728,7 @@ fn zoom_keeps_the_point_under_the_cursor_still() {
|
||||
app.set_option("left", "closed").unwrap();
|
||||
app.set_option("panel", "none").unwrap();
|
||||
});
|
||||
settle(&mut h);
|
||||
crate::support::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);
|
||||
@@ -754,7 +744,7 @@ fn trackpad_pinch_zooms_the_pdf_under_the_pointer() {
|
||||
app.set_option("left", "closed").unwrap();
|
||||
app.set_option("panel", "none").unwrap();
|
||||
});
|
||||
settle(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
let before = h.state().views[0].glyph_screen_pos(0, 4).expect("glyph on screen");
|
||||
let zoom = h.state().views[0].zoom;
|
||||
h.hover_at(before);
|
||||
@@ -781,7 +771,7 @@ fn selection_works_in_every_view_rotation() {
|
||||
// Keep the whole rotated page on screen.
|
||||
app.set_option("zoom", "60").unwrap();
|
||||
});
|
||||
settle(&mut h);
|
||||
crate::support::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"))
|
||||
@@ -1321,7 +1311,7 @@ fn document_tabs_expose_selection_and_activate_from_keyboard() {
|
||||
}
|
||||
if let Ok(dir) = std::env::var("PDFCRAFT_TAB_SHOTS") {
|
||||
let _gpu = crate::gpu();
|
||||
settle(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
h.render().unwrap().save(format!("{dir}/focused-tab-{key:?}.png")).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -1547,7 +1537,7 @@ fn short_window_rail_buttons_scroll_clear_of_page_navigation() {
|
||||
app.open_bytes("short.pdf", None, FIXTURE.to_vec()).unwrap();
|
||||
app
|
||||
});
|
||||
settle(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
let rail = |h: &Harness<'static, PdfCraftApp>, label: &str| {
|
||||
h.query_all_by_label(label).map(|n| n.rect()).find(|r| r.left() >= width - 48.0).unwrap_or_else(|| panic!("{label} in the rail"))
|
||||
};
|
||||
|
||||
@@ -1266,13 +1266,7 @@ fn highlight_fields_tints_the_field_area() {
|
||||
h.state_mut().set_option("panel", "none").unwrap();
|
||||
// Highlighting is on by default; start from plain fields.
|
||||
h.state_mut().set_option("fields", "off").unwrap();
|
||||
for _ in 0..100 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
crate::support::settle(&mut h);
|
||||
let r = rect(&h, 0).expect("page 1");
|
||||
// The field spans x 50..250, y 300..330 (PDF space, y up) on a 300×400 page; sample inside it.
|
||||
let at = egui::pos2(r.min.x + r.width() * (60.0 / 300.0), r.min.y + r.height() * (1.0 - 305.0 / 400.0));
|
||||
@@ -1409,13 +1403,7 @@ fn required_radio_buttons_get_a_round_red_border() {
|
||||
app.set_option("fields", "on").unwrap();
|
||||
app
|
||||
});
|
||||
for _ in 0..100 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
crate::support::settle(&mut h);
|
||||
let r = rect(&h, 0).expect("page 1");
|
||||
let ppp = h.ctx.pixels_per_point();
|
||||
let img = h.render().expect("renders");
|
||||
@@ -1560,13 +1548,7 @@ fn required_fields_get_a_red_border_when_highlighting() {
|
||||
h.state_mut().set_option("fields", "on").unwrap();
|
||||
let props = pdfcraft_engine::FieldProps { required: Some(true), ..Default::default() };
|
||||
h.state_mut().apply_edit(pdfcraft_engine::Edit::SetFieldProps { name: "fullname".into(), props: Box::new(props) });
|
||||
for _ in 0..100 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
crate::support::settle(&mut h);
|
||||
let r = rect(&h, 0).expect("page 1");
|
||||
// Just inside the left edge of the field (x 50, y 300..330 on a 300×400 page).
|
||||
let ppp = h.ctx.pixels_per_point();
|
||||
@@ -1622,13 +1604,7 @@ trailer << /Root 1 0 R >>
|
||||
/// Mean and largest difference between page 1 on screen and the page rendered directly at the
|
||||
/// view's exact device scale, over the lined area of `page_raster_maps_one_to_one_onto_screen_pixels`.
|
||||
fn compare_page_with_raster(h: &mut Harness<'static, PdfCraftApp>, pdf: &[u8], zoom: f32) -> (f64, u8) {
|
||||
for _ in 0..100 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
crate::support::settle(h);
|
||||
let ppp = h.ctx.pixels_per_point();
|
||||
let r = rect(h, 0).expect("page 1");
|
||||
let img = h.render().expect("renders");
|
||||
@@ -1693,16 +1669,6 @@ fn grid_harness(pdf: Vec<u8>, zoom: &str) -> Harness<'static, PdfCraftApp> {
|
||||
})
|
||||
}
|
||||
|
||||
fn settle(h: &mut Harness<'static, PdfCraftApp>) {
|
||||
for _ in 0..400 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
|
||||
/// Detail in the middle of the window: the summed difference between neighbouring pixels.
|
||||
fn centre_detail(h: &mut Harness<'static, PdfCraftApp>) -> u64 {
|
||||
let img = h.render().expect("renders");
|
||||
@@ -1720,7 +1686,7 @@ fn centre_detail(h: &mut Harness<'static, PdfCraftApp>) -> u64 {
|
||||
fn zooming_a_tiled_page_shows_its_earlier_tiles_until_new_ones_arrive() {
|
||||
let _gpu = gpu();
|
||||
let mut h = grid_harness(fine_grid_pdf(), "300");
|
||||
settle(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
let sharp = centre_detail(&mut h);
|
||||
// The first frame at a new zoom has no tiles for it yet (they are requested at its end).
|
||||
h.state_mut().set_option("zoom", "310").unwrap();
|
||||
@@ -1736,13 +1702,13 @@ fn zooming_a_tiled_page_shows_its_earlier_tiles_until_new_ones_arrive() {
|
||||
fn returning_to_a_visited_zoom_reuses_its_cached_tiles() {
|
||||
let _gpu = gpu();
|
||||
let mut h = grid_harness(fine_grid_pdf(), "300");
|
||||
settle(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
let before = h.state().views[0].tiles_debug(0);
|
||||
assert!(!before.is_empty(), "the 300 % page should be tiled, but it cached no tiles");
|
||||
// Zoom right out, where the 1200 pt page fits in one raster and is not tiled at all, so
|
||||
// none of its tiles is demanded...
|
||||
h.state_mut().set_option("zoom", "50").unwrap();
|
||||
settle(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
// ...then back to the first zoom, which was already rendered sharp.
|
||||
h.state_mut().set_option("zoom", "300").unwrap();
|
||||
h.step();
|
||||
|
||||
@@ -40,13 +40,7 @@ fn harness() -> Harness<'static, PdfCraftApp> {
|
||||
app
|
||||
});
|
||||
// Background rendering and text extraction can take seconds on a loaded CI runner (FreeBSD VM).
|
||||
for _ in 0..3000 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
crate::support::settle(&mut h);
|
||||
h
|
||||
}
|
||||
|
||||
@@ -1732,13 +1726,7 @@ fn a_picture_inside_form_artwork_under_text_is_selected_and_copied() {
|
||||
app.open_bytes("scan.pdf", None, pdf).unwrap();
|
||||
app
|
||||
});
|
||||
for _ in 0..40 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
crate::support::settle(&mut h);
|
||||
h.state_mut().system_clipboard = false;
|
||||
let i = h.state().active.unwrap();
|
||||
let r = h.state().views[i].page_screen_rect(0).expect("on screen");
|
||||
|
||||
@@ -103,17 +103,6 @@ fn ordinary_forms_and_documents_have_no_xfa_notice() {
|
||||
assert_eq!(xfa(&h), Some(pdfcraft_render::Xfa::Dynamic));
|
||||
}
|
||||
|
||||
/// Run frames until the pages are drawn (fields can be clicked then).
|
||||
fn wait_for_pages(h: &mut Harness<'static, PdfCraftApp>) {
|
||||
for _ in 0..40 {
|
||||
h.run_steps(2);
|
||||
if !h.state().render_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
|
||||
/// Click the centre of a field's widget.
|
||||
fn click_field(h: &mut Harness<'static, PdfCraftApp>, name: &str) {
|
||||
let p = {
|
||||
@@ -133,7 +122,7 @@ fn click_field(h: &mut Harness<'static, PdfCraftApp>, name: &str) {
|
||||
#[test]
|
||||
fn xfa_buttons_run_their_scripts_in_the_app() {
|
||||
let mut h = open(pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::scripted_template()));
|
||||
wait_for_pages(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
let names = |h: &Harness<'static, PdfCraftApp>| {
|
||||
let s = h.state();
|
||||
s.session.get(s.views[0].id).unwrap().form.iter().map(|f| f.name.clone()).collect::<Vec<_>>()
|
||||
@@ -189,7 +178,7 @@ fn a_form_whose_scripts_were_turned_off_at_open_says_so() {
|
||||
fn scripts_turned_off_by_a_click_bring_a_closed_notice_bar_back() {
|
||||
let tpl = pdfcraft_xfa::fixtures::scripted_template().replace(r#"xfa.host.messageBox("Hello " + qty.rawValue);"#, RUNAWAY);
|
||||
let mut h = open(pdfcraft_xfa::fixtures::shell(&tpl));
|
||||
wait_for_pages(&mut h);
|
||||
crate::support::settle(&mut h);
|
||||
// The form opens with its scripts on, and the user closes the notice bar.
|
||||
let id = h.state().views[0].id;
|
||||
assert!(!h.state().session.get(id).unwrap().xfa_scripts_off());
|
||||
|
||||
Reference in New Issue
Block a user