trainlog/tui/tests/test_app_shell.c

222 lines
9.2 KiB
C

/**
* @file test_app_shell.c
* @brief APP_SHELL_V1 state and geometry contract regressions.
*/
#include <stdio.h>
#include <string.h>
#include "trainlog/app_shell.h"
#include "trainlog/terminal.h"
#define CHECK(value) do { if (!(value)) { \
(void)fprintf(stderr, "CHECK failed at %s:%d: %s\n", \
__FILE__, __LINE__, #value); return false; } } while (0)
static bool test_geometry(void)
{
static const int sizes[][2] = {
{120, 35}, {120, 31}, {100, 30}, {100, 25}, {80, 24}, {72, 20}
};
size_t index;
for (index = 0U; index < sizeof(sizes) / sizeof(sizes[0]); ++index) {
TrainlogShellLayout layout;
TrainlogRect overlay;
trainlog_shell_layout_compute(sizes[index][0], sizes[index][1], &layout);
CHECK(layout.usable);
CHECK(layout.header.y == 0 && layout.header.height == 2);
CHECK(layout.content.y == 2 && layout.content.height == sizes[index][1] - 4);
CHECK(layout.footer.y == sizes[index][1] - 2 && layout.footer.height == 2);
CHECK(layout.content.y + layout.content.height == layout.footer.y);
CHECK(layout.sidebar_visible ==
(sizes[index][0] >= 100 && sizes[index][1] >= 26));
CHECK(layout.sidebar_expanded ==
(sizes[index][0] >= 120 && sizes[index][1] >= 32));
if (layout.sidebar_visible) {
CHECK(layout.sidebar.width == 22 && layout.separator.x == 22);
CHECK(layout.content.x == 23);
} else CHECK(layout.content.x == 0);
overlay = trainlog_shell_overlay_rect(&layout, 80, 29);
CHECK(trainlog_shell_rect_contains(&(TrainlogRect){2, 0,
sizes[index][1] - 4, sizes[index][0]}, &overlay));
CHECK(overlay.y + overlay.height <= layout.footer.y);
}
{
TrainlogShellLayout layout;
trainlog_shell_layout_compute(99, 40, &layout);
CHECK(!layout.sidebar_visible);
trainlog_shell_layout_compute(160, 25, &layout);
CHECK(!layout.sidebar_visible);
trainlog_shell_layout_compute(71, 20, &layout);
CHECK(!layout.usable);
}
return true;
}
static bool test_navigation_and_actions(void)
{
TrainlogNavigationState navigation;
TrainlogActionModel actions;
TrainlogAction action = {"open.exercises", '3', "Exercices", true, 20U,
TRAINLOG_INTENT_OPEN_ROUTE, TRAINLOG_ROUTE_EXERCISES};
const TrainlogAction *found;
trainlog_navigation_init(&navigation);
CHECK(trainlog_navigation_open(&navigation, TRAINLOG_ROUTE_EXERCISES,
"ex_11111111-1111-4111-8111-111111111111"));
CHECK(navigation.current.route == TRAINLOG_ROUTE_EXERCISES);
CHECK(trainlog_navigation_open(&navigation, TRAINLOG_ROUTE_STATS_EXERCISE,
navigation.current.stable_id));
CHECK(trainlog_navigation_back(&navigation));
CHECK(navigation.current.route == TRAINLOG_ROUTE_EXERCISES);
CHECK(strcmp(navigation.current.stable_id,
"ex_11111111-1111-4111-8111-111111111111") == 0);
trainlog_actions_clear(&actions);
CHECK(trainlog_actions_add(&actions, &action));
CHECK(!trainlog_actions_add(&actions, &action));
CHECK(actions.count == 1U);
action.identifier = "help"; action.key = '?'; action.label = "Aide";
action.priority = 10U; action.intent = TRAINLOG_INTENT_OPEN_HELP;
CHECK(trainlog_actions_add(&actions, &action));
found = trainlog_actions_find_key(&actions, '3');
CHECK(found != NULL && found->intent == TRAINLOG_INTENT_OPEN_ROUTE);
CHECK(strcmp(trainlog_actions_at_priority(&actions, 0U)->identifier, "help") == 0);
actions.items[0].enabled = false;
CHECK(trainlog_actions_find_key(&actions, '3') == NULL);
return true;
}
static bool test_overlay_isolation_and_focus(void)
{
TrainlogOverlayStack stack;
TrainlogFocusTarget focus = TRAINLOG_FOCUS_NAVIGATION;
char selected[TRAINLOG_SHELL_STABLE_ID_CAPACITY];
trainlog_overlays_init(&stack);
CHECK(trainlog_overlays_push(&stack, TRAINLOG_OVERLAY_NAVIGATION,
TRAINLOG_FOCUS_CONTENT, "se_1"));
CHECK(trainlog_overlays_push(&stack, TRAINLOG_OVERLAY_CONFIRMATION,
TRAINLOG_FOCUS_OVERLAY, "se_1"));
CHECK(trainlog_overlays_push(&stack, TRAINLOG_OVERLAY_HELP,
TRAINLOG_FOCUS_OVERLAY, "se_1"));
CHECK(!trainlog_overlays_push(&stack, TRAINLOG_OVERLAY_HELP,
TRAINLOG_FOCUS_OVERLAY, "se_1"));
CHECK(trainlog_overlays_top(&stack)->type == TRAINLOG_OVERLAY_HELP);
CHECK(trainlog_overlays_pop(&stack, &focus, selected));
CHECK(focus == TRAINLOG_FOCUS_OVERLAY && strcmp(selected, "se_1") == 0);
CHECK(trainlog_overlays_top(&stack)->type == TRAINLOG_OVERLAY_CONFIRMATION);
return true;
}
static bool test_search_utf8(void)
{
TrainlogSearchState search;
char fill[2] = {'a', '\0'};
size_t index;
trainlog_search_init(&search);
search.open = true; search.focused = true;
CHECK(trainlog_search_insert(&search, "é", strlen("é")));
CHECK(trainlog_search_insert(&search, "", strlen("")));
trainlog_search_home(&search); trainlog_search_right(&search);
CHECK(search.cursor == strlen("é"));
trainlog_search_left(&search); CHECK(search.cursor == 0U);
trainlog_search_end(&search);
CHECK(trainlog_search_backspace(&search));
CHECK(strcmp(search.text, "é") == 0 && search.bytes == strlen("é"));
CHECK(!trainlog_search_escape(&search));
CHECK(search.open && search.focused && search.bytes == 0U);
CHECK(trainlog_search_escape(&search));
CHECK(!search.open && !search.focused);
trainlog_search_init(&search);
for (index = 0U; index < 200U; ++index)
CHECK(trainlog_search_insert(&search, fill, 1U));
CHECK(!trainlog_search_insert(&search, fill, 1U));
CHECK(search.capacity_error && search.bytes == 200U);
trainlog_search_init(&search);
CHECK(trainlog_search_insert(&search, "e", 1U));
CHECK(trainlog_search_insert(&search, "\xcc\x81", 2U));
CHECK(trainlog_search_insert(&search, "x", 1U));
trainlog_search_left(&search);
CHECK(search.cursor == 3U);
trainlog_search_left(&search);
CHECK(search.cursor == 0U);
trainlog_search_end(&search);
CHECK(trainlog_search_backspace(&search));
CHECK(strcmp(search.text, "e\xcc\x81") == 0);
CHECK(trainlog_search_backspace(&search));
CHECK(search.bytes == 0U && search.cursor == 0U);
return true;
}
static bool test_stable_selection_and_resize(void)
{
const char *first[] = {"a", "b", "c", "d", "e"};
const char *filtered[] = {"b", "d", "e"};
TrainlogListState list;
trainlog_list_init(&list);
trainlog_list_set_items(&list, first, 5U, 2U, false, true);
trainlog_list_move(&list, first, 3);
CHECK(strcmp(list.selected_id, "d") == 0 && list.viewport_start == 2U);
trainlog_list_set_items(&list, filtered, 3U, 1U, false, true);
CHECK(strcmp(list.selected_id, "d") == 0 && list.selected_index == 1U);
trainlog_list_set_items(&list, filtered, 3U, 3U, true, false);
CHECK(strcmp(list.selected_id, "d") == 0 && list.viewport_start == 0U);
return true;
}
static bool test_list_label_clipping(void)
{
char output[64];
trainlog_shell_format_list_label("Développé couché très long", 12,
output, sizeof(output));
CHECK(strcmp(output, "Développé c…") == 0);
trainlog_shell_format_list_label("Court", 12, output, sizeof(output));
CHECK(strcmp(output, "Court") == 0);
trainlog_shell_format_list_label("界界界", 5, output, sizeof(output));
CHECK(strcmp(output, "界界…") == 0);
return true;
}
static bool test_draft_and_generator_guards(void)
{
TrainlogDurabilityState state = {0};
CHECK(trainlog_shell_leave_decision(&state, false) == TRAINLOG_LEAVE_ALLOW);
state.session_draft = true; state.session_dirty = true;
CHECK(trainlog_shell_leave_decision(&state, false) == TRAINLOG_LEAVE_ALLOW);
CHECK(trainlog_shell_leave_decision(&state, true) == TRAINLOG_LEAVE_CONFIRM_DISCARD);
state.generator_preview = true; state.generator_preview_dirty = true;
CHECK(trainlog_shell_leave_decision(&state, false) == TRAINLOG_LEAVE_CONFIRM_KEEP);
trainlog_shell_discard_transient(&state);
CHECK(trainlog_shell_leave_decision(&state, true) == TRAINLOG_LEAVE_ALLOW);
return true;
}
static bool test_shared_form_adapter(void)
{
TrainlogFormField field;
trainlog_form_init(&field, "12");
CHECK(field.active && strcmp(field.text, "12") == 0);
CHECK(trainlog_form_handle(&field, TRAINLOG_KEY_HOME) ==
TRAINLOG_FORM_IGNORED);
CHECK(trainlog_form_handle(&field, 0x00e9) == TRAINLOG_FORM_EDITED);
CHECK(strcmp(field.text, "é12") == 0);
CHECK(trainlog_form_handle(&field, TRAINLOG_KEY_TAB) ==
TRAINLOG_FORM_NEXT);
CHECK(trainlog_form_handle(&field, TRAINLOG_KEY_F6) ==
TRAINLOG_FORM_OPEN_NAVIGATION);
CHECK(trainlog_form_handle(&field, TRAINLOG_KEY_F7) ==
TRAINLOG_FORM_OPEN_ACTIONS);
CHECK(trainlog_form_handle(&field, TRAINLOG_KEY_ESCAPE) ==
TRAINLOG_FORM_CANCEL);
return true;
}
int main(void)
{
if (!test_geometry() || !test_navigation_and_actions() ||
!test_overlay_isolation_and_focus() || !test_search_utf8() ||
!test_stable_selection_and_resize() ||
!test_list_label_clipping() ||
!test_draft_and_generator_guards() ||
!test_shared_form_adapter()) return 1;
(void)printf("PASS app_shell\n");
return 0;
}