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

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

Generated by: LCOV version 2.0-1