Files
nocal/tests/tui_tests.cpp
Bernardo Magri 88d370bf99 feat: add calendar visibility picker
Derive session calendars from event metadata and apply one visibility contract to month rendering, appointment focus, and search without filtering persistence writes.

Add keyboard picker coverage, full-model save regression tests, documentation, warning-clean builds, sanitizer verification, and live PTY terminal restoration checks.
2026-07-18 09:01:27 +01:00

209 lines
7.8 KiB
C++

#include "nocal/domain/date.hpp"
#include "nocal/tui/Screen.hpp"
#include "nocal/tui/TuiApp.hpp"
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <iostream>
#include <optional>
#include <string>
#include <vector>
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<nocal::tui::CalendarItem> 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<const nocal::tui::CalendarItem>{}, 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");
check(nocal::tui::decode_key("c") == nocal::tui::Action::calendars,
"c opens the calendar picker");
}
void test_calendar_picker_frame()
{
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 = 52;
state.height = 10;
state.ansi = false;
state.show_calendar_picker = true;
state.calendar_index = 1;
const std::vector<nocal::Calendar> calendars{
{.id = "", .name = "Default", .color = {}, .visible = true,
.read_only = false},
{.id = "work", .name = "Work", .color = {}, .visible = false,
.read_only = false},
};
const auto frame = nocal::tui::render_month(
std::span<const nocal::Event>{}, calendars, state);
check(frame.find("CALENDARS") != std::string::npos &&
frame.find("1 of 2 visible") != std::string::npos &&
frame.find("[x] Default") != std::string::npos &&
frame.find("[ ] Work") != std::string::npos,
"calendar picker renders visibility and selection context");
check(std::count(frame.begin(), frame.end(), '\n') == 9,
"calendar picker fills the requested height");
check(frame.find("\x1b[") == std::string::npos,
"calendar picker remains readable without ANSI styling");
}
void test_navigation()
{
using namespace std::chrono;
std::vector<nocal::Event> 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<const nocal::tui::CalendarItem>{}, 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<const nocal::tui::CalendarItem>{}, 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();
test_calendar_picker_frame();
if (failures != 0) {
std::cerr << failures << " TUI test(s) failed\n";
return EXIT_FAILURE;
}
std::cout << "TUI tests passed\n";
return EXIT_SUCCESS;
}