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

            Line data    Source code
       1              : /**
       2              :  * @file tests/unit/test_tui_dialog_pane.c
       3              :  * @brief Unit tests for the dialog pane view-model (US-11 v2).
       4              :  */
       5              : 
       6              : #include "test_helpers.h"
       7              : #include "tui/dialog_pane.h"
       8              : 
       9              : #include <string.h>
      10              : 
      11              : /* --- Helpers --- */
      12              : 
      13          133 : static DialogEntry mk_entry(DialogPeerKind kind, int64_t id,
      14              :                              const char *title, int unread) {
      15          133 :     DialogEntry e = {0};
      16          133 :     e.kind = kind;
      17          133 :     e.peer_id = id;
      18          133 :     e.unread_count = unread;
      19          133 :     if (title) {
      20          133 :         strncpy(e.title, title, sizeof(e.title) - 1);
      21          133 :         e.title[sizeof(e.title) - 1] = '\0';
      22              :     }
      23          133 :     return e;
      24              : }
      25              : 
      26              : /* Scan the given row of the back buffer for the first run of non-blank
      27              :  * cells and copy the codepoints out as an ASCII string (good enough for
      28              :  * tests that use ASCII titles). */
      29            6 : static void row_text(const Screen *s, int row, char *out, size_t cap) {
      30            6 :     size_t oi = 0;
      31          156 :     for (int c = 0; c < s->cols && oi + 1 < cap; c++) {
      32          150 :         ScreenCell cell = s->back[row * s->cols + c];
      33          150 :         out[oi++] = (cell.cp && cell.cp < 128) ? (char)cell.cp : ' ';
      34              :     }
      35              :     /* Trim trailing spaces. */
      36           87 :     while (oi > 0 && out[oi - 1] == ' ') oi--;
      37            6 :     out[oi] = '\0';
      38            6 : }
      39              : 
      40              : /* --- State tests --- */
      41              : 
      42            1 : static void test_init_is_empty(void) {
      43              :     DialogPane dp;
      44            1 :     dialog_pane_init(&dp);
      45            1 :     ASSERT(dp.count == 0, "count 0");
      46            1 :     ASSERT(dp.lv.selected == -1, "selected -1");
      47            1 :     ASSERT(dialog_pane_selected(&dp) == NULL, "no selection returns NULL");
      48              : }
      49              : 
      50            1 : static void test_set_entries_populates_and_resets_selection(void) {
      51            1 :     DialogPane dp; dialog_pane_init(&dp);
      52              :     DialogEntry src[3] = {
      53            1 :         mk_entry(DIALOG_PEER_USER, 1, "Alice", 0),
      54            1 :         mk_entry(DIALOG_PEER_CHANNEL, 2, "News", 5),
      55            1 :         mk_entry(DIALOG_PEER_CHAT, 3, "Team", 0),
      56              :     };
      57            1 :     dialog_pane_set_entries(&dp, src, 3);
      58            1 :     ASSERT(dp.count == 3, "3 entries");
      59            1 :     ASSERT(dp.lv.selected == 0, "selected first");
      60            1 :     const DialogEntry *sel = dialog_pane_selected(&dp);
      61            1 :     ASSERT(sel != NULL && sel->peer_id == 1, "selected is Alice");
      62              : }
      63              : 
      64            1 : static void test_set_entries_clamps_overflow(void) {
      65            1 :     DialogPane dp; dialog_pane_init(&dp);
      66              :     /* src must be at least DIALOG_PANE_MAX+1 so the clamp actually fires
      67              :      * without reading beyond the array bounds. */
      68              :     DialogEntry src[DIALOG_PANE_MAX + 10];
      69            1 :     memset(src, 0, sizeof(src));
      70          111 :     for (int i = 0; i < DIALOG_PANE_MAX + 10; i++)
      71          110 :         src[i] = mk_entry(DIALOG_PEER_USER, i + 1, "x", 0);
      72              :     /* Ask to copy DIALOG_PANE_MAX+10 — should clamp to DIALOG_PANE_MAX. */
      73            1 :     dialog_pane_set_entries(&dp, src, DIALOG_PANE_MAX + 10);
      74            1 :     ASSERT(dp.count == DIALOG_PANE_MAX, "clamped to max");
      75            1 :     ASSERT(dp.entries[0].peer_id == 1, "first entry copied");
      76              :     /* Verify set_entries with 0 resets to empty. */
      77            1 :     dialog_pane_set_entries(&dp, NULL, 0);
      78            1 :     ASSERT(dp.count == 0 && dp.lv.selected == -1, "reset to empty");
      79              : }
      80              : 
      81            1 : static void test_selected_after_navigation(void) {
      82            1 :     DialogPane dp; dialog_pane_init(&dp);
      83              :     DialogEntry src[4];
      84            5 :     for (int i = 0; i < 4; i++)
      85            4 :         src[i] = mk_entry(DIALOG_PEER_USER, 100 + i, "u", 0);
      86            1 :     dialog_pane_set_entries(&dp, src, 4);
      87            1 :     list_view_move_down(&dp.lv);
      88            1 :     list_view_move_down(&dp.lv);
      89            1 :     const DialogEntry *sel = dialog_pane_selected(&dp);
      90            1 :     ASSERT(sel != NULL && sel->peer_id == 102, "selection follows list_view");
      91              : }
      92              : 
      93              : /* --- Render tests --- */
      94              : 
      95            1 : static void test_render_empty_pane_shows_placeholder(void) {
      96            1 :     Screen s; ASSERT(screen_init(&s, 5, 20) == 0, "init screen");
      97            1 :     DialogPane dp; dialog_pane_init(&dp);
      98            1 :     Pane p = { .row = 0, .col = 0, .rows = 5, .cols = 20 };
      99            1 :     dp.lv.rows_visible = p.rows;
     100            1 :     dialog_pane_render(&dp, &p, &s, /*focused*/ 1);
     101              : 
     102              :     /* Row 2 (middle of 5 rows) should hold the placeholder text. */
     103            1 :     char buf[32]; row_text(&s, 2, buf, sizeof(buf));
     104            1 :     ASSERT(strstr(buf, "(no dialogs)") != NULL, "placeholder rendered");
     105            1 :     screen_free(&s);
     106              : }
     107              : 
     108            1 : static void test_render_writes_kind_prefix_and_title(void) {
     109            1 :     Screen s; ASSERT(screen_init(&s, 5, 30) == 0, "init screen");
     110            1 :     DialogPane dp; dialog_pane_init(&dp);
     111              :     DialogEntry src[3] = {
     112            1 :         mk_entry(DIALOG_PEER_USER, 1, "Alice", 0),
     113            1 :         mk_entry(DIALOG_PEER_CHANNEL, 2, "News channel", 5),
     114            1 :         mk_entry(DIALOG_PEER_CHAT, 3, "Team", 0),
     115              :     };
     116            1 :     dialog_pane_set_entries(&dp, src, 3);
     117            1 :     Pane p = { .row = 0, .col = 0, .rows = 5, .cols = 30 };
     118            1 :     dp.lv.rows_visible = p.rows;
     119            1 :     dialog_pane_render(&dp, &p, &s, /*focused*/ 0);
     120              : 
     121            1 :     char row0[64]; row_text(&s, 0, row0, sizeof(row0));
     122            1 :     ASSERT(row0[0] == 'u', "row 0 begins with 'u' (user)");
     123            1 :     ASSERT(strstr(row0, "Alice") != NULL, "row 0 contains title");
     124              : 
     125            1 :     char row1[64]; row_text(&s, 1, row1, sizeof(row1));
     126            1 :     ASSERT(row1[0] == 'c', "row 1 begins with 'c' (channel)");
     127            1 :     ASSERT(strstr(row1, "[5]") != NULL, "row 1 shows unread badge");
     128            1 :     ASSERT(strstr(row1, "News channel") != NULL, "row 1 contains title");
     129              : 
     130            1 :     char row2[64]; row_text(&s, 2, row2, sizeof(row2));
     131            1 :     ASSERT(row2[0] == 't', "row 2 begins with 't' (chat)");
     132            1 :     ASSERT(strstr(row2, "Team") != NULL, "row 2 contains title");
     133            1 :     screen_free(&s);
     134              : }
     135              : 
     136            1 : static void test_render_highlights_selection_when_focused(void) {
     137            1 :     Screen s; ASSERT(screen_init(&s, 3, 25) == 0, "init screen");
     138            1 :     DialogPane dp; dialog_pane_init(&dp);
     139              :     DialogEntry src[3] = {
     140            1 :         mk_entry(DIALOG_PEER_USER, 1, "A", 0),
     141            1 :         mk_entry(DIALOG_PEER_USER, 2, "B", 0),
     142            1 :         mk_entry(DIALOG_PEER_USER, 3, "C", 0),
     143              :     };
     144            1 :     dialog_pane_set_entries(&dp, src, 3);
     145            1 :     list_view_move_down(&dp.lv); /* select "B" on row 1 */
     146            1 :     Pane p = { .row = 0, .col = 0, .rows = 3, .cols = 25 };
     147            1 :     dp.lv.rows_visible = p.rows;
     148            1 :     dialog_pane_render(&dp, &p, &s, /*focused*/ 1);
     149              : 
     150              :     /* Row 0 (not selected) should not be reversed. */
     151            1 :     ASSERT((s.back[0 * 25 + 0].attrs & SCREEN_ATTR_REVERSE) == 0,
     152              :            "row 0 not highlighted");
     153              :     /* Row 1 (selected) should be reversed across the full pane width. */
     154           26 :     for (int c = 0; c < p.cols; c++) {
     155           25 :         uint8_t a = s.back[1 * 25 + c].attrs;
     156           25 :         ASSERT(a & SCREEN_ATTR_REVERSE, "row 1 fully highlighted");
     157              :     }
     158              :     /* Row 2 should not be reversed. */
     159            1 :     ASSERT((s.back[2 * 25 + 0].attrs & SCREEN_ATTR_REVERSE) == 0,
     160              :            "row 2 not highlighted");
     161            1 :     screen_free(&s);
     162              : }
     163              : 
     164            1 : static void test_render_does_not_highlight_when_unfocused(void) {
     165            1 :     Screen s; ASSERT(screen_init(&s, 2, 20) == 0, "init screen");
     166            1 :     DialogPane dp; dialog_pane_init(&dp);
     167              :     DialogEntry src[2] = {
     168            1 :         mk_entry(DIALOG_PEER_USER, 1, "A", 0),
     169            1 :         mk_entry(DIALOG_PEER_USER, 2, "B", 0),
     170              :     };
     171            1 :     dialog_pane_set_entries(&dp, src, 2);
     172            1 :     Pane p = { .row = 0, .col = 0, .rows = 2, .cols = 20 };
     173            1 :     dp.lv.rows_visible = p.rows;
     174            1 :     dialog_pane_render(&dp, &p, &s, /*focused*/ 0);
     175              :     /* Selected row stays on row 0; must NOT be reversed. */
     176           21 :     for (int c = 0; c < p.cols; c++) {
     177           20 :         ASSERT((s.back[c].attrs & SCREEN_ATTR_REVERSE) == 0,
     178              :                "no reverse when unfocused");
     179              :     }
     180            1 :     screen_free(&s);
     181              : }
     182              : 
     183            1 : static void test_render_bolds_unread_rows(void) {
     184            1 :     Screen s; ASSERT(screen_init(&s, 2, 25) == 0, "init screen");
     185            1 :     DialogPane dp; dialog_pane_init(&dp);
     186              :     DialogEntry src[2] = {
     187            1 :         mk_entry(DIALOG_PEER_USER, 1, "ReadRow", 0),
     188            1 :         mk_entry(DIALOG_PEER_USER, 2, "HasUnread", 3),
     189              :     };
     190            1 :     dialog_pane_set_entries(&dp, src, 2);
     191            1 :     Pane p = { .row = 0, .col = 0, .rows = 2, .cols = 25 };
     192            1 :     dp.lv.rows_visible = p.rows;
     193            1 :     dialog_pane_render(&dp, &p, &s, /*focused*/ 0);
     194            1 :     ASSERT((s.back[0].attrs & SCREEN_ATTR_BOLD) == 0, "no unread → not bold");
     195            1 :     ASSERT(s.back[25].attrs & SCREEN_ATTR_BOLD, "unread row is bold");
     196            1 :     screen_free(&s);
     197              : }
     198              : 
     199            1 : static void test_render_respects_scroll_top(void) {
     200            1 :     Screen s; ASSERT(screen_init(&s, 3, 20) == 0, "init screen");
     201            1 :     DialogPane dp; dialog_pane_init(&dp);
     202              :     DialogEntry src[6];
     203            7 :     for (int i = 0; i < 6; i++)
     204            6 :         src[i] = mk_entry(DIALOG_PEER_USER, i + 1,
     205            6 :                           (char[]){ (char)('A' + i), '\0' }, 0);
     206            1 :     dialog_pane_set_entries(&dp, src, 6);
     207            1 :     Pane p = { .row = 0, .col = 0, .rows = 3, .cols = 20 };
     208            1 :     dp.lv.rows_visible = p.rows;
     209              :     /* Scroll down so rows 3,4,5 are visible. */
     210            1 :     list_view_end(&dp.lv);
     211            1 :     dialog_pane_render(&dp, &p, &s, /*focused*/ 1);
     212            1 :     char row0[32]; row_text(&s, 0, row0, sizeof(row0));
     213            1 :     char row2[32]; row_text(&s, 2, row2, sizeof(row2));
     214            1 :     ASSERT(strstr(row0, "D") != NULL, "row 0 shows D (index 3)");
     215            1 :     ASSERT(strstr(row2, "F") != NULL, "row 2 shows F (index 5)");
     216            1 :     screen_free(&s);
     217              : }
     218              : 
     219            1 : static void test_render_null_args_are_noops(void) {
     220            1 :     Screen s; ASSERT(screen_init(&s, 2, 10) == 0, "init screen");
     221            1 :     Pane p = { .row = 0, .col = 0, .rows = 2, .cols = 10 };
     222            1 :     DialogPane dp; dialog_pane_init(&dp);
     223            1 :     dialog_pane_render(NULL, &p, &s, 0);
     224            1 :     dialog_pane_render(&dp, NULL, &s, 0);
     225            1 :     dialog_pane_render(&dp, &p, NULL, 0);
     226              :     /* No crash; back grid untouched. */
     227            1 :     ASSERT(s.back[0].cp == ' ', "back untouched");
     228            1 :     screen_free(&s);
     229              : }
     230              : 
     231            1 : void test_tui_dialog_pane_run(void) {
     232            1 :     RUN_TEST(test_init_is_empty);
     233            1 :     RUN_TEST(test_set_entries_populates_and_resets_selection);
     234            1 :     RUN_TEST(test_set_entries_clamps_overflow);
     235            1 :     RUN_TEST(test_selected_after_navigation);
     236            1 :     RUN_TEST(test_render_empty_pane_shows_placeholder);
     237            1 :     RUN_TEST(test_render_writes_kind_prefix_and_title);
     238            1 :     RUN_TEST(test_render_highlights_selection_when_focused);
     239            1 :     RUN_TEST(test_render_does_not_highlight_when_unfocused);
     240            1 :     RUN_TEST(test_render_bolds_unread_rows);
     241            1 :     RUN_TEST(test_render_respects_scroll_top);
     242            1 :     RUN_TEST(test_render_null_args_are_noops);
     243            1 : }
        

Generated by: LCOV version 2.0-1