use ftui_core::event::Event;
use ftui_render::frame::Frame;
use ftui_runtime::program::{Cmd, Model, TaskSpec};
use ftui_runtime::simulator::ProgramSimulator;
use std::time::Duration;
struct TestModel {
executed_after_quit: bool,
}
#[derive(Debug)]
enum TestMsg {
QuitInBatch,
SetExecuted,
}
impl From<Event> for TestMsg {
fn from(_: Event) -> Self {
TestMsg::QuitInBatch
}
}
impl Model for TestModel {
type Message = TestMsg;
fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
match msg {
TestMsg::QuitInBatch => Cmd::Batch(vec![
Cmd::Quit,
Cmd::Msg(TestMsg::SetExecuted), ]),
TestMsg::SetExecuted => {
self.executed_after_quit = true;
Cmd::None
}
}
}
fn view(&self, _frame: &mut Frame) {}
}
#[test]
fn batch_stops_after_quit() {
let mut sim = ProgramSimulator::new(TestModel {
executed_after_quit: false,
});
sim.init();
sim.send(TestMsg::QuitInBatch);
assert!(
!sim.model().executed_after_quit,
"Commands after Quit in Batch should not be executed"
);
assert!(!sim.is_running(), "Simulator should have stopped");
}
struct TaskOrderModel {
trace: Vec<&'static str>,
}
#[derive(Debug)]
enum TaskOrderMsg {
Start,
TaskDone,
Followup,
AfterTask,
}
impl From<Event> for TaskOrderMsg {
fn from(_: Event) -> Self {
TaskOrderMsg::Start
}
}
impl Model for TaskOrderModel {
type Message = TaskOrderMsg;
fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
match msg {
TaskOrderMsg::Start => Cmd::Batch(vec![
Cmd::task_named("task-order", || TaskOrderMsg::TaskDone),
Cmd::Msg(TaskOrderMsg::AfterTask),
]),
TaskOrderMsg::TaskDone => {
self.trace.push("task");
Cmd::Msg(TaskOrderMsg::Followup)
}
TaskOrderMsg::Followup => {
self.trace.push("followup");
Cmd::None
}
TaskOrderMsg::AfterTask => {
self.trace.push("after");
Cmd::None
}
}
}
fn view(&self, _frame: &mut Frame) {}
}
#[test]
fn task_executes_before_following_batch_command() {
let mut sim = ProgramSimulator::new(TaskOrderModel { trace: Vec::new() });
sim.init();
sim.send(TaskOrderMsg::Start);
assert_eq!(sim.model().trace, vec!["task", "followup", "after"]);
}
struct TickTaskModel {
ticks: usize,
trace: Vec<&'static str>,
}
#[derive(Debug)]
enum TickTaskMsg {
Start,
TaskDone,
Tick,
}
impl From<Event> for TickTaskMsg {
fn from(event: Event) -> Self {
match event {
Event::Tick => TickTaskMsg::Tick,
_ => TickTaskMsg::Start,
}
}
}
impl Model for TickTaskModel {
type Message = TickTaskMsg;
fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
match msg {
TickTaskMsg::Start => Cmd::Batch(vec![
Cmd::Tick(Duration::from_millis(50)),
Cmd::task_named("tick-task", || TickTaskMsg::TaskDone),
]),
TickTaskMsg::TaskDone => {
self.trace.push("task");
Cmd::Tick(Duration::from_millis(10))
}
TickTaskMsg::Tick => {
self.ticks += 1;
self.trace.push("tick");
Cmd::None
}
}
}
fn view(&self, _frame: &mut Frame) {}
}
#[test]
fn tick_scheduling_is_deterministic_and_task_driven() {
let mut sim = ProgramSimulator::new(TickTaskModel {
ticks: 0,
trace: Vec::new(),
});
sim.init();
sim.send(TickTaskMsg::Start);
assert_eq!(sim.tick_rate(), Some(Duration::from_millis(10)));
sim.inject_event(Event::Tick);
sim.inject_event(Event::Tick);
assert_eq!(sim.model().ticks, 2);
assert_eq!(sim.tick_rate(), Some(Duration::from_millis(10)));
assert_eq!(sim.model().trace, vec!["task", "tick", "tick"]);
}
#[test]
fn task_spec_builder_sets_fields() {
let spec = TaskSpec::new(2.5, 12.0).with_name("demo-task");
assert_eq!(spec.weight, 2.5);
assert_eq!(spec.estimate_ms, 12.0);
assert_eq!(spec.name.as_deref(), Some("demo-task"));
}
#[test]
fn cmd_task_variants_capture_spec() {
let cmd_named: Cmd<TestMsg> = Cmd::task_named("named", || TestMsg::SetExecuted);
match cmd_named {
Cmd::Task(spec, _) => {
assert_eq!(spec.name.as_deref(), Some("named"));
assert_eq!(spec.weight, 1.0);
assert_eq!(spec.estimate_ms, 10.0);
}
_ => panic!("expected Cmd::Task for task_named"),
}
let cmd_weighted: Cmd<TestMsg> = Cmd::task_weighted(3.0, 7.5, || TestMsg::SetExecuted);
match cmd_weighted {
Cmd::Task(spec, _) => {
assert_eq!(spec.name.as_deref(), None);
assert_eq!(spec.weight, 3.0);
assert_eq!(spec.estimate_ms, 7.5);
}
_ => panic!("expected Cmd::Task for task_weighted"),
}
let spec = TaskSpec::new(4.0, 9.0).with_name("spec-task");
let cmd_spec: Cmd<TestMsg> = Cmd::task_with_spec(spec, || TestMsg::SetExecuted);
match cmd_spec {
Cmd::Task(spec, _) => {
assert_eq!(spec.name.as_deref(), Some("spec-task"));
assert_eq!(spec.weight, 4.0);
assert_eq!(spec.estimate_ms, 9.0);
}
_ => panic!("expected Cmd::Task for task_with_spec"),
}
}
#[test]
fn cmd_count_matches_nested_structure() {
let cmd: Cmd<TestMsg> = Cmd::Batch(vec![
Cmd::None,
Cmd::Msg(TestMsg::SetExecuted),
Cmd::Sequence(vec![
Cmd::None,
Cmd::Msg(TestMsg::QuitInBatch),
Cmd::Batch(vec![
Cmd::Msg(TestMsg::SetExecuted),
Cmd::Msg(TestMsg::SetExecuted),
]),
]),
]);
assert_eq!(cmd.count(), 4);
}