use pdfrum_form::edit::{Place, Range, Selection, UndoItem, UndoStack};
use pdfrum_form::field::choice::{
find_next, is_index_selected, move_selection, select_only, select_range_to, set_index_selected,
toggle_index, top_visible_for, type_ahead,
};
use pdfrum_form::field::text::{route_char, route_key};
use pdfrum_form::field::{ChoiceConfig, ChoiceOption, ChoiceState};
use pdfrum_form::{Button, Key, Modifiers};
struct Gen(u32);
impl Gen {
fn next(&mut self) -> u32 {
self.0 = self.0.wrapping_mul(1_103_515_245).wrapping_add(12_345);
self.0 >> 8
}
fn below(&mut self, n: u32) -> u32 {
if n == 0 { 0 } else { self.next() % n }
}
}
#[test]
fn the_undo_stack_never_panics_and_holds_its_invariants() {
let mut rng = Gen(29);
for max in [4u32, 5, 8, 16] {
for _ in 0..40 {
let mut stack = UndoStack::with_max(max);
for _ in 0..40 {
match rng.below(4) {
0 => {
stack.push(UndoItem::GroupBoundary);
for _ in 0..rng.below(3) {
stack.push(UndoItem::Clear {
range: Range::empty_at(Place::start()),
text: String::new(),
before: Selection::empty(),
});
}
stack.push(UndoItem::GroupBoundary);
}
1 => {
stack.undo();
}
2 => {
stack.redo();
}
_ => stack.push(UndoItem::InsertWord {
old: Place::start(),
new: Place::start(),
ch: 'x',
before: Selection::empty(),
}),
}
assert!(stack.len() <= max as usize, "capacity exceeded");
let boundaries = stack.items().filter(|i| i.is_boundary()).count();
assert_eq!(boundaries % 2, 0, "eviction split a group");
assert!(stack.position() <= stack.len());
}
let mut steps = 0;
while stack.can_undo() {
stack.undo();
steps += 1;
assert!(steps <= max as usize + 1, "the undo walk did not terminate");
}
steps = 0;
while stack.can_redo() {
stack.redo();
steps += 1;
assert!(steps <= max as usize + 1, "the redo walk did not terminate");
}
}
}
}
#[test]
fn choice_operations_never_panic() {
let mut rng = Gen(41);
for _ in 0..200 {
let count = rng.below(5) as usize;
let options: Vec<ChoiceOption> = (0..count)
.map(|i| ChoiceOption {
label: format!("Row {i}"),
value: format!("v{i}"),
})
.collect();
let config = ChoiceConfig {
combo: rng.below(2) == 0,
editable: rng.below(2) == 0,
multi_select: rng.below(2) == 0,
read_only: rng.below(2) == 0,
};
let mut field = ChoiceState::new(options, config);
for _ in 0..30 {
let index = rng.below(10) as usize;
match rng.below(7) {
0 => {
set_index_selected(&mut field, index, rng.below(2) == 0);
}
1 => {
select_only(&mut field, index);
}
2 => {
select_range_to(&mut field, index);
}
3 => {
toggle_index(&mut field, index);
}
4 => {
move_selection(&mut field, i32::try_from(rng.below(7)).unwrap_or(0) - 3);
}
5 => {
type_ahead(
&mut field,
char::from_u32(65 + rng.below(30)).unwrap_or('A'),
);
}
_ => {
let _ = is_index_selected(&field, index);
}
}
assert!(field.selected.iter().all(|i| *i < count));
let _ = field.focused_text();
}
}
}
#[test]
fn type_ahead_terminates_and_stays_in_range() {
let mut rng = Gen(53);
for count in [0usize, 1, 2, 5] {
let labels: Vec<String> = (0..count).map(|i| format!("Row {i}")).collect();
for _ in 0..50 {
let from = rng.below(10) as usize;
let ch = char::from_u32(rng.below(200)).unwrap_or('a');
match find_next(&labels, from.min(count.saturating_sub(1)), ch) {
Some(index) => assert!(index < count.max(1)),
None => assert_eq!(count, 0, "only an empty list may answer nothing"),
}
}
}
}
#[test]
fn the_scroll_clamp_stays_in_range() {
let mut rng = Gen(67);
for _ in 0..500 {
let count = rng.below(20) as usize;
let visible = rng.below(20) as usize;
let selected = rng.below(30) as usize;
let top = top_visible_for(count, visible, selected);
assert!(
top == 0 || top < count,
"top {top} is past the end of {count} rows"
);
}
}
#[test]
fn keyboard_routing_is_total() {
for code in (0u16..=0xFF).chain([0x1000, 0xFFFF]) {
for bits in 0u32..16 {
let modifiers = Modifiers::from_bits(bits);
for accelerator in [Modifiers::CONTROL, Modifiers::META] {
for redo_y in [true, false] {
for selected in [true, false] {
let _ = route_key(
Key::from_virtual(code),
modifiers,
accelerator,
redo_y,
selected,
);
}
}
}
}
}
let mut rng = Gen(71);
for _ in 0..2000 {
let ch = char::from_u32(rng.below(0x11_0000)).unwrap_or('\u{FFFD}');
let modifiers = Modifiers::from_bits(rng.below(512));
for read_only in [true, false] {
for multi_line in [true, false] {
let _ = route_char(ch, modifiers, Modifiers::CONTROL, read_only, multi_line);
}
}
}
}
#[test]
fn both_buttons_are_representable() {
for button in [Button::Left, Button::Right] {
let event = pdfrum_form::Event::MouseDown {
button,
at: kurbo::Point::ZERO,
modifiers: Modifiers::NONE,
};
assert!(matches!(event, pdfrum_form::Event::MouseDown { .. }));
}
}