Add tests, fix offset logic, and bump dependencies

- Introduce a comprehensive test suite (cli, app, board, tasks, audio)
  and a
  new TESTING.md document describing test execution.
- Correct vertical offset calculation in `BoardState` to use saturating
  subtraction and ceiling division.
- Clamp initial `Offset` values to the provided maximum in
  `Offset::new`.
- Export `load_audio` from the audio thread module and switch to
  `LoopedDecoder` for proper OGG looping.
- Update several crate versions: clap, ratatui, log, rodio, and
  bitflags.
This commit is contained in:
GarandPLG committed 2026-10-18 20:29:04 +02:00
1 parent 9bc42dc898
commit fcdbbaf74c
16 files changed
+817 -246

No files matched your search

+161
View File
@@ -0,0 +1,161 @@
use clap::Parser;
use ratatui::{
Terminal,
backend::TestBackend,
crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
layout::Rect,
};
use std::sync::mpsc::{Receiver, channel};
use war_in_tunnels::{
app::{App, GameStates, View, handle_keybindings, threads::AudioCmd},
cli::Cli,
};
fn app() -> (App, Receiver<AudioCmd>) {
let args = Cli::try_parse_from(["game", "--disable-audio"]).unwrap();
let (tx, rx) = channel();
let mut app = App::new(args, tx);
app.window_area = Rect::new(0, 0, 120, 40);
app.states = Some(GameStates::new(&app.args, &app.window_area));
(app, rx)
}
fn press(app: &mut App, code: KeyCode) {
handle_keybindings(app, KeyEvent::new(code, KeyModifiers::NONE));
}
#[test]
fn menu_selection_stays_in_bounds_and_opens_each_view() {
let (mut app, _) = app();
for _ in 0..10 {
press(&mut app, KeyCode::Up);
}
assert_eq!(app.states_ref().unwrap().main_menu.selected_view, 1);
for view in [View::Skirmish, View::PerkDecks, View::SkillsConfig] {
press(&mut app, KeyCode::Char(' '));
assert_eq!(app.view, view);
press(&mut app, KeyCode::Esc);
assert_eq!(app.view, View::MainMenu);
press(&mut app, KeyCode::Down);
}
for _ in 0..10 {
press(&mut app, KeyCode::Down);
}
assert_eq!(app.states_ref().unwrap().main_menu.selected_view, 3);
}
#[test]
fn quit_keys_work_in_every_view() {
for view in [
View::MainMenu,
View::Skirmish,
View::PerkDecks,
View::SkillsConfig,
] {
for key in [
KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE),
KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL),
] {
let (mut app, _) = app();
app.view = view;
handle_keybindings(&mut app, key);
assert!(app.exit);
}
}
}
#[test]
fn key_release_does_not_trigger_quit() {
let (mut app, _) = app();
handle_keybindings(
&mut app,
KeyEvent::new_with_kind(
KeyCode::Char('q'),
KeyModifiers::NONE,
KeyEventKind::Release,
),
);
assert!(!app.exit);
}
#[test]
fn music_keys_send_commands_without_opening_audio_device() {
let (mut app, rx) = app();
press(&mut app, KeyCode::Char('m'));
press(&mut app, KeyCode::Char('b'));
press(&mut app, KeyCode::Char('n'));
assert!(matches!(rx.try_recv().unwrap(), AudioCmd::Mute));
assert!(matches!(rx.try_recv().unwrap(), AudioCmd::VolumeUp));
assert!(matches!(rx.try_recv().unwrap(), AudioCmd::VolumeDown));
drop(rx);
press(&mut app, KeyCode::Char('m'));
assert!(!app.exit);
}
#[test]
fn skirmish_keys_move_focus_toggle_panel_and_limit_zoom() {
let (mut app, _) = app();
app.view = View::Skirmish;
press(&mut app, KeyCode::Right);
assert_eq!(
app.states_ref()
.unwrap()
.skirmish
.board
.get_focused_cell()
.get_coords(),
(5, 4)
);
press(&mut app, KeyCode::Tab);
assert!(app.states_ref().unwrap().skirmish.side_panel);
press(&mut app, KeyCode::Tab);
assert!(!app.states_ref().unwrap().skirmish.side_panel);
for _ in 0..4 {
press(&mut app, KeyCode::Char(','));
}
assert_eq!(
app.states_ref().unwrap().skirmish.board.zoom_level,
war_in_tunnels::app::states::skirmish_states::ZoomLevel::ZoomedIn
);
for _ in 0..4 {
press(&mut app, KeyCode::Char('.'));
}
assert_eq!(
app.states_ref().unwrap().skirmish.board.zoom_level,
war_in_tunnels::app::states::skirmish_states::ZoomLevel::ZoomedOut
);
}
#[test]
fn all_views_render_without_real_terminal() {
let (mut app, _) = app();
let mut terminal = Terminal::new(TestBackend::new(120, 40)).unwrap();
for view in [
View::MainMenu,
View::Skirmish,
View::PerkDecks,
View::SkillsConfig,
] {
app.view = view;
let before = app.states.clone();
terminal
.draw(|frame| frame.render_widget(&mut app, frame.area()))
.unwrap();
let buffer = terminal.backend().buffer();
let text: String = buffer.content.iter().map(|cell| cell.symbol()).collect();
assert!(!text.trim().is_empty(), "{view:?}");
if matches!(view, View::PerkDecks | View::SkillsConfig) {
assert!(text.contains("Work in progress"));
}
assert_eq!(app.states, before);
}
}
#[test]
fn common_keys_work_before_states_are_initialized() {
let (mut app, _) = app();
app.states = None;
press(&mut app, KeyCode::Down);
press(&mut app, KeyCode::Char('q'));
assert!(app.exit);
}