mirror of
https://github.com/lexmount/moli.git
synced 2026-09-29 08:01:36 +00:00
247 lines
8.0 KiB
Rust
247 lines
8.0 KiB
Rust
use std::time::{Duration, Instant};
|
|
|
|
use moli_action_window::{
|
|
ActionBarrier, ActionBatchCause, ActionCompaction, ActionWindow, AdmissionState, ClickAction,
|
|
InputModifiers, MouseButton, PlannedAction, Point, ScrollAction, ScrollDeltaMode, WindowAction,
|
|
};
|
|
|
|
#[test]
|
|
fn default_public_surface_starts_idle_with_one_second_policy() {
|
|
let base = Instant::now();
|
|
let mut window = ActionWindow::<String>::default();
|
|
|
|
assert!(window.is_idle());
|
|
assert_eq!(window.next_deadline(), None);
|
|
let admission = window.push(
|
|
"document".to_owned(),
|
|
WindowAction::Scroll(ScrollAction::pixels(Point::default(), 0.0, 1.0)),
|
|
base,
|
|
);
|
|
assert_eq!(admission.deadline(), base + Duration::from_secs(1));
|
|
}
|
|
|
|
#[test]
|
|
fn admissions_expose_the_sequence_retained_by_the_batch() {
|
|
let base = Instant::now();
|
|
let mut window = ActionWindow::<u8, ()>::new();
|
|
let admission = window.push(
|
|
7,
|
|
WindowAction::Scroll(ScrollAction::pixels(Point::new(1.0, 2.0), 3.0, 4.0)),
|
|
base,
|
|
);
|
|
let admitted_sequence = admission.sequence();
|
|
|
|
let batch = window
|
|
.flush(ActionBarrier::Explicit, base)
|
|
.expect("admitted action should flush");
|
|
let PlannedAction::Scroll { run, .. } = &batch.actions()[0] else {
|
|
panic!("scroll admission should retain a scroll run");
|
|
};
|
|
assert_eq!(run.steps()[0].sequence(), admitted_sequence);
|
|
}
|
|
|
|
#[test]
|
|
fn public_api_accepts_owned_scope_and_non_clone_ordered_payload() {
|
|
#[derive(Debug, PartialEq)]
|
|
struct NonClonePayload(u32);
|
|
|
|
let base = Instant::now();
|
|
let mut window = ActionWindow::<String, NonClonePayload>::default();
|
|
window.push(
|
|
"document-a".to_owned(),
|
|
WindowAction::Ordered(NonClonePayload(42)),
|
|
base,
|
|
);
|
|
|
|
let batch = window
|
|
.flush(ActionBarrier::Explicit, base)
|
|
.expect("ordered action should flush");
|
|
let mut actions = batch.into_actions();
|
|
assert_eq!(actions.len(), 1);
|
|
|
|
let PlannedAction::Ordered { scope, action } = actions.remove(0) else {
|
|
panic!("expected ordered payload");
|
|
};
|
|
assert_eq!(scope, "document-a");
|
|
assert_eq!(action.into_value(), NonClonePayload(42));
|
|
}
|
|
|
|
#[test]
|
|
fn late_admission_can_be_consumed_to_execute_deadline_batch() {
|
|
let base = Instant::now();
|
|
let mut window = ActionWindow::<u8, &'static str>::new();
|
|
window.push(1, WindowAction::Ordered("first"), base);
|
|
|
|
let admission = window.push(
|
|
1,
|
|
WindowAction::Ordered("second"),
|
|
base + Duration::from_secs(1),
|
|
);
|
|
|
|
assert_eq!(admission.state(), AdmissionState::Rotated);
|
|
assert_eq!(admission.batch_id().get(), 2);
|
|
let ready = admission
|
|
.into_ready_batch()
|
|
.expect("deadline rotation must return the first batch");
|
|
assert_eq!(ready.id().get(), 1);
|
|
assert_eq!(ready.cause(), ActionBatchCause::Deadline);
|
|
assert_eq!(ready.admitted_action_count(), 1);
|
|
assert_eq!(ready.retained_action_count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn consuming_batch_preserves_all_scroll_step_metadata() {
|
|
let base = Instant::now();
|
|
let mut window = ActionWindow::<u8, ()>::default();
|
|
let scroll = ScrollAction {
|
|
position: Point::new(12.5, 17.5),
|
|
delta_x: -4.0,
|
|
delta_y: 9.0,
|
|
delta_mode: ScrollDeltaMode::Page,
|
|
modifiers: InputModifiers::ALT | InputModifiers::META,
|
|
};
|
|
window.push(7, WindowAction::Scroll(scroll.clone()), base);
|
|
|
|
let mut actions = window
|
|
.flush(ActionBarrier::Explicit, base)
|
|
.expect("batch")
|
|
.into_actions();
|
|
let PlannedAction::Scroll { scope, run } = actions.remove(0) else {
|
|
panic!("expected scroll run");
|
|
};
|
|
|
|
assert_eq!(scope, 7);
|
|
assert_eq!(run.len(), 1);
|
|
assert!(!run.is_empty());
|
|
assert_eq!(run.steps()[0].sequence().get(), 1);
|
|
assert_eq!(run.steps()[0].admitted_at(), base);
|
|
assert_eq!(run.steps()[0].value(), &scroll);
|
|
}
|
|
|
|
#[test]
|
|
fn all_mouse_buttons_and_click_counts_survive_public_model() {
|
|
let buttons = [
|
|
MouseButton::Left,
|
|
MouseButton::Middle,
|
|
MouseButton::Right,
|
|
MouseButton::Back,
|
|
MouseButton::Forward,
|
|
];
|
|
|
|
for (index, button) in buttons.into_iter().enumerate() {
|
|
let base = Instant::now();
|
|
let mut window = ActionWindow::<u8, ()>::default();
|
|
let click = ClickAction {
|
|
position: Point::new(index as f64, 2.0),
|
|
button,
|
|
click_count: (index + 1) as u32,
|
|
modifiers: InputModifiers::CONTROL,
|
|
};
|
|
window.push(1, WindowAction::Click(click.clone()), base);
|
|
|
|
let batch = window
|
|
.flush(ActionBarrier::Explicit, base)
|
|
.expect("click batch");
|
|
let PlannedAction::Click {
|
|
click: scheduled, ..
|
|
} = &batch.actions()[0]
|
|
else {
|
|
panic!("expected click");
|
|
};
|
|
assert_eq!(scheduled.value(), &click);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn canceling_unknown_scope_is_a_noop_and_preserves_deadline() {
|
|
let base = Instant::now();
|
|
let mut window = ActionWindow::<String, ()>::default();
|
|
window.push(
|
|
"kept".to_owned(),
|
|
WindowAction::Scroll(ScrollAction::pixels(Point::default(), 0.0, 1.0)),
|
|
base,
|
|
);
|
|
let deadline = window.next_deadline();
|
|
|
|
assert!(window.cancel_scope(&"missing".to_owned()).is_empty());
|
|
assert_eq!(window.next_deadline(), deadline);
|
|
assert_eq!(window.pending_admitted_action_count(), 1);
|
|
assert_eq!(window.pending_retained_action_count(), 1);
|
|
assert_eq!(window.pending_planned_action_count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn cancel_scope_returns_retained_sequences() {
|
|
let base = Instant::now();
|
|
let mut window = ActionWindow::<&str, &str>::default();
|
|
let first_scroll = window.push(
|
|
"retired",
|
|
WindowAction::Scroll(ScrollAction::pixels(Point::default(), 0.0, 10.0)),
|
|
base,
|
|
);
|
|
let kept_scroll = window.push(
|
|
"current",
|
|
WindowAction::Scroll(ScrollAction::pixels(Point::default(), 0.0, 20.0)),
|
|
base + Duration::from_millis(10),
|
|
);
|
|
let replaced_click = window.push(
|
|
"retired",
|
|
WindowAction::Click(ClickAction::new(Point::default(), MouseButton::Left, 1)),
|
|
base + Duration::from_millis(20),
|
|
);
|
|
let retained_click = window.push(
|
|
"retired",
|
|
WindowAction::Click(ClickAction::new(Point::default(), MouseButton::Left, 2)),
|
|
base + Duration::from_millis(30),
|
|
);
|
|
let ordered = window.push(
|
|
"retired",
|
|
WindowAction::Ordered("renderer-sidecar"),
|
|
base + Duration::from_millis(40),
|
|
);
|
|
let original_deadline = window.next_deadline();
|
|
|
|
assert_eq!(
|
|
window.cancel_scope(&"retired"),
|
|
vec![
|
|
first_scroll.sequence(),
|
|
retained_click.sequence(),
|
|
ordered.sequence(),
|
|
]
|
|
);
|
|
assert_ne!(replaced_click.sequence(), retained_click.sequence());
|
|
assert_eq!(window.next_deadline(), original_deadline);
|
|
assert_eq!(window.pending_admitted_action_count(), 5);
|
|
assert_eq!(window.pending_retained_action_count(), 1);
|
|
|
|
let batch = window
|
|
.flush(ActionBarrier::Explicit, base + Duration::from_millis(50))
|
|
.expect("the other scope should remain pending");
|
|
let PlannedAction::Scroll { scope, run } = &batch.actions()[0] else {
|
|
panic!("the surviving action should be a scroll run");
|
|
};
|
|
assert_eq!(*scope, "current");
|
|
assert_eq!(run.steps()[0].sequence(), kept_scroll.sequence());
|
|
}
|
|
|
|
#[test]
|
|
fn click_replacement_reports_public_compaction_without_rotation() {
|
|
let base = Instant::now();
|
|
let mut window = ActionWindow::<u8, ()>::new();
|
|
window.push(
|
|
1,
|
|
WindowAction::Click(ClickAction::new(Point::new(1.0, 1.0), MouseButton::Left, 1)),
|
|
base,
|
|
);
|
|
|
|
let replacement = window.push(
|
|
1,
|
|
WindowAction::Click(ClickAction::new(Point::new(2.0, 2.0), MouseButton::Left, 2)),
|
|
base + Duration::from_millis(1),
|
|
);
|
|
|
|
assert_eq!(replacement.state(), AdmissionState::Joined);
|
|
assert_eq!(replacement.compaction(), ActionCompaction::ReplacedClick);
|
|
assert!(replacement.ready_batch().is_none());
|
|
}
|