Line data Source code
1 : /**
2 : * @file tests/unit/test_tui_history_pane.c
3 : * @brief Unit tests for the history pane view-model (US-11 v2).
4 : */
5 :
6 : #include "test_helpers.h"
7 : #include "tui/history_pane.h"
8 :
9 : #include <string.h>
10 :
11 : /* --- Helpers --- */
12 :
13 111 : static HistoryEntry mk_text(int32_t id, int outgoing, const char *text) {
14 111 : HistoryEntry e = {0};
15 111 : e.id = id;
16 111 : e.out = outgoing;
17 111 : if (text) {
18 111 : strncpy(e.text, text, HISTORY_TEXT_MAX - 1);
19 111 : e.text[HISTORY_TEXT_MAX - 1] = '\0';
20 : }
21 111 : return e;
22 : }
23 :
24 2 : static HistoryEntry mk_complex(int32_t id) {
25 2 : HistoryEntry e = {0};
26 2 : e.id = id;
27 2 : e.complex = 1;
28 2 : return e;
29 : }
30 :
31 1 : static HistoryEntry mk_media(int32_t id, MediaKind kind) {
32 1 : HistoryEntry e = {0};
33 1 : e.id = id;
34 1 : e.media = kind;
35 1 : return e;
36 : }
37 :
38 7 : static HistoryPeer mk_peer_self(void) {
39 7 : HistoryPeer p = { .kind = HISTORY_PEER_SELF };
40 7 : return p;
41 : }
42 :
43 8 : static void row_text(const Screen *s, int row, char *out, size_t cap) {
44 8 : size_t oi = 0;
45 238 : for (int c = 0; c < s->cols && oi + 1 < cap; c++) {
46 230 : ScreenCell cell = s->back[row * s->cols + c];
47 230 : out[oi++] = (cell.cp && cell.cp < 128) ? (char)cell.cp : ' ';
48 : }
49 114 : while (oi > 0 && out[oi - 1] == ' ') oi--;
50 8 : out[oi] = '\0';
51 8 : }
52 :
53 : /* --- State tests --- */
54 :
55 1 : static void test_init_is_empty_and_unloaded(void) {
56 : HistoryPane hp;
57 1 : history_pane_init(&hp);
58 1 : ASSERT(hp.count == 0, "no entries");
59 1 : ASSERT(hp.peer_loaded == 0, "not yet loaded");
60 1 : ASSERT(hp.lv.selected == -1, "no selection");
61 : }
62 :
63 1 : static void test_set_entries_marks_loaded(void) {
64 1 : HistoryPane hp; history_pane_init(&hp);
65 1 : HistoryPeer p = mk_peer_self();
66 1 : HistoryEntry src[2] = { mk_text(10, 1, "hi"), mk_text(9, 0, "hey") };
67 1 : history_pane_set_entries(&hp, &p, src, 2);
68 1 : ASSERT(hp.peer_loaded == 1, "loaded");
69 1 : ASSERT(hp.count == 2, "2 entries");
70 1 : ASSERT(hp.peer.kind == HISTORY_PEER_SELF, "peer copied");
71 1 : ASSERT(hp.lv.selected == 0, "selection at top");
72 : }
73 :
74 1 : static void test_set_entries_clamps_overflow(void) {
75 1 : HistoryPane hp; history_pane_init(&hp);
76 1 : HistoryPeer p = mk_peer_self();
77 : /* src must be at least HISTORY_PANE_MAX+1 so the clamp fires without
78 : * reading beyond the array bounds. */
79 : HistoryEntry src[HISTORY_PANE_MAX + 1];
80 1 : memset(src, 0, sizeof(src));
81 102 : for (int i = 0; i < HISTORY_PANE_MAX + 1; i++)
82 101 : src[i] = mk_text(i + 1, 0, "x");
83 1 : history_pane_set_entries(&hp, &p, src, HISTORY_PANE_MAX + 1);
84 1 : ASSERT(hp.count == HISTORY_PANE_MAX, "clamped to max");
85 : /* Setting count=0 should keep peer_loaded=1 but clear the entries. */
86 1 : history_pane_set_entries(&hp, &p, NULL, 0);
87 1 : ASSERT(hp.count == 0, "reset to empty");
88 1 : ASSERT(hp.peer_loaded == 1, "peer still loaded");
89 1 : ASSERT(hp.lv.selected == -1, "no selection on empty");
90 : }
91 :
92 : /* --- Render tests --- */
93 :
94 1 : static void test_render_unloaded_shows_select_hint(void) {
95 1 : Screen s; ASSERT(screen_init(&s, 5, 25) == 0, "init screen");
96 1 : HistoryPane hp; history_pane_init(&hp);
97 1 : Pane p = { .row = 0, .col = 0, .rows = 5, .cols = 25 };
98 1 : hp.lv.rows_visible = p.rows;
99 1 : history_pane_render(&hp, &p, &s);
100 1 : char buf[64]; row_text(&s, 2, buf, sizeof(buf));
101 1 : ASSERT(strstr(buf, "(select a dialog)") != NULL, "hint shown");
102 1 : screen_free(&s);
103 : }
104 :
105 1 : static void test_render_loaded_empty_shows_no_messages(void) {
106 1 : Screen s; ASSERT(screen_init(&s, 5, 25) == 0, "init screen");
107 1 : HistoryPane hp; history_pane_init(&hp);
108 1 : HistoryPeer peer = mk_peer_self();
109 1 : history_pane_set_entries(&hp, &peer, NULL, 0);
110 1 : Pane p = { .row = 0, .col = 0, .rows = 5, .cols = 25 };
111 1 : hp.lv.rows_visible = p.rows;
112 1 : history_pane_render(&hp, &p, &s);
113 1 : char buf[64]; row_text(&s, 2, buf, sizeof(buf));
114 1 : ASSERT(strstr(buf, "(no messages)") != NULL, "empty placeholder");
115 1 : screen_free(&s);
116 : }
117 :
118 1 : static void test_render_rows_show_direction_and_text(void) {
119 1 : Screen s; ASSERT(screen_init(&s, 4, 30) == 0, "init screen");
120 1 : HistoryPane hp; history_pane_init(&hp);
121 1 : HistoryPeer peer = mk_peer_self();
122 : HistoryEntry src[3] = {
123 1 : mk_text(100, 1, "hello"), /* outgoing */
124 1 : mk_text(99, 0, "hi there"), /* incoming */
125 1 : mk_complex(98),
126 : };
127 1 : history_pane_set_entries(&hp, &peer, src, 3);
128 1 : Pane p = { .row = 0, .col = 0, .rows = 4, .cols = 30 };
129 1 : hp.lv.rows_visible = p.rows;
130 1 : history_pane_render(&hp, &p, &s);
131 1 : char row0[64]; row_text(&s, 0, row0, sizeof(row0));
132 1 : char row1[64]; row_text(&s, 1, row1, sizeof(row1));
133 1 : char row2[64]; row_text(&s, 2, row2, sizeof(row2));
134 1 : ASSERT(row0[0] == '>', "outgoing arrow");
135 1 : ASSERT(strstr(row0, "[100]") != NULL, "id badge");
136 1 : ASSERT(strstr(row0, "hello") != NULL, "text rendered");
137 1 : ASSERT(row1[0] == '<', "incoming arrow");
138 1 : ASSERT(strstr(row2, "(complex)") != NULL, "complex marker");
139 1 : screen_free(&s);
140 : }
141 :
142 1 : static void test_render_media_without_text_shows_media_marker(void) {
143 1 : Screen s; ASSERT(screen_init(&s, 2, 30) == 0, "init screen");
144 1 : HistoryPane hp; history_pane_init(&hp);
145 1 : HistoryPeer peer = mk_peer_self();
146 1 : HistoryEntry src[1] = { mk_media(50, MEDIA_PHOTO) };
147 1 : history_pane_set_entries(&hp, &peer, src, 1);
148 1 : Pane p = { .row = 0, .col = 0, .rows = 2, .cols = 30 };
149 1 : hp.lv.rows_visible = p.rows;
150 1 : history_pane_render(&hp, &p, &s);
151 1 : char row[64]; row_text(&s, 0, row, sizeof(row));
152 1 : ASSERT(strstr(row, "(media)") != NULL, "media marker");
153 1 : screen_free(&s);
154 : }
155 :
156 1 : static void test_render_respects_scroll(void) {
157 1 : Screen s; ASSERT(screen_init(&s, 3, 30) == 0, "init screen");
158 1 : HistoryPane hp; history_pane_init(&hp);
159 1 : HistoryPeer peer = mk_peer_self();
160 : HistoryEntry src[6];
161 7 : for (int i = 0; i < 6; i++) {
162 6 : char text[8]; snprintf(text, sizeof(text), "msg%d", i);
163 6 : src[i] = mk_text(1000 + i, 0, text);
164 : }
165 1 : history_pane_set_entries(&hp, &peer, src, 6);
166 1 : Pane p = { .row = 0, .col = 0, .rows = 3, .cols = 30 };
167 1 : hp.lv.rows_visible = p.rows;
168 1 : list_view_end(&hp.lv); /* scroll to bottom */
169 1 : history_pane_render(&hp, &p, &s);
170 1 : char row0[64]; row_text(&s, 0, row0, sizeof(row0));
171 1 : char row2[64]; row_text(&s, 2, row2, sizeof(row2));
172 1 : ASSERT(strstr(row0, "msg3") != NULL, "row 0 shows msg3 after scroll");
173 1 : ASSERT(strstr(row2, "msg5") != NULL, "row 2 shows last message");
174 1 : screen_free(&s);
175 : }
176 :
177 1 : static void test_render_complex_row_is_dimmed(void) {
178 1 : Screen s; ASSERT(screen_init(&s, 2, 20) == 0, "init screen");
179 1 : HistoryPane hp; history_pane_init(&hp);
180 1 : HistoryPeer peer = mk_peer_self();
181 1 : HistoryEntry src[1] = { mk_complex(42) };
182 1 : history_pane_set_entries(&hp, &peer, src, 1);
183 1 : Pane p = { .row = 0, .col = 0, .rows = 2, .cols = 20 };
184 1 : hp.lv.rows_visible = p.rows;
185 1 : history_pane_render(&hp, &p, &s);
186 1 : ASSERT(s.back[0].attrs & SCREEN_ATTR_DIM, "complex row is dimmed");
187 1 : screen_free(&s);
188 : }
189 :
190 1 : static void test_render_null_args_noop(void) {
191 1 : Screen s; ASSERT(screen_init(&s, 2, 10) == 0, "init screen");
192 1 : Pane p = { .row = 0, .col = 0, .rows = 2, .cols = 10 };
193 1 : HistoryPane hp; history_pane_init(&hp);
194 1 : history_pane_render(NULL, &p, &s);
195 1 : history_pane_render(&hp, NULL, &s);
196 1 : history_pane_render(&hp, &p, NULL);
197 1 : ASSERT(s.back[0].cp == ' ', "back untouched");
198 1 : screen_free(&s);
199 : }
200 :
201 1 : void test_tui_history_pane_run(void) {
202 1 : RUN_TEST(test_init_is_empty_and_unloaded);
203 1 : RUN_TEST(test_set_entries_marks_loaded);
204 1 : RUN_TEST(test_set_entries_clamps_overflow);
205 1 : RUN_TEST(test_render_unloaded_shows_select_hint);
206 1 : RUN_TEST(test_render_loaded_empty_shows_no_messages);
207 1 : RUN_TEST(test_render_rows_show_direction_and_text);
208 1 : RUN_TEST(test_render_media_without_text_shows_media_marker);
209 1 : RUN_TEST(test_render_respects_scroll);
210 1 : RUN_TEST(test_render_complex_row_is_dimmed);
211 1 : RUN_TEST(test_render_null_args_noop);
212 1 : }
|