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.
This commit is contained in:
@@ -956,6 +956,89 @@ void test_search_prompt_results_and_occurrence_jump()
|
||||
check(!app.search_active(), "Esc/back closes empty search results");
|
||||
}
|
||||
|
||||
void test_calendar_visibility_filters_without_partial_saves()
|
||||
{
|
||||
const auto day = nocal::today();
|
||||
auto default_event = make_event("default", "D", day, 8, 0,
|
||||
day, 9, 0);
|
||||
auto personal_event = make_event("personal", "P", day, 9, 0,
|
||||
day, 10, 0);
|
||||
personal_event.calendar_id = "personal";
|
||||
auto work_event = make_event("work", "W", day, 10, 0,
|
||||
day, 11, 0);
|
||||
work_event.calendar_id = "work";
|
||||
std::vector<nocal::Event> events{default_event, personal_event, work_event};
|
||||
int save_calls = 0;
|
||||
std::vector<nocal::Event> saved;
|
||||
nocal::tui::TuiApp app{
|
||||
events,
|
||||
[&](const std::span<const nocal::Event> values) {
|
||||
++save_calls;
|
||||
saved.assign(values.begin(), values.end());
|
||||
},
|
||||
-1,
|
||||
-1,
|
||||
};
|
||||
|
||||
check(app.calendars().size() == 3 && app.calendars()[0].id.empty() &&
|
||||
app.calendars()[0].name == "Default" &&
|
||||
app.calendars()[1].id == "personal" && app.calendars()[2].id == "work",
|
||||
"controller derives a stable default-first calendar list from event metadata");
|
||||
app.dispatch(nocal::tui::Action::calendars);
|
||||
check(app.calendar_picker_active(), "c opens the calendar picker");
|
||||
app.dispatch(nocal::tui::Action::down);
|
||||
app.dispatch(nocal::tui::Action::down);
|
||||
app.dispatch(nocal::tui::Action::select);
|
||||
check(!app.calendars()[2].visible && save_calls == 0,
|
||||
"toggling calendar visibility never crosses the persistence boundary");
|
||||
app.dispatch(nocal::tui::Action::back);
|
||||
|
||||
auto render_state = app.state();
|
||||
render_state.width = 120;
|
||||
render_state.height = 36;
|
||||
auto frame = nocal::tui::render_month(events, app.calendars(), render_state);
|
||||
check(frame.find("08:00 D") != std::string::npos &&
|
||||
frame.find("09:00 P") != std::string::npos &&
|
||||
frame.find("10:00 W") == std::string::npos,
|
||||
"month rendering excludes only events from hidden calendars");
|
||||
|
||||
app.handle_input("/");
|
||||
app.handle_input("work");
|
||||
app.handle_input("\r");
|
||||
check(app.state().search_results.empty(),
|
||||
"search shares the month view's calendar visibility contract");
|
||||
app.dispatch(nocal::tui::Action::back);
|
||||
|
||||
app.dispatch(nocal::tui::Action::next_event);
|
||||
check(focus_is(app, "default"), "focus starts on the first visible calendar event");
|
||||
app.dispatch(nocal::tui::Action::next_event);
|
||||
check(focus_is(app, "personal"), "focus advances to another visible calendar");
|
||||
app.dispatch(nocal::tui::Action::next_event);
|
||||
check(focus_is(app, "default"), "focus traversal skips hidden-calendar events");
|
||||
|
||||
app.dispatch(nocal::tui::Action::delete_event);
|
||||
app.handle_input("y");
|
||||
check(save_calls == 1 && saved.size() == 2 &&
|
||||
std::any_of(saved.begin(), saved.end(), [](const nocal::Event& event) {
|
||||
return event.uid == "work" && event.calendar_id == "work";
|
||||
}),
|
||||
"mutations save the full model, including events in hidden calendars");
|
||||
|
||||
app.dispatch(nocal::tui::Action::calendars);
|
||||
app.dispatch(nocal::tui::Action::select);
|
||||
check(app.calendars()[2].visible && save_calls == 1,
|
||||
"calendar picker retains its selection and can reveal a calendar without saving");
|
||||
app.dispatch(nocal::tui::Action::back);
|
||||
app.dispatch(nocal::tui::Action::next_event);
|
||||
app.dispatch(nocal::tui::Action::next_event);
|
||||
check(focus_is(app, "work"), "revealed calendar events become focusable again");
|
||||
app.dispatch(nocal::tui::Action::calendars);
|
||||
app.dispatch(nocal::tui::Action::select);
|
||||
check(!app.state().focused_event_id,
|
||||
"hiding the focused event's calendar clears stale appointment focus");
|
||||
check(save_calls == 1, "all visibility-only interactions remain unsaved session state");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
@@ -983,6 +1066,7 @@ int main()
|
||||
test_recurring_occurrence_navigation_and_whole_series_crud();
|
||||
test_recurrence_grid_expansion_and_source_zone_display();
|
||||
test_search_prompt_results_and_occurrence_jump();
|
||||
test_calendar_visibility_filters_without_partial_saves();
|
||||
if (failures != 0) {
|
||||
std::cerr << failures << " TUI focus test(s) failed\n";
|
||||
return EXIT_FAILURE;
|
||||
|
||||
Reference in New Issue
Block a user