Line data Source code
1 : /**
2 : * @file tests/unit/test_tui_pane.c
3 : * @brief Unit tests for the TUI pane + layout geometry (US-11 v2).
4 : */
5 :
6 : #include "test_helpers.h"
7 : #include "tui/pane.h"
8 : #include "tui/screen.h"
9 :
10 : #include <locale.h>
11 : #include <stdio.h>
12 : #include <stdlib.h>
13 : #include <string.h>
14 :
15 : /* --- Layout tests --- */
16 :
17 1 : static void test_layout_standard_80x24(void) {
18 1 : Layout L; layout_compute(&L, 24, 80, 30);
19 1 : ASSERT(pane_is_valid(&L.dialogs), "dialogs pane valid");
20 1 : ASSERT(pane_is_valid(&L.history), "history pane valid");
21 1 : ASSERT(pane_is_valid(&L.status), "status pane valid");
22 1 : ASSERT(L.dialogs.row == 0 && L.dialogs.col == 0, "dialogs at (0,0)");
23 1 : ASSERT(L.dialogs.rows == 23, "dialogs rows = screen_rows - 1");
24 1 : ASSERT(L.dialogs.cols == 30, "dialogs cols = hint");
25 1 : ASSERT(L.history.row == 0 && L.history.col == 30, "history right of dialogs");
26 1 : ASSERT(L.history.cols == 50, "history fills remainder");
27 1 : ASSERT(L.status.row == 23 && L.status.col == 0, "status on last row");
28 1 : ASSERT(L.status.cols == 80 && L.status.rows == 1, "status is full width, 1 row");
29 : }
30 :
31 1 : static void test_layout_clamps_left_width_hint(void) {
32 : Layout L;
33 1 : layout_compute(&L, 24, 100, 5); /* hint below minimum */
34 1 : ASSERT(L.dialogs.cols == TUI_MIN_LEFT_WIDTH, "hint clamped to min");
35 1 : layout_compute(&L, 24, 100, 200); /* hint above maximum */
36 1 : ASSERT(L.dialogs.cols == TUI_MAX_LEFT_WIDTH, "hint clamped to max");
37 : }
38 :
39 1 : static void test_layout_shrinks_left_on_narrow_terminal(void) {
40 : /* 45 cols, hint 35 — history would be 10 cols, too narrow. Left shrinks
41 : * so history gets at least min_right (20), so left = 25 is expected. */
42 1 : Layout L; layout_compute(&L, 24, 45, 35);
43 1 : ASSERT(L.history.cols >= 20, "history keeps at least 20 cols");
44 1 : ASSERT(L.dialogs.cols + L.history.cols == 45, "left + right == cols");
45 : }
46 :
47 1 : static void test_layout_rejects_too_small_screen(void) {
48 : Layout L;
49 1 : layout_compute(&L, 2, 80, 30);
50 1 : ASSERT(!pane_is_valid(&L.dialogs), "too few rows rejected");
51 1 : layout_compute(&L, 24, 10, 5);
52 1 : ASSERT(!pane_is_valid(&L.dialogs), "too few cols rejected");
53 : }
54 :
55 1 : static void test_layout_null_out_is_noop(void) {
56 1 : layout_compute(NULL, 24, 80, 30); /* must not crash */
57 1 : ASSERT(1, "null layout handled");
58 : }
59 :
60 : /* --- Pane writing tests --- */
61 :
62 1 : static void test_pane_put_str_translates_to_screen_coords(void) {
63 1 : Screen s; ASSERT(screen_init(&s, 10, 40) == 0, "init");
64 1 : Pane p = { .row = 2, .col = 5, .rows = 6, .cols = 10 };
65 1 : int w = pane_put_str(&p, &s, 0, 0, "hi", SCREEN_ATTR_NORMAL);
66 1 : ASSERT(w == 2, "two cells written");
67 1 : ASSERT(s.back[2 * 40 + 5].cp == 'h', "h at abs (2,5)");
68 1 : ASSERT(s.back[2 * 40 + 6].cp == 'i', "i at abs (2,6)");
69 1 : screen_free(&s);
70 : }
71 :
72 1 : static void test_pane_put_str_clips_at_right_edge_of_pane(void) {
73 1 : Screen s; ASSERT(screen_init(&s, 5, 40) == 0, "init");
74 1 : Pane p = { .row = 0, .col = 10, .rows = 5, .cols = 6 };
75 1 : int w = pane_put_str(&p, &s, 0, 0, "abcdefghij", SCREEN_ATTR_NORMAL);
76 1 : ASSERT(w == 6, "clipped to pane width (6)");
77 1 : ASSERT(s.back[10].cp == 'a', "first char lands in pane");
78 1 : ASSERT(s.back[15].cp == 'f', "last char lands inside pane");
79 : /* Screen column 16 is outside the pane. It may contain untouched blank. */
80 1 : ASSERT(s.back[16].cp == ' ', "no spill past pane");
81 1 : screen_free(&s);
82 : }
83 :
84 1 : static void test_pane_put_str_respects_internal_offset(void) {
85 1 : Screen s; ASSERT(screen_init(&s, 5, 40) == 0, "init");
86 1 : Pane p = { .row = 1, .col = 4, .rows = 4, .cols = 10 };
87 1 : int w = pane_put_str(&p, &s, 2, 3, "hello", SCREEN_ATTR_NORMAL);
88 1 : ASSERT(w == 5, "wrote 5 cells");
89 1 : ASSERT(s.back[3 * 40 + 7].cp == 'h', "h at abs (row=3, col=7)");
90 1 : screen_free(&s);
91 : }
92 :
93 1 : static void test_pane_put_str_rejects_out_of_range_relative_coords(void) {
94 1 : Screen s; ASSERT(screen_init(&s, 5, 40) == 0, "init");
95 1 : Pane p = { .row = 0, .col = 0, .rows = 3, .cols = 10 };
96 1 : ASSERT(pane_put_str(&p, &s, -1, 0, "x", 0) == 0, "row < 0 rejected");
97 1 : ASSERT(pane_put_str(&p, &s, 3, 0, "x", 0) == 0, "row == rows rejected");
98 1 : ASSERT(pane_put_str(&p, &s, 0, 10, "x", 0) == 0, "col == cols rejected");
99 1 : ASSERT(pane_put_str(&p, &s, 0, -1, "x", 0) == 0, "col < 0 rejected");
100 1 : screen_free(&s);
101 : }
102 :
103 1 : static void test_pane_fill_translates_and_clips(void) {
104 1 : Screen s; ASSERT(screen_init(&s, 5, 40) == 0, "init");
105 1 : Pane p = { .row = 0, .col = 5, .rows = 3, .cols = 8 };
106 1 : pane_fill(&p, &s, 1, 2, 100, SCREEN_ATTR_REVERSE);
107 : /* Should fill absolute cols 7..12 inclusive on row 1. */
108 1 : ASSERT(s.back[1 * 40 + 6].attrs == 0, "col 6 untouched");
109 1 : ASSERT(s.back[1 * 40 + 7].attrs == SCREEN_ATTR_REVERSE, "first fill cell");
110 1 : ASSERT(s.back[1 * 40 + 12].attrs == SCREEN_ATTR_REVERSE, "last fill cell");
111 1 : ASSERT(s.back[1 * 40 + 13].attrs == 0, "col 13 untouched (outside pane)");
112 1 : screen_free(&s);
113 : }
114 :
115 1 : static void test_pane_clear_resets_every_cell(void) {
116 1 : Screen s; ASSERT(screen_init(&s, 5, 40) == 0, "init");
117 1 : Pane p = { .row = 1, .col = 3, .rows = 2, .cols = 5 };
118 : /* Dirty the whole pane first. */
119 3 : for (int r = 0; r < p.rows; r++) {
120 2 : pane_fill(&p, &s, r, 0, p.cols, SCREEN_ATTR_BOLD);
121 : }
122 1 : pane_clear(&p, &s);
123 3 : for (int r = 0; r < p.rows; r++) {
124 12 : for (int c = 0; c < p.cols; c++) {
125 10 : int idx = (p.row + r) * 40 + (p.col + c);
126 10 : ASSERT(s.back[idx].cp == ' ', "cell blank after clear");
127 10 : ASSERT(s.back[idx].attrs == 0, "attrs reset after clear");
128 : }
129 : }
130 : /* Adjacent cells should be untouched. */
131 1 : ASSERT(s.back[1 * 40 + 2].attrs == 0, "left-of-pane untouched");
132 1 : ASSERT(s.back[1 * 40 + 8].attrs == 0, "right-of-pane untouched");
133 1 : screen_free(&s);
134 : }
135 :
136 1 : static void test_pane_invalid_rect_writes_are_noop(void) {
137 1 : Screen s; ASSERT(screen_init(&s, 5, 40) == 0, "init");
138 1 : Pane empty = { 0 };
139 1 : ASSERT(!pane_is_valid(&empty), "empty pane invalid");
140 1 : ASSERT(pane_put_str(&empty, &s, 0, 0, "hi", 0) == 0, "put on invalid");
141 1 : pane_fill(&empty, &s, 0, 0, 5, SCREEN_ATTR_BOLD); /* no crash */
142 1 : pane_clear(&empty, &s); /* no crash */
143 1 : ASSERT(s.back[0].cp == ' ', "back grid untouched");
144 1 : screen_free(&s);
145 : }
146 :
147 1 : static void test_pane_put_str_wide_char_does_not_spill(void) {
148 1 : setlocale(LC_CTYPE, "en_US.UTF-8");
149 1 : Screen s; ASSERT(screen_init(&s, 2, 10) == 0, "init");
150 1 : Pane p = { .row = 0, .col = 0, .rows = 2, .cols = 3 };
151 : /* "a日" — 'a' (1) + wide char (2) = 3 exactly fits; next write of another
152 : * wide char should be refused. */
153 1 : int w1 = pane_put_str(&p, &s, 0, 0, "a\xE6\x97\xA5", SCREEN_ATTR_NORMAL);
154 1 : if (w1 != 3) {
155 0 : screen_free(&s); return; /* no UTF-8 locale */
156 : }
157 : /* Adjacent cell (screen col 3) must remain blank. */
158 1 : ASSERT(s.back[3].cp == ' ', "no spill into col past pane");
159 1 : screen_free(&s);
160 : }
161 :
162 1 : void test_tui_pane_run(void) {
163 1 : RUN_TEST(test_layout_standard_80x24);
164 1 : RUN_TEST(test_layout_clamps_left_width_hint);
165 1 : RUN_TEST(test_layout_shrinks_left_on_narrow_terminal);
166 1 : RUN_TEST(test_layout_rejects_too_small_screen);
167 1 : RUN_TEST(test_layout_null_out_is_noop);
168 1 : RUN_TEST(test_pane_put_str_translates_to_screen_coords);
169 1 : RUN_TEST(test_pane_put_str_clips_at_right_edge_of_pane);
170 1 : RUN_TEST(test_pane_put_str_respects_internal_offset);
171 1 : RUN_TEST(test_pane_put_str_rejects_out_of_range_relative_coords);
172 1 : RUN_TEST(test_pane_fill_translates_and_clips);
173 1 : RUN_TEST(test_pane_clear_resets_every_cell);
174 1 : RUN_TEST(test_pane_invalid_rect_writes_are_noop);
175 1 : RUN_TEST(test_pane_put_str_wide_char_does_not_spill);
176 1 : }
|