use cli_tutor::app::{App, ContentView, DifficultyFilter, SubmitState};
use cli_tutor::config::Config;
use cli_tutor::content::load_modules;
use cli_tutor::progress::{Progress, Stats};
fn make_app() -> App {
App::new(load_modules(), Config::default())
}
#[test]
fn input_push_appends_to_empty() {
let mut app = make_app();
app.input_push('a');
app.input_push('b');
app.input_push('c');
assert_eq!(app.input, "abc");
assert_eq!(app.cursor_pos, 3);
}
#[test]
fn input_backspace_removes_before_cursor() {
let mut app = make_app();
"hello".chars().for_each(|c| app.input_push(c));
app.input_backspace();
assert_eq!(app.input, "hell");
assert_eq!(app.cursor_pos, 4);
}
#[test]
fn input_backspace_at_start_does_nothing() {
let mut app = make_app();
app.input_push('a');
app.cursor_home();
app.input_backspace();
assert_eq!(app.input, "a");
assert_eq!(app.cursor_pos, 0);
}
#[test]
fn input_delete_removes_at_cursor() {
let mut app = make_app();
"hello".chars().for_each(|c| app.input_push(c));
app.cursor_home();
app.input_delete();
assert_eq!(app.input, "ello");
assert_eq!(app.cursor_pos, 0);
}
#[test]
fn input_delete_at_end_does_nothing() {
let mut app = make_app();
"hi".chars().for_each(|c| app.input_push(c));
app.input_delete();
assert_eq!(app.input, "hi");
}
#[test]
fn cursor_left_right_move_correctly() {
let mut app = make_app();
"abc".chars().for_each(|c| app.input_push(c));
assert_eq!(app.cursor_pos, 3);
app.cursor_left();
assert_eq!(app.cursor_pos, 2);
app.cursor_right();
assert_eq!(app.cursor_pos, 3);
}
#[test]
fn cursor_home_end() {
let mut app = make_app();
"hello".chars().for_each(|c| app.input_push(c));
app.cursor_home();
assert_eq!(app.cursor_pos, 0);
app.cursor_end();
assert_eq!(app.cursor_pos, 5);
}
#[test]
fn cursor_left_does_not_go_below_zero() {
let mut app = make_app();
app.cursor_left();
assert_eq!(app.cursor_pos, 0);
}
#[test]
fn cursor_right_does_not_exceed_input_length() {
let mut app = make_app();
app.input_push('x');
app.cursor_right();
app.cursor_right();
assert_eq!(app.cursor_pos, 1);
}
#[test]
fn insert_at_middle_of_input() {
let mut app = make_app();
"ac".chars().for_each(|c| app.input_push(c));
app.cursor_left(); app.input_push('b');
assert_eq!(app.input, "abc");
}
#[test]
fn clear_input_resets_cursor() {
let mut app = make_app();
"hello".chars().for_each(|c| app.input_push(c));
app.clear_input();
assert_eq!(app.input, "");
assert_eq!(app.cursor_pos, 0);
}
#[test]
fn cycle_view_goes_intro_examples_exercise() {
let mut app = make_app();
assert_eq!(app.current_view, ContentView::Intro);
app.cycle_view();
assert_eq!(app.current_view, ContentView::Examples);
app.cycle_view();
assert_eq!(app.current_view, ContentView::Exercise);
app.cycle_view();
assert_eq!(app.current_view, ContentView::FreePractice);
app.cycle_view();
assert_eq!(app.current_view, ContentView::Intro);
}
#[test]
fn select_next_module_increments() {
let mut app = make_app();
assert_eq!(app.selected_module, 0);
app.select_next_module();
assert_eq!(app.selected_module, 1);
}
#[test]
fn select_prev_module_at_start_does_nothing() {
let mut app = make_app();
app.select_prev_module();
assert_eq!(app.selected_module, 0);
}
#[test]
fn select_next_module_at_end_does_nothing() {
let mut app = make_app();
let last = app.modules.len() - 1;
app.selected_module = last;
app.select_next_module();
assert_eq!(app.selected_module, last);
}
#[test]
fn select_module_resets_view_and_exercise() {
let mut app = make_app();
app.cycle_view(); app.select_next_module();
assert_eq!(app.current_view, ContentView::Intro);
assert_eq!(app.current_exercise, 0);
}
#[test]
fn next_exercise_increments() {
let mut app = make_app();
app.current_view = ContentView::Exercise;
app.next_exercise();
assert_eq!(app.current_exercise, 1);
}
#[test]
fn prev_exercise_at_zero_does_nothing() {
let mut app = make_app();
app.prev_exercise();
assert_eq!(app.current_exercise, 0);
}
#[test]
fn next_exercise_at_last_does_nothing() {
let mut app = make_app();
let last = app.exercise_count() - 1;
app.current_exercise = last;
app.next_exercise();
assert_eq!(app.current_exercise, last);
}
#[test]
fn next_exercise_resets_state() {
let mut app = make_app();
"grep mango".chars().for_each(|c| app.input_push(c));
app.hints_revealed = 2;
app.next_exercise();
assert_eq!(app.input, "");
assert_eq!(app.hints_revealed, 0);
}
#[test]
fn reveal_hint_increments_up_to_max() {
let mut app = make_app();
let hint_count = app.current_exercise_opt().unwrap().hints.len();
for _ in 0..hint_count + 5 {
app.reveal_next_hint();
}
assert_eq!(app.hints_revealed, hint_count);
}
#[test]
fn toggle_solution_flips() {
let mut app = make_app();
assert!(!app.show_solution);
app.toggle_solution();
assert!(app.show_solution);
app.toggle_solution();
assert!(!app.show_solution);
}
#[test]
fn toggle_files_flips() {
let mut app = make_app();
assert!(!app.show_files);
app.toggle_files();
assert!(app.show_files);
}
#[test]
fn toggle_help_flips() {
let mut app = make_app();
assert!(!app.show_help);
app.toggle_help();
assert!(app.show_help);
app.toggle_help();
assert!(!app.show_help);
}
#[test]
fn reset_exercise_clears_all_state() {
let mut app = make_app();
"grep mango fruits.txt".chars().for_each(|c| app.input_push(c));
app.hints_revealed = 1;
app.show_solution = true;
app.show_files = true;
app.reset_exercise();
assert_eq!(app.input, "");
assert_eq!(app.hints_revealed, 0);
assert!(!app.show_solution);
assert!(!app.show_files);
assert_eq!(app.submit_state, SubmitState::Idle);
}
#[test]
fn submit_empty_command_does_nothing() {
let mut app = make_app();
app.submit_command();
assert_eq!(app.submit_state, SubmitState::Idle);
assert!(app.last_output.is_none());
}
#[test]
fn submit_wrong_command_sets_wrong_state() {
let mut app = make_app();
app.current_view = ContentView::Exercise;
"echo wrong_output".chars().for_each(|c| app.input_push(c));
app.submit_command();
assert_eq!(app.submit_state, SubmitState::Wrong);
}
#[test]
fn submit_correct_solution_sets_correct_state() {
let mut app = make_app();
app.current_view = ContentView::Exercise;
let solution = app.current_exercise_opt().unwrap().solution.clone();
solution.chars().for_each(|c| app.input_push(c));
app.submit_command();
assert_eq!(app.submit_state, SubmitState::Correct);
}
#[test]
fn submit_correct_saves_progress() {
let mut app = make_app();
let exercise_id = app.current_exercise_opt().unwrap().id.clone();
let module_name = app.current_module().module.name.clone();
let solution = app.current_exercise_opt().unwrap().solution.clone();
solution.chars().for_each(|c| app.input_push(c));
app.submit_command();
assert!(app.progress.is_completed(&module_name, &exercise_id));
}
#[test]
fn scroll_up_does_not_go_below_zero() {
let mut app = make_app();
app.current_view = ContentView::Intro;
app.scroll_up();
assert_eq!(app.intro_scroll, 0);
}
#[test]
fn scroll_down_increments() {
let mut app = make_app();
app.current_view = ContentView::Intro;
app.scroll_down();
assert_eq!(app.intro_scroll, 1);
}
#[test]
fn app_starts_with_twenty_five_modules() {
let app = make_app();
assert_eq!(app.modules.len(), 25);
}
#[test]
fn first_module_is_ls() {
let app = make_app();
assert_eq!(app.current_module().module.name, "ls");
}
#[test]
fn cursor_word_left_moves_to_word_start() {
let mut app = make_app();
"hello world".chars().for_each(|c| app.input_push(c));
app.cursor_word_left();
assert_eq!(app.cursor_pos, 6);
}
#[test]
fn cursor_word_right_moves_to_word_end() {
let mut app = make_app();
"hello world".chars().for_each(|c| app.input_push(c));
app.cursor_home();
app.cursor_word_right();
assert_eq!(app.cursor_pos, 5);
}
#[test]
fn cursor_word_left_at_start_stays() {
let mut app = make_app();
"abc".chars().for_each(|c| app.input_push(c));
app.cursor_home();
app.cursor_word_left();
assert_eq!(app.cursor_pos, 0);
}
#[test]
fn push_history_stores_entries() {
let mut app = make_app();
app.current_view = ContentView::Exercise;
"echo wrong".chars().for_each(|c| app.input_push(c));
app.submit_command(); assert!(!app.command_history.is_empty());
}
#[test]
fn history_prev_loads_last_command() {
let mut app = make_app();
app.current_view = ContentView::Exercise;
"echo one".chars().for_each(|c| app.input_push(c));
app.submit_command();
app.clear_input();
app.history_prev();
assert_eq!(app.input, "echo one");
}
#[test]
fn history_next_restores_draft() {
let mut app = make_app();
app.current_view = ContentView::Exercise;
"echo one".chars().for_each(|c| app.input_push(c));
app.submit_command();
app.clear_input();
"draft".chars().for_each(|c| app.input_push(c));
app.history_prev(); app.history_next(); assert_eq!(app.input, "draft");
}
#[test]
fn no_consecutive_duplicates_in_history() {
let mut app = make_app();
app.current_view = ContentView::Exercise;
for _ in 0..3 {
"echo dup".chars().for_each(|c| app.input_push(c));
app.submit_command();
app.clear_input();
}
assert_eq!(app.command_history.iter().filter(|s| *s == "echo dup").count(), 1);
}
#[test]
fn activate_search_clears_query() {
let mut app = make_app();
app.search_query = "old".to_string();
app.activate_search();
assert!(app.search_active);
assert!(app.search_query.is_empty());
}
#[test]
fn search_push_filters_modules() {
let mut app = make_app();
app.activate_search();
app.search_push('g');
app.search_push('r');
app.search_push('e');
app.search_push('p');
assert_eq!(app.search_filtered.len(), 1);
assert_eq!(app.modules[app.search_filtered[0]].module.name, "grep");
}
#[test]
fn search_confirm_deactivates_search() {
let mut app = make_app();
app.activate_search();
app.search_push('g');
app.search_confirm();
assert!(!app.search_active);
assert!(app.search_query.is_empty());
}
#[test]
fn search_cancel_clears_state() {
let mut app = make_app();
app.activate_search();
app.search_push('a');
app.search_cancel();
assert!(!app.search_active);
assert!(app.search_query.is_empty());
assert!(app.search_filtered.is_empty());
}
#[test]
fn visible_module_indices_all_when_no_search() {
let app = make_app();
let indices = app.visible_module_indices();
assert_eq!(indices.len(), app.modules.len());
}
#[test]
fn toggle_progress_flips_flag() {
let mut app = make_app();
assert!(!app.show_progress);
app.show_progress = true;
assert!(app.show_progress);
app.show_progress = false;
assert!(!app.show_progress);
}
#[test]
fn cycle_difficulty_filter_cycles_all_values() {
let mut app = make_app();
assert_eq!(app.difficulty_filter, DifficultyFilter::None);
app.cycle_difficulty_filter();
assert_eq!(app.difficulty_filter, DifficultyFilter::Beginner);
app.cycle_difficulty_filter();
assert_eq!(app.difficulty_filter, DifficultyFilter::Intermediate);
app.cycle_difficulty_filter();
assert_eq!(app.difficulty_filter, DifficultyFilter::Advanced);
app.cycle_difficulty_filter();
assert_eq!(app.difficulty_filter, DifficultyFilter::None);
}
#[test]
fn input_paste_inserts_at_cursor() {
let mut app = make_app();
app.input_paste("hello world");
assert_eq!(app.input, "hello world");
assert_eq!(app.cursor_pos, 11);
}
#[test]
fn input_paste_skips_control_characters() {
let mut app = make_app();
app.input_paste("abc\x01\x0adef");
assert_eq!(app.input, "abcdef");
}
#[test]
fn submit_command_free_produces_output() {
let mut app = make_app();
app.current_view = ContentView::FreePractice;
"echo hello".chars().for_each(|c| app.input_push(c));
app.submit_command_free();
let out = app.last_output.as_ref().unwrap();
assert!(out.stdout.contains("hello"));
}
#[test]
fn xp_awarded_on_first_correct_solve() {
let mut app = make_app();
app.progress = Progress::default();
app.stats = Stats::default();
app.current_view = ContentView::Exercise;
let solution = app.current_exercise_opt().unwrap().solution.clone();
solution.chars().for_each(|c| app.input_push(c));
app.submit_command();
assert!(app.stats.total_xp > 0, "XP should be awarded on first correct solve");
}
#[test]
fn xp_not_awarded_on_second_correct_solve() {
let mut app = make_app();
app.progress = Progress::default();
app.stats = Stats::default();
app.current_view = ContentView::Exercise;
let solution = app.current_exercise_opt().unwrap().solution.clone();
solution.chars().for_each(|c| app.input_push(c));
app.submit_command();
let xp_after_first = app.stats.total_xp;
app.clear_input();
solution.chars().for_each(|c| app.input_push(c));
app.submit_command();
assert_eq!(
app.stats.total_xp, xp_after_first,
"XP must not increase on re-solve"
);
}
#[test]
fn streak_starts_at_one_on_first_solve() {
let mut stats = Stats::default();
stats.update_streak();
assert_eq!(stats.streak_days, 1);
}
#[test]
fn streak_unchanged_on_same_day() {
let mut stats = Stats::default();
stats.update_streak();
stats.update_streak();
assert_eq!(stats.streak_days, 1);
}
#[test]
fn xp_add_accumulates() {
let mut stats = Stats::default();
stats.add_xp(10);
stats.add_xp(20);
assert_eq!(stats.total_xp, 30);
}