pub mod conformance;
use crate::app::{App, Flow, Frame};
use crate::backend::Headless;
use crate::event::{
Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
};
use crate::grid::Pos;
use crate::terminal::Terminal;
use alloc::collections::VecDeque;
use alloc::string::String;
use core::fmt;
use core::time::Duration;
pub const STEP_DELTA: Duration = Duration::from_millis(16);
pub const DEFAULT_MAX_STEPS: u32 = 64;
pub struct TestHarness {
term: Terminal<Headless>,
frame: u64,
queued: VecDeque<Event>,
}
impl TestHarness {
#[must_use]
pub fn new(width: u16, height: u16) -> Self {
Self {
term: Terminal::new(Headless::new(width, height)),
frame: 0,
queued: VecDeque::new(),
}
}
pub fn push_event(&mut self, event: Event) {
self.queued.push_back(event);
}
pub fn click(&mut self, x: u16, y: u16) {
self.click_button(x, y, MouseButton::Left);
}
pub fn click_button(&mut self, x: u16, y: u16, button: MouseButton) {
let position = Pos::new(x, y);
for kind in [MouseEventKind::Down(button), MouseEventKind::Up(button)] {
self.push_event(Event::Mouse(MouseEvent {
kind,
position,
pixel_position: None,
modifiers: KeyModifiers::NONE,
}));
}
}
pub fn mouse_move(&mut self, x: u16, y: u16) {
self.push_event(Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
position: Pos::new(x, y),
pixel_position: None,
modifiers: KeyModifiers::NONE,
}));
}
pub fn key(&mut self, code: KeyCode) {
self.key_with(code, KeyModifiers::NONE);
}
pub fn key_with(&mut self, code: KeyCode, modifiers: KeyModifiers) {
self.push_event(Event::Key(KeyEvent::new(code, modifiers)));
}
pub fn resize(&mut self, width: u16, height: u16) {
self.term.resize(width, height);
self.push_event(Event::Resize(width, height));
}
pub fn step<A: App<Headless>>(&mut self, app: &mut A) -> Flow {
if let Some(event) = self.queued.pop_front() {
self.term.backend_mut().push_event(event);
}
let frame = Frame {
delta: STEP_DELTA,
frame: self.frame,
};
self.frame = self.frame.wrapping_add(1);
let present_count_before = self.term.present_count();
let flow = app.update(&mut self.term, &frame);
if flow != Flow::Idle && self.term.present_count() == present_count_before {
let Ok(()) = self.term.present();
}
flow
}
pub fn settle<A: App<Headless>>(
&mut self,
app: &mut A,
max_steps: u32,
) -> Result<u32, RunError> {
let mut steps = 0;
loop {
let flow = self.step(app);
steps += 1;
if flow == Flow::Exit || self.queued.is_empty() {
return Ok(steps);
}
if steps >= max_steps {
return Err(RunError::ExceededMaxSteps { max_steps });
}
}
}
pub fn run<A: App<Headless>>(&mut self, app: &mut A) -> u32 {
match self.settle(app, DEFAULT_MAX_STEPS) {
Ok(steps) => steps,
Err(err) => panic!("{err}"),
}
}
pub fn run_steps<A: App<Headless>>(&mut self, app: &mut A, steps: u32) {
for _ in 0..steps {
self.step(app);
}
}
#[must_use]
pub fn view(&self) -> String {
self.term.backend().format_view()
}
#[must_use]
pub const fn term(&self) -> &Terminal<Headless> {
&self.term
}
#[must_use]
pub const fn term_mut(&mut self) -> &mut Terminal<Headless> {
&mut self.term
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum RunError {
ExceededMaxSteps {
max_steps: u32,
},
}
impl fmt::Display for RunError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ExceededMaxSteps { max_steps } => write!(
f,
"TestHarness::settle did not drain its event queue within {max_steps} steps"
),
}
}
}
impl core::error::Error for RunError {}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::Flow;
use crate::backend::Backend;
use crate::color::Style;
use ixy::HasSize;
struct Clicker {
clicks: u32,
}
impl<B: Backend> App<B> for Clicker {
fn update(&mut self, term: &mut Terminal<B>, _frame: &Frame) -> Flow {
for event in term.drain_events() {
if matches!(
event,
Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
..
})
) {
self.clicks += 1;
}
}
term.surface().put((0, 0), 'x', Style::default());
Flow::Continue
}
}
#[test]
fn step_presents_before_view_reflects_it() {
struct Drawer;
impl<B: Backend> App<B> for Drawer {
fn update(&mut self, term: &mut Terminal<B>, _frame: &Frame) -> Flow {
term.surface().put((0, 0), '@', Style::default());
Flow::Continue
}
}
let mut harness = TestHarness::new(3, 1);
let mut app = Drawer;
assert!(harness.view().starts_with('ยท'));
harness.step(&mut app);
assert!(harness.view().starts_with('@'));
}
#[test]
fn click_resolves_after_settle_not_after_one_step() {
let mut harness = TestHarness::new(5, 1);
let mut app = Clicker { clicks: 0 };
harness.click(0, 0);
harness.step(&mut app);
assert_eq!(
app.clicks, 1,
"the queued Down event resolves on the first step"
);
harness.run(&mut app);
assert!(harness.view().starts_with('x'));
}
#[test]
fn settle_reports_exceeded_max_steps() {
struct NeverDrains;
impl<B: Backend> App<B> for NeverDrains {
fn update(&mut self, _term: &mut Terminal<B>, _frame: &Frame) -> Flow {
Flow::Continue
}
}
let mut harness = TestHarness::new(2, 1);
let mut app = NeverDrains;
harness.push_event(Event::Key(KeyEvent::new(
KeyCode::Char('q'),
KeyModifiers::NONE,
)));
harness.push_event(Event::Key(KeyEvent::new(
KeyCode::Char('w'),
KeyModifiers::NONE,
)));
let err = harness.settle(&mut app, 0).unwrap_err();
assert_eq!(err, RunError::ExceededMaxSteps { max_steps: 0 });
}
#[test]
fn run_steps_ignores_flow_exit() {
struct AlwaysExits {
calls: u32,
}
impl<B: Backend> App<B> for AlwaysExits {
fn update(&mut self, _term: &mut Terminal<B>, _frame: &Frame) -> Flow {
self.calls += 1;
Flow::Exit
}
}
let mut harness = TestHarness::new(2, 1);
let mut app = AlwaysExits { calls: 0 };
harness.run_steps(&mut app, 3);
assert_eq!(app.calls, 3);
}
#[test]
fn resize_updates_backend_and_queues_event() {
struct Resized {
seen: Option<(u16, u16)>,
}
impl<B: Backend> App<B> for Resized {
fn update(&mut self, term: &mut Terminal<B>, _frame: &Frame) -> Flow {
for event in term.drain_events() {
if let Event::Resize(w, h) = event {
self.seen = Some((w, h));
}
}
Flow::Continue
}
}
let mut harness = TestHarness::new(4, 4);
let mut app = Resized { seen: None };
harness.resize(8, 2);
harness.run(&mut app);
assert_eq!(harness.term().size().width(), 8);
assert_eq!(app.seen, Some((8, 2)));
}
#[test]
fn run_error_display_message() {
let err = RunError::ExceededMaxSteps { max_steps: 5 };
assert_eq!(
err.to_string(),
"TestHarness::settle did not drain its event queue within 5 steps"
);
}
}