#![allow(clippy::expect_used)]
use pdfrum::{Document, FormSession, Key, Modifiers, Point};
fn document() -> Document {
Document::open("tests/fixtures/text_form.pdf").expect("the text_form fixture must open")
}
const INSIDE: Point = Point::new(120.0, 115.0);
const OUTSIDE: Point = Point::new(10.0, 10.0);
fn click(session: &mut FormSession<'_>, at: Point) {
session.mouse_move(0, at, Modifiers::NONE);
session.mouse_down(0, at, Modifiers::NONE);
session.mouse_up(0, at, Modifiers::NONE);
}
fn type_text(session: &mut FormSession<'_>, text: &str) {
for ch in text.chars() {
session.character(ch, Modifiers::NONE);
}
}
#[test]
fn a_click_focuses_the_field_under_it() {
let doc = document();
let mut session = FormSession::new(&doc);
assert!(session.focused_annot().is_none());
click(&mut session, INSIDE);
let focused = session.focused_annot().expect("the click must focus");
assert_eq!(focused.page, pdfrum::PageIndex::FIRST);
assert_eq!(focused.index, 0);
}
#[test]
fn a_click_on_bare_page_focuses_nothing() {
let doc = document();
let mut session = FormSession::new(&doc);
click(&mut session, OUTSIDE);
assert!(session.focused_annot().is_none());
}
#[test]
fn typing_after_a_click_reaches_the_field() {
let doc = document();
let mut session = FormSession::new(&doc);
click(&mut session, INSIDE);
type_text(&mut session, "Hello");
assert_eq!(session.focused_text().as_deref(), Some("Hello"));
}
#[test]
fn typing_with_no_focus_changes_nothing() {
let doc = document();
let mut session = FormSession::new(&doc);
let response = session.character('x', Modifiers::NONE);
assert!(!response.consumed);
assert!(response.updates.is_empty());
assert!(session.focused_annot().is_none());
assert!(session.focused_text().is_none());
}
#[test]
fn click_type_read_undo_is_one_round_trip() {
let doc = document();
let mut session = FormSession::new(&doc);
click(&mut session, INSIDE);
type_text(&mut session, "abc");
assert_eq!(session.focused_text().as_deref(), Some("abc"));
assert!(session.can_undo());
session.key_down(Key::Z, Modifiers::CONTROL);
assert_eq!(session.focused_text().as_deref(), Some("ab"));
assert!(session.can_redo());
session.key_down(Key::Y, Modifiers::CONTROL);
assert_eq!(session.focused_text().as_deref(), Some("abc"));
}
#[test]
fn a_field_keeps_its_text_across_a_focus_change() {
let doc = document();
let mut session = FormSession::new(&doc);
click(&mut session, INSIDE);
type_text(&mut session, "kept");
click(&mut session, OUTSIDE);
assert!(session.focused_annot().is_none());
click(&mut session, INSIDE);
assert_eq!(session.focused_text().as_deref(), Some("kept"));
}
#[test]
fn dropping_focus_produces_an_appearance_for_the_field_that_lost_it() {
let doc = document();
let mut session = FormSession::new(&doc);
click(&mut session, INSIDE);
type_text(&mut session, "value");
let response = session.blur();
assert!(session.focused_annot().is_none());
assert!(
!response.updates.is_empty(),
"losing focus must report what changed"
);
}
#[test]
fn a_click_inside_a_widget_is_consumed_and_one_outside_is_not() {
let doc = document();
let mut session = FormSession::new(&doc);
let inside = session.mouse_down(0, INSIDE, Modifiers::NONE);
assert!(inside.consumed);
let mut fresh = FormSession::new(&doc);
let outside = fresh.mouse_down(0, OUTSIDE, Modifiers::NONE);
assert!(!outside.consumed);
}
#[test]
fn select_all_then_typing_replaces_the_value() {
let doc = document();
let mut session = FormSession::new(&doc);
click(&mut session, INSIDE);
type_text(&mut session, "old");
session.key_down(Key::A, Modifiers::CONTROL);
type_text(&mut session, "new");
assert_eq!(session.focused_text().as_deref(), Some("new"));
}
#[test]
fn an_event_on_a_missing_page_answers_rather_than_panicking() {
let doc = document();
let mut session = FormSession::new(&doc);
let response = session.mouse_down(999, Point::new(10.0, 10.0), Modifiers::NONE);
assert!(!response.consumed);
assert!(session.focused_annot().is_none());
}
const FORM_BEGIN_X: f64 = 102.0;
const FORM_END_X: f64 = 195.0;
const FORM_Y: f64 = 115.0;
fn drag(session: &mut FormSession<'_>, from: f64, to: f64) {
session.mouse_move(0, Point::new(from, FORM_Y), Modifiers::NONE);
session.mouse_down(0, Point::new(from, FORM_Y), Modifiers::NONE);
session.mouse_move(0, Point::new(to, FORM_Y), Modifiers::NONE);
session.mouse_up(0, Point::new(to, FORM_Y), Modifiers::NONE);
}
fn type_run(session: &mut FormSession<'_>, count: u32) {
session.mouse_move(0, Point::new(FORM_BEGIN_X, FORM_Y), Modifiers::NONE);
session.mouse_down(0, Point::new(FORM_BEGIN_X, FORM_Y), Modifiers::NONE);
session.mouse_up(0, Point::new(FORM_BEGIN_X, FORM_Y), Modifiers::NONE);
for i in 0..count {
let ch = char::from_u32(u32::from(b'A') + i).unwrap_or('?');
session.character(ch, Modifiers::NONE);
}
}
#[test]
fn a_drag_selects_what_it_crosses() {
let doc = document();
let mut session = FormSession::new(&doc);
assert!(session.focused_text().is_none());
assert!(session.selected_text().is_none());
type_run(&mut session, 3);
assert_eq!(session.focused_text().as_deref(), Some("ABC"));
drag(&mut session, 125.0, FORM_BEGIN_X);
assert_eq!(session.selected_text().as_deref(), Some("ABC"));
}
#[test]
fn an_empty_selection_is_distinguishable_from_no_field() {
let doc = document();
let mut session = FormSession::new(&doc);
type_run(&mut session, 3);
drag(&mut session, 125.0, 125.0);
assert_eq!(session.selected_text().as_deref(), Some(""));
assert!(session.focused_text().is_some());
}
#[test]
fn a_drag_selects_the_same_run_in_either_direction() {
let doc = document();
let mut session = FormSession::new(&doc);
type_run(&mut session, 12);
assert_eq!(session.focused_text().as_deref(), Some("ABCDEFGHIJKL"));
let backwards = {
drag(&mut session, 170.0, 125.0);
session.selected_text()
};
let forwards = {
drag(&mut session, 125.0, 170.0);
session.selected_text()
};
assert_eq!(
backwards, forwards,
"a drag must select the same run whichever way it is made"
);
assert!(
backwards.is_some_and(|text| !text.is_empty()),
"and it must select something"
);
}
#[test]
fn a_drag_across_the_whole_field_selects_all_of_it() {
let doc = document();
let mut session = FormSession::new(&doc);
type_run(&mut session, 12);
drag(&mut session, FORM_END_X, FORM_BEGIN_X);
assert_eq!(session.selected_text().as_deref(), Some("ABCDEFGHIJKL"));
}
#[test]
fn a_double_click_selects_the_whole_line() {
let doc = document();
let mut session = FormSession::new(&doc);
type_run(&mut session, 0);
for ch in "Hello World".chars() {
session.character(ch, Modifiers::NONE);
}
session.double_click(0, Point::new(130.0, FORM_Y), Modifiers::NONE);
assert_eq!(session.selected_text().as_deref(), Some("Hello World"));
}
#[test]
fn text_replaced_at_a_caret_is_inserted_there() {
let doc = document();
let mut session = FormSession::new(&doc);
type_run(&mut session, 8);
assert_eq!(session.focused_text().as_deref(), Some("ABCDEFGH"));
drag(&mut session, FORM_BEGIN_X, FORM_BEGIN_X);
assert_eq!(session.selected_text().as_deref(), Some(""));
assert!(session.replace_selection("Hello"));
assert_eq!(session.focused_text().as_deref(), Some("HelloABCDEFGH"));
}
#[test]
fn text_replaced_at_the_end_is_appended() {
let doc = document();
let mut session = FormSession::new(&doc);
type_run(&mut session, 8);
drag(&mut session, FORM_END_X, FORM_END_X);
assert!(session.replace_selection("Hello"));
assert_eq!(session.focused_text().as_deref(), Some("ABCDEFGHHello"));
}
#[test]
fn deleting_the_whole_selection_empties_the_field() {
let doc = document();
let mut session = FormSession::new(&doc);
type_run(&mut session, 12);
drag(&mut session, FORM_END_X, FORM_BEGIN_X);
assert_eq!(session.selected_text().as_deref(), Some("ABCDEFGHIJKL"));
assert!(session.replace_selection(""));
assert_eq!(session.focused_text().as_deref(), Some(""));
}
#[test]
fn deleting_an_empty_selection_does_nothing() {
let doc = document();
let mut session = FormSession::new(&doc);
type_run(&mut session, 12);
assert_eq!(session.selected_text().as_deref(), Some(""));
assert!(
!session.replace_selection(""),
"an empty replacement of an empty selection changes nothing"
);
assert_eq!(session.focused_text().as_deref(), Some("ABCDEFGHIJKL"));
}
#[test]
fn deleting_a_middle_selection_leaves_both_ends() {
let doc = document();
let mut session = FormSession::new(&doc);
type_run(&mut session, 12);
drag(&mut session, 170.0, 125.0);
let selected = session
.selected_text()
.expect("the drag selected something");
assert!(!selected.is_empty());
assert!(session.replace_selection(""));
let left = session.focused_text().expect("the field still has text");
assert_eq!(
left,
"ABCDEFGHIJKL".replace(&selected, ""),
"exactly the selected run is removed"
);
}
#[test]
fn replacing_a_selection_undoes_as_one_step() {
let doc = document();
let mut session = FormSession::new(&doc);
type_run(&mut session, 8);
drag(&mut session, FORM_END_X, FORM_BEGIN_X);
session.replace_selection("xyz");
assert_eq!(session.focused_text().as_deref(), Some("xyz"));
session.key_down(Key::Z, Modifiers::CONTROL);
assert_eq!(
session.focused_text().as_deref(),
Some("ABCDEFGH"),
"a replacement undoes as one step, not three"
);
}
#[test]
fn replacing_with_no_focus_refuses() {
let doc = document();
let mut session = FormSession::new(&doc);
assert!(!session.replace_selection("Hello"));
assert!(session.focused_text().is_none());
}
#[test]
fn select_all_selects_the_whole_value() {
let doc = document();
let mut session = FormSession::new(&doc);
session.focus_at(0, Point::new(115.0, 115.0), Modifiers::NONE);
assert!(session.focused_annot().is_some());
assert!(session.replace_selection("Hello"));
assert_eq!(session.focused_text().as_deref(), Some("Hello"));
assert_eq!(session.selected_text().as_deref(), Some(""));
session.key_down(Key::A, Modifiers::CONTROL);
assert_eq!(session.selected_text().as_deref(), Some("Hello"));
}
#[test]
fn an_unfocused_field_and_an_empty_selection_are_different_answers() {
let doc = document();
let mut session = FormSession::new(&doc);
assert_eq!(session.selected_text(), None);
session.focus_at(0, Point::new(115.0, 115.0), Modifiers::NONE);
assert_eq!(session.selected_text().as_deref(), Some(""));
}
#[test]
fn select_all_on_an_empty_field_selects_nothing_and_succeeds() {
let doc = document();
let mut session = FormSession::new(&doc);
session.focus_at(0, Point::new(115.0, 115.0), Modifiers::NONE);
session.key_down(Key::A, Modifiers::CONTROL);
assert_eq!(session.selected_text().as_deref(), Some(""));
assert_eq!(session.focused_text().as_deref(), Some(""));
}
#[test]
fn taking_focus_reports_the_change_in_the_updates() {
let doc = document();
let mut session = FormSession::new(&doc);
session.mouse_move(0, INSIDE, Modifiers::NONE);
let response = session.mouse_down(0, INSIDE, Modifiers::NONE);
let focus_changes = response
.updates
.iter()
.filter(|update| matches!(update.kind, pdfrum::UpdateKind::FocusChanged { .. }))
.count();
assert_eq!(
focus_changes, 1,
"one focus change is reported, whatever the embedding interface"
);
}
#[test]
fn re_clicking_the_focused_field_reports_no_focus_change() {
let doc = document();
let mut session = FormSession::new(&doc);
click(&mut session, INSIDE);
let response = session.mouse_down(0, Point::new(130.0, 115.0), Modifiers::NONE);
let focus_changes = response
.updates
.iter()
.filter(|update| matches!(update.kind, pdfrum::UpdateKind::FocusChanged { .. }))
.count();
assert_eq!(
focus_changes, 0,
"clicking inside the field that already has focus moves the caret only"
);
}
#[test]
fn a_text_field_has_no_selectable_rows() {
let doc = document();
let mut session = FormSession::new(&doc);
type_run(&mut session, 3);
assert_eq!(session.focused_text().as_deref(), Some("ABC"));
for index in [0, 1, 100, usize::MAX] {
assert!(
!session.is_index_selected(index),
"a text field has no row {index}"
);
assert!(
!session.set_index_selected(index, true),
"and none can be selected"
);
}
assert_eq!(session.focused_text().as_deref(), Some("ABC"));
}