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 : }
|