#include "nocal/domain/date.hpp" #include "nocal/tui/Screen.hpp" #include "nocal/tui/TuiApp.hpp" #include #include #include #include #include #include #include namespace { int failures = 0; void check(bool condition, const char* message) { if (!condition) { ++failures; std::cerr << "FAIL: " << message << '\n'; } } void test_full_month_render() { using namespace std::chrono; const nocal::tui::ScreenState state{ .visible_month = year{2026} / July, .selected_day = year{2026} / July / day{17}, .today = year{2026} / July / day{17}, .width = 100, .height = 30, .show_help = false, .ansi = false, .focused_event_id = std::nullopt, .show_event_details = false, .confirm_delete = false, .search_prompt = false, .show_search_results = false, .search_query = {}, .search_results = {}, .search_result_index = 0, .notification = {}, .notification_is_error = false, }; const std::vector items{ {.id = "1", .title = "Design review", .day = year{2026} / July / day{17}, .time_of_day = hours{9} + minutes{30}, .all_day = false, .end_time_of_day = std::nullopt, .start_day = {}, .end_day = {}, .location = {}, .description = {}, .detail_title = {}, .detail_all_day = false, .detail_start_time_of_day = std::nullopt, .segment = nocal::tui::ItemSegment::single, .recurring = false, .recurrence_summary = {}, .source_time_zone = {}}, {.id = "2", .title = "Release day", .day = year{2026} / July / day{17}, .time_of_day = std::nullopt, .all_day = true, .end_time_of_day = std::nullopt, .start_day = {}, .end_day = {}, .location = {}, .description = {}, .detail_title = {}, .detail_all_day = true, .detail_start_time_of_day = std::nullopt, .segment = nocal::tui::ItemSegment::single, .recurring = false, .recurrence_summary = {}, .source_time_zone = {}}, }; const auto frame = nocal::tui::render_month(items, state); check(frame.find("Design") != std::string::npos, "appointment appears in its day cell"); check(frame.find("Release") != std::string::npos, "all-day appointment appears in its day cell"); check(frame.find("July 2026") != std::string::npos, "month title is rendered"); check(std::count(frame.begin(), frame.end(), '\n') == 29, "renderer fills the requested height deterministically"); check(frame.find("\x1b[") == std::string::npos, "ANSI can be disabled"); } void test_compact_and_input() { using namespace std::chrono; const nocal::tui::ScreenState state{ .visible_month = year{2026} / July, .selected_day = year{2026} / July / day{17}, .today = year{2026} / July / day{17}, .width = 30, .height = 12, .show_help = false, .ansi = false, .focused_event_id = std::nullopt, .show_event_details = false, .confirm_delete = false, .search_prompt = false, .show_search_results = false, .search_query = {}, .search_results = {}, .search_result_index = 0, .notification = {}, .notification_is_error = false, }; const auto frame = nocal::tui::render_month(std::span{}, state); check(frame.find("July 2026") != std::string::npos, "compact mode keeps month context"); check(nocal::tui::decode_key("\x1b[6~") == nocal::tui::Action::next_month, "PageDown is decoded"); check(nocal::tui::decode_key("k") == nocal::tui::Action::up, "Vim movement is decoded"); } void test_navigation() { using namespace std::chrono; std::vector events; nocal::tui::TuiApp app{events, -1, -1}; const auto initial = app.state().selected_day; app.dispatch(nocal::tui::Action::right); check(sys_days{app.state().selected_day} == sys_days{initial} + days{1}, "right moves by one civil day"); app.dispatch(nocal::tui::Action::down); check(sys_days{app.state().selected_day} == sys_days{initial} + days{8}, "down moves by one week"); } void test_search_frames() { using namespace std::chrono; auto state = nocal::tui::ScreenState{}; state.visible_month = year{2026} / July; state.selected_day = year{2026} / July / day{17}; state.today = state.selected_day; state.width = 72; state.height = 12; state.ansi = false; state.search_prompt = true; state.search_query = "team"; auto frame = nocal::tui::render_month( std::span{}, state); check(frame.find("SEARCH APPOINTMENTS") != std::string::npos && frame.find("/ team_") != std::string::npos, "search prompt renders its query and context"); check(std::count(frame.begin(), frame.end(), '\n') == 11, "search prompt fills the requested height"); state.search_prompt = false; state.show_search_results = true; state.search_results = { {.focus_id = "one", .title = "Team planning", .day = year{2026} / July / day{18}, .time_of_day = hours{9}, .all_day = false, .location = "Studio", .recurring = false}, {.focus_id = "two", .title = "Team holiday", .day = year{2026} / July / day{19}, .time_of_day = std::nullopt, .all_day = true, .location = {}, .recurring = true}, }; state.search_result_index = 1; frame = nocal::tui::render_month( std::span{}, state); check(frame.find("2 matches") != std::string::npos && frame.find("2026-07-18 09:00") != std::string::npos && frame.find("Team holiday") != std::string::npos, "search results render counts, dates, times, and titles"); check(frame.find("\x1b[") == std::string::npos, "search remains readable with ANSI disabled"); check(nocal::tui::decode_key("/") == nocal::tui::Action::search, "slash opens appointment search"); } } // namespace int main() { test_full_month_render(); test_compact_and_input(); test_navigation(); test_search_frames(); if (failures != 0) { std::cerr << failures << " TUI test(s) failed\n"; return EXIT_FAILURE; } std::cout << "TUI tests passed\n"; return EXIT_SUCCESS; }