feat: add occurrence-aware appointment search

This commit is contained in:
2026-07-18 08:31:09 +01:00
parent c19098004e
commit 2774087d14
14 changed files with 485 additions and 11 deletions

View File

@@ -875,6 +875,87 @@ void test_recurrence_grid_expansion_and_source_zone_display()
::tzset();
}
void test_search_prompt_results_and_occurrence_jump()
{
using namespace std::chrono;
const auto first_day = nocal::today();
const auto second_day = nocal::from_sys_days(nocal::to_sys_days(first_day) + days{1});
const auto third_day = nocal::from_sys_days(nocal::to_sys_days(first_day) + days{2});
const auto fourth_day = nocal::from_sys_days(nocal::to_sys_days(first_day) + days{3});
auto recurring = make_event("search-series", "Quarter Sync", first_day, 9, 0,
first_day, 10, 0);
recurring.description = "Planning notes";
recurring.recurrence = nocal::RecurrenceRule{
.frequency = nocal::RecurrenceFrequency::daily,
.interval = 1,
.count = 3,
.until = std::nullopt,
.by_weekday = {},
.by_month_day = {},
.by_month = {},
};
auto other = make_event("other", "Unrelated", second_day, 12, 0,
second_day, 13, 0);
other.location = "Quarter room";
auto holiday = make_event("holiday", "Quarter holiday", third_day, 0, 0,
fourth_day, 0, 0, true);
std::vector<nocal::Event> events{recurring, other, holiday};
int saves = 0;
nocal::tui::TuiApp app{
events, [&](std::span<const nocal::Event>) { ++saves; }, -1, -1};
app.handle_input("/");
check(app.search_active() && app.state().search_prompt,
"slash opens the search prompt");
app.handle_input("QUARTER SYNX");
app.handle_input("\x7f");
app.handle_input("C");
check(app.state().search_query == "QUARTER SYNC",
"search query accepts text and backspace editing");
app.handle_input("\r");
check(app.state().show_search_results &&
app.state().search_results.size() == 3,
"search expands every matching recurrence in the bounded window");
check(app.state().search_results.front().day == first_day &&
app.state().search_results.back().day == third_day,
"search results retain chronological occurrence dates");
app.dispatch(nocal::tui::Action::down);
app.dispatch(nocal::tui::Action::down);
app.dispatch(nocal::tui::Action::select);
check(!app.search_active() && app.state().selected_day == third_day &&
app.state().visible_month == third_day.year() / third_day.month() &&
focus_starts_with(app, "search-series@occ:"),
"choosing a recurring result jumps to and focuses that exact instance");
check(saves == 0, "search and result navigation never cross the persistence boundary");
const auto selected_after_jump = app.state().selected_day;
app.handle_input("/");
app.handle_input("\x1b");
check(!app.search_active() && app.state().selected_day == selected_after_jump,
"cancelling search preserves the calendar selection");
app.handle_input("/");
app.handle_input("quarter");
app.handle_input("\r");
check(app.state().search_results.size() == 5 &&
app.state().search_results[0].day == first_day &&
app.state().search_results[1].day == second_day &&
app.state().search_results[2].day == second_day &&
app.state().search_results[3].title == "Quarter holiday" &&
app.state().search_results[4].title == "Quarter Sync",
"search results sort by date with all-day events first within each day");
app.dispatch(nocal::tui::Action::back);
app.handle_input("/");
app.handle_input("missing");
app.handle_input("\r");
check(app.state().show_search_results && app.state().search_results.empty(),
"a query with no matches produces a safe empty results view");
app.dispatch(nocal::tui::Action::back);
check(!app.search_active(), "Esc/back closes empty search results");
}
} // namespace
int main()
@@ -901,6 +982,7 @@ int main()
test_multiday_segment_labels_and_series_messaging();
test_recurring_occurrence_navigation_and_whole_series_crud();
test_recurrence_grid_expansion_and_source_zone_display();
test_search_prompt_results_and_occurrence_jump();
if (failures != 0) {
std::cerr << failures << " TUI focus test(s) failed\n";
return EXIT_FAILURE;