LCOV - code coverage report
Current view: top level - tests/unit - test_tui_app.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 98.8 % 166 164
Test Date: 2026-04-20 19:54:22 Functions: 100.0 % 18 18

            Line data    Source code
       1              : /**
       2              :  * @file tests/unit/test_tui_app.c
       3              :  * @brief Unit tests for the TUI state machine (US-11 v2).
       4              :  */
       5              : 
       6              : #include "test_helpers.h"
       7              : #include "tui/app.h"
       8              : 
       9              : #include <string.h>
      10              : 
      11           27 : static DialogEntry mk_entry(DialogPeerKind kind, int64_t id,
      12              :                              const char *title) {
      13           27 :     DialogEntry e = {0};
      14           27 :     e.kind = kind;
      15           27 :     e.peer_id = id;
      16           27 :     if (title) {
      17           27 :         strncpy(e.title, title, sizeof(e.title) - 1);
      18              :     }
      19           27 :     return e;
      20              : }
      21              : 
      22            8 : static void seed_dialogs(TuiApp *app, int n) {
      23              :     DialogEntry src[5];
      24            8 :     if (n > 5) n = 5;
      25           35 :     for (int i = 0; i < n; i++) {
      26           27 :         const char *title = (const char *[]){"Alice","Bob","Carol","Dan","Eve"}[i];
      27           27 :         src[i] = mk_entry(DIALOG_PEER_USER, 1000 + i, title);
      28              :     }
      29            8 :     dialog_pane_set_entries(&app->dialogs, src, n);
      30            8 : }
      31              : 
      32              : /* --- Init / resize --- */
      33              : 
      34            1 : static void test_init_succeeds_on_valid_size(void) {
      35              :     TuiApp app;
      36            1 :     ASSERT(tui_app_init(&app, 24, 80) == 0, "init ok");
      37            1 :     ASSERT(app.rows == 24 && app.cols == 80, "size recorded");
      38            1 :     ASSERT(app.focus == TUI_FOCUS_DIALOGS, "start focus dialogs");
      39            1 :     ASSERT(pane_is_valid(&app.layout.dialogs), "dialogs pane valid");
      40            1 :     ASSERT(pane_is_valid(&app.layout.history), "history pane valid");
      41            1 :     ASSERT(pane_is_valid(&app.layout.status), "status pane valid");
      42              :     /* Viewport heights should match pane rows. */
      43            1 :     ASSERT(app.dialogs.lv.rows_visible == app.layout.dialogs.rows,
      44              :            "dialog viewport matches pane");
      45            1 :     ASSERT(app.history.lv.rows_visible == app.layout.history.rows,
      46              :            "history viewport matches pane");
      47            1 :     tui_app_free(&app);
      48              : }
      49              : 
      50            1 : static void test_init_rejects_too_small(void) {
      51              :     TuiApp app;
      52            1 :     ASSERT(tui_app_init(&app, 0, 80) != 0, "rows=0 rejected");
      53            1 :     ASSERT(tui_app_init(&app, 24, 0) != 0, "cols=0 rejected");
      54              : }
      55              : 
      56            1 : static void test_resize_recomputes_layout(void) {
      57              :     TuiApp app;
      58            1 :     ASSERT(tui_app_init(&app, 24, 80) == 0, "init");
      59            1 :     seed_dialogs(&app, 3);
      60            1 :     list_view_end(&app.dialogs.lv);  /* dirty scroll */
      61            1 :     ASSERT(tui_app_resize(&app, 40, 120) == 0, "resize ok");
      62            1 :     ASSERT(app.rows == 40 && app.cols == 120, "new size");
      63            1 :     ASSERT(app.layout.status.cols == 120, "status widened");
      64            1 :     ASSERT(app.dialogs.lv.rows_visible == app.layout.dialogs.rows,
      65              :            "viewport follows resize");
      66            1 :     tui_app_free(&app);
      67              : }
      68              : 
      69              : /* --- Key handling --- */
      70              : 
      71            1 : static void test_ctrl_c_quits(void) {
      72            1 :     TuiApp app; tui_app_init(&app, 24, 80);
      73            1 :     TuiEvent ev = tui_app_handle_key(&app, TERM_KEY_QUIT);
      74            1 :     ASSERT(ev == TUI_EVENT_QUIT, "ctrl-c returns QUIT");
      75            1 :     tui_app_free(&app);
      76              : }
      77              : 
      78            1 : static void test_q_char_quits(void) {
      79            1 :     TuiApp app; tui_app_init(&app, 24, 80);
      80            1 :     TuiEvent ev = tui_app_handle_char(&app, 'q');
      81            1 :     ASSERT(ev == TUI_EVENT_QUIT, "'q' returns QUIT");
      82            1 :     ev = tui_app_handle_char(&app, 'Q');
      83            1 :     ASSERT(ev == TUI_EVENT_QUIT, "'Q' returns QUIT");
      84            1 :     tui_app_free(&app);
      85              : }
      86              : 
      87            1 : static void test_left_right_switch_focus(void) {
      88            1 :     TuiApp app; tui_app_init(&app, 24, 80);
      89            1 :     seed_dialogs(&app, 3);
      90              :     /* Start in dialogs; RIGHT should switch to history. */
      91            1 :     TuiEvent ev = tui_app_handle_key(&app, TERM_KEY_RIGHT);
      92            1 :     ASSERT(ev == TUI_EVENT_REDRAW, "right triggers redraw");
      93            1 :     ASSERT(app.focus == TUI_FOCUS_HISTORY, "focus moved to history");
      94            1 :     ASSERT(app.status.mode == STATUS_MODE_HISTORY, "status mode sync");
      95              :     /* RIGHT again is a no-op. */
      96            1 :     ev = tui_app_handle_key(&app, TERM_KEY_RIGHT);
      97            1 :     ASSERT(ev == TUI_EVENT_NONE, "right is no-op when already in history");
      98            1 :     ev = tui_app_handle_key(&app, TERM_KEY_LEFT);
      99            1 :     ASSERT(ev == TUI_EVENT_REDRAW, "left switches back");
     100            1 :     ASSERT(app.focus == TUI_FOCUS_DIALOGS, "focus back on dialogs");
     101            1 :     tui_app_free(&app);
     102              : }
     103              : 
     104            1 : static void test_vim_keys_switch_focus(void) {
     105            1 :     TuiApp app; tui_app_init(&app, 24, 80);
     106            1 :     TuiEvent ev = tui_app_handle_char(&app, 'l');
     107            1 :     ASSERT(ev == TUI_EVENT_REDRAW, "'l' switches to history");
     108            1 :     ASSERT(app.focus == TUI_FOCUS_HISTORY, "focus history");
     109            1 :     ev = tui_app_handle_char(&app, 'h');
     110            1 :     ASSERT(ev == TUI_EVENT_REDRAW, "'h' switches back");
     111            1 :     ASSERT(app.focus == TUI_FOCUS_DIALOGS, "focus dialogs");
     112            1 :     tui_app_free(&app);
     113              : }
     114              : 
     115            1 : static void test_arrow_keys_navigate_focused_pane(void) {
     116            1 :     TuiApp app; tui_app_init(&app, 24, 80);
     117            1 :     seed_dialogs(&app, 5);
     118            1 :     int initial = app.dialogs.lv.selected;
     119            1 :     tui_app_handle_key(&app, TERM_KEY_NEXT_LINE);
     120            1 :     ASSERT(app.dialogs.lv.selected == initial + 1, "down moved dialog cursor");
     121            1 :     tui_app_handle_key(&app, TERM_KEY_PREV_LINE);
     122            1 :     ASSERT(app.dialogs.lv.selected == initial, "up reversed");
     123            1 :     tui_app_handle_key(&app, TERM_KEY_END);
     124            1 :     ASSERT(app.dialogs.lv.selected == 4, "end to last");
     125            1 :     tui_app_handle_key(&app, TERM_KEY_HOME);
     126            1 :     ASSERT(app.dialogs.lv.selected == 0, "home to first");
     127            1 :     tui_app_free(&app);
     128              : }
     129              : 
     130            1 : static void test_jk_chars_navigate(void) {
     131            1 :     TuiApp app; tui_app_init(&app, 24, 80);
     132            1 :     seed_dialogs(&app, 3);
     133            1 :     tui_app_handle_char(&app, 'j');
     134            1 :     ASSERT(app.dialogs.lv.selected == 1, "'j' is down");
     135            1 :     tui_app_handle_char(&app, 'k');
     136            1 :     ASSERT(app.dialogs.lv.selected == 0, "'k' is up");
     137            1 :     tui_app_free(&app);
     138              : }
     139              : 
     140            1 : static void test_navigation_targets_focused_pane_only(void) {
     141            1 :     TuiApp app; tui_app_init(&app, 24, 80);
     142            1 :     seed_dialogs(&app, 5);
     143            1 :     tui_app_handle_key(&app, TERM_KEY_RIGHT);  /* focus history */
     144            1 :     tui_app_handle_key(&app, TERM_KEY_NEXT_LINE);
     145            1 :     ASSERT(app.dialogs.lv.selected == 0, "dialogs cursor not moved");
     146            1 :     tui_app_free(&app);
     147              : }
     148              : 
     149            1 : static void test_enter_on_dialog_requests_load_and_shifts_focus(void) {
     150            1 :     TuiApp app; tui_app_init(&app, 24, 80);
     151            1 :     seed_dialogs(&app, 3);
     152            1 :     TuiEvent ev = tui_app_handle_key(&app, TERM_KEY_ENTER);
     153            1 :     ASSERT(ev == TUI_EVENT_OPEN_DIALOG, "enter requests open");
     154            1 :     ASSERT(app.focus == TUI_FOCUS_HISTORY, "focus jumped to history");
     155            1 :     ASSERT(app.status.mode == STATUS_MODE_HISTORY, "status follows");
     156            1 :     tui_app_free(&app);
     157              : }
     158              : 
     159            1 : static void test_enter_with_no_dialog_is_noop(void) {
     160            1 :     TuiApp app; tui_app_init(&app, 24, 80);
     161            1 :     TuiEvent ev = tui_app_handle_key(&app, TERM_KEY_ENTER);
     162            1 :     ASSERT(ev == TUI_EVENT_NONE, "enter on empty list is no-op");
     163            1 :     ASSERT(app.focus == TUI_FOCUS_DIALOGS, "focus unchanged");
     164            1 :     tui_app_free(&app);
     165              : }
     166              : 
     167            1 : static void test_esc_also_quits(void) {
     168            1 :     TuiApp app; tui_app_init(&app, 24, 80);
     169            1 :     TuiEvent ev = tui_app_handle_key(&app, TERM_KEY_ESC);
     170            1 :     ASSERT(ev == TUI_EVENT_QUIT, "esc quits");
     171            1 :     tui_app_free(&app);
     172              : }
     173              : 
     174              : /* --- Paint --- */
     175              : 
     176            1 : static void test_paint_renders_all_three_panes(void) {
     177            1 :     TuiApp app; tui_app_init(&app, 10, 50);
     178            1 :     seed_dialogs(&app, 3);
     179            1 :     tui_app_paint(&app);
     180              :     /* Something should be drawn in each pane — check a known cell in each. */
     181            1 :     int dialog_row = app.layout.dialogs.row;
     182            1 :     ASSERT(app.screen.back[dialog_row * 50 + 0].cp == 'u',
     183              :            "dialog pane painted");
     184              :     /* History pane should show the "(select a dialog)" hint on peer_loaded=0. */
     185            1 :     int hist_mid = app.layout.history.row
     186            1 :                  + app.layout.history.rows / 2;
     187            1 :     int hit = 0;
     188            3 :     for (int c = 0; c < app.layout.history.cols; c++) {
     189            3 :         if (app.screen.back[hist_mid * 50 + app.layout.history.col + c].cp
     190            1 :             == '(') { hit = 1; break; }
     191              :     }
     192            1 :     ASSERT(hit, "history pane shows hint");
     193              :     /* Status row is reverse-video full width. */
     194            1 :     int status_row = app.layout.status.row;
     195            1 :     int rev = 1;
     196           51 :     for (int c = 0; c < 50 && rev; c++) {
     197           50 :         if (!(app.screen.back[status_row * 50 + c].attrs & SCREEN_ATTR_REVERSE))
     198            0 :             rev = 0;
     199              :     }
     200            1 :     ASSERT(rev, "status row fully reversed");
     201            1 :     tui_app_free(&app);
     202              : }
     203              : 
     204            1 : static void test_paint_does_not_flip_stdout(void) {
     205              :     /* If paint were to call screen_flip/fwrite, this test would produce
     206              :      * visible noise during the suite run. Tests print their own names, but
     207              :      * stray ANSI would show up. Sanity check: back buffer has content,
     208              :      * front buffer is still blank (flip hasn't happened). */
     209            1 :     TuiApp app; tui_app_init(&app, 10, 50);
     210            1 :     seed_dialogs(&app, 2);
     211            1 :     tui_app_paint(&app);
     212            1 :     int any_back = 0, any_front_nonblank = 0;
     213          501 :     for (int i = 0; i < 10 * 50; i++) {
     214          500 :         if (app.screen.back[i].cp && app.screen.back[i].cp != ' ') any_back = 1;
     215          500 :         if (app.screen.front[i].cp && app.screen.front[i].cp != ' ')
     216            0 :             any_front_nonblank = 1;
     217              :     }
     218            1 :     ASSERT(any_back, "paint filled back");
     219            1 :     ASSERT(!any_front_nonblank, "paint did not touch front (no flip)");
     220            1 :     tui_app_free(&app);
     221              : }
     222              : 
     223            1 : void test_tui_app_run(void) {
     224            1 :     RUN_TEST(test_init_succeeds_on_valid_size);
     225            1 :     RUN_TEST(test_init_rejects_too_small);
     226            1 :     RUN_TEST(test_resize_recomputes_layout);
     227            1 :     RUN_TEST(test_ctrl_c_quits);
     228            1 :     RUN_TEST(test_q_char_quits);
     229            1 :     RUN_TEST(test_left_right_switch_focus);
     230            1 :     RUN_TEST(test_vim_keys_switch_focus);
     231            1 :     RUN_TEST(test_arrow_keys_navigate_focused_pane);
     232            1 :     RUN_TEST(test_jk_chars_navigate);
     233            1 :     RUN_TEST(test_navigation_targets_focused_pane_only);
     234            1 :     RUN_TEST(test_enter_on_dialog_requests_load_and_shifts_focus);
     235            1 :     RUN_TEST(test_enter_with_no_dialog_is_noop);
     236            1 :     RUN_TEST(test_esc_also_quits);
     237            1 :     RUN_TEST(test_paint_renders_all_three_panes);
     238            1 :     RUN_TEST(test_paint_does_not_flip_stdout);
     239            1 : }
        

Generated by: LCOV version 2.0-1