/* Unit tests for the TUI widget layer (clients/tui.c). Only the pure parts are exercised here; drawing is smoke-tested over a pty. */ #include #include #include #include "tui.h" #include "util.h" static int failures = 0; static int checks = 0; #define CHECK(cond) \ do { \ checks++; \ if (!(cond)) { \ failures++; \ fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, \ #cond); \ } \ } while (0) static void test_disp_width(void) { CHECK(tui_disp_width("") == 0); CHECK(tui_disp_width("abc") == 3); CHECK(tui_disp_width("åäö") == 3); CHECK(tui_disp_width("aåb") == 3); CHECK(tui_disp_width_n("åäö", 2) == 1); CHECK(tui_disp_width_n("åäö", 4) == 2); CHECK(tui_disp_width_n("abc", 2) == 2); } static void test_formats(void) { char buf[64]; tui_kr_format(123456, buf, sizeof buf); CHECK(strcmp(buf, "1 234,56") == 0); tui_kr_format(-1000000, buf, sizeof buf); CHECK(strcmp(buf, "-10 000,00") == 0); tui_kr_format(5, buf, sizeof buf); CHECK(strcmp(buf, "0,05") == 0); tui_kr0_format(123456, buf, sizeof buf); CHECK(strcmp(buf, "1 235") == 0); tui_kr0_format(-123456, buf, sizeof buf); CHECK(strcmp(buf, "-1 235") == 0); tui_amt_col(buf, sizeof buf, 12, 123456); CHECK(strcmp(buf, " 1 234,56") == 0); snprintf(buf, sizeof buf, "åäö"); tui_pad_field(buf, sizeof buf, 2); CHECK(strcmp(buf, "åä") == 0); } static void test_parse_kr(void) { int64_t v = -1; CHECK(tui_parse_kr("", &v) == 0 && v == 0); CHECK(tui_parse_kr("0", &v) == 0 && v == 0); CHECK(tui_parse_kr("12", &v) == 0 && v == 1200); CHECK(tui_parse_kr("1 234,56", &v) == 0 && v == 123456); CHECK(tui_parse_kr("12,3", &v) == 0 && v == 1230); CHECK(tui_parse_kr("12.30", &v) == 0 && v == 1230); CHECK(tui_parse_kr("1.234", &v) != 0); CHECK(tui_parse_kr("1,23,4", &v) != 0); CHECK(tui_parse_kr("-5", &v) != 0); CHECK(tui_parse_kr("12a", &v) != 0); } static void test_ledit(void) { char buf[32]; struct tui_ledit e; snprintf(buf, sizeof buf, "abc"); tui_le_init(&e, buf, sizeof buf); CHECK(e.pos == 3 && e.len == 3); CHECK(tui_le_key(&e, KEY_LEFT) && e.pos == 2); CHECK(tui_le_key(&e, 'X')); CHECK(strcmp(buf, "abXc") == 0 && e.pos == 3); CHECK(tui_le_key(&e, KEY_BACKSPACE)); CHECK(strcmp(buf, "abc") == 0 && e.pos == 2); CHECK(tui_le_key(&e, KEY_DC)); CHECK(strcmp(buf, "ab") == 0 && e.pos == 2); CHECK(tui_le_key(&e, KEY_HOME) && e.pos == 0); CHECK(tui_le_key(&e, 21)); /* ^U */ CHECK(strcmp(buf, "") == 0 && e.len == 0); /* UTF-8: the caret moves by code point, not by byte */ snprintf(buf, sizeof buf, "åä"); tui_le_init(&e, buf, sizeof buf); CHECK(e.pos == 4); CHECK(tui_le_key(&e, KEY_LEFT) && e.pos == 2); CHECK(tui_le_key(&e, KEY_LEFT) && e.pos == 0); CHECK(tui_le_key(&e, KEY_RIGHT) && e.pos == 2); CHECK(tui_le_key(&e, KEY_BACKSPACE)); CHECK(strcmp(buf, "ä") == 0); /* capacity: never overflow */ snprintf(buf, sizeof buf, "12345"); tui_le_init(&e, buf, 4); CHECK(tui_le_key(&e, '9') == 1 || 1); CHECK(strlen(buf) < 4 && e.len < 4); CHECK(tui_le_key(&e, KEY_UP) == 0); /* not an editing key */ } static void test_field_fresh(void) { char buf[16]; size_t pos = (size_t)-1; int fresh = 1; /* fresh + backspace deletes the last character, it does not clear */ snprintf(buf, sizeof buf, "abc"); CHECK(tui_field_edit(buf, sizeof buf, &pos, KEY_BACKSPACE, &fresh)); CHECK(strcmp(buf, "ab") == 0); /* the first printable still replaces the whole prefilled value */ snprintf(buf, sizeof buf, "abc"); pos = (size_t)-1; fresh = 1; CHECK(tui_field_edit(buf, sizeof buf, &pos, 'x', &fresh)); CHECK(strcmp(buf, "x") == 0); /* movement keeps the content, clears fresh; the next printable is inserted at the caret */ snprintf(buf, sizeof buf, "abc"); pos = (size_t)-1; fresh = 1; CHECK(tui_field_edit(buf, sizeof buf, &pos, KEY_LEFT, &fresh)); CHECK(strcmp(buf, "abc") == 0 && pos == 2 && fresh == 0); CHECK(tui_field_edit(buf, sizeof buf, &pos, 'x', &fresh)); CHECK(strcmp(buf, "abxc") == 0); /* fresh + delete removes the character at the caret, no clear */ snprintf(buf, sizeof buf, "abc"); pos = 0; fresh = 1; CHECK(tui_field_edit(buf, sizeof buf, &pos, KEY_DC, &fresh)); CHECK(strcmp(buf, "bc") == 0); } static void date_feed(char *buf, size_t cap, const char *digits) { size_t pos = (size_t)-1; int fresh = 1; for (const char *p = digits; *p; p++) tui_date_field_edit(buf, cap, &pos, *p, &fresh); } static void test_date_field(void) { char buf[16] = ""; size_t pos = (size_t)-1; int fresh = 1; date_feed(buf, sizeof buf, "20260604"); CHECK(strcmp(buf, "2026-06-04") == 0); fresh = 0; CHECK(tui_date_field_edit(buf, sizeof buf, &pos, KEY_BACKSPACE, &fresh)); CHECK(strcmp(buf, "2026-06-0") == 0); CHECK(tui_date_field_edit(buf, sizeof buf, &pos, 21, &fresh)); /* ^U */ CHECK(strcmp(buf, "") == 0); CHECK(tui_date_field_edit(buf, sizeof buf, &pos, 'x', &fresh) == 0); /* fresh replaces a prefilled value on the first digit */ snprintf(buf, sizeof buf, "2025-01-01"); pos = (size_t)-1; fresh = 1; tui_date_field_edit(buf, sizeof buf, &pos, '9', &fresh); CHECK(strcmp(buf, "9") == 0); /* full field overwrites at the caret */ date_feed(buf, sizeof buf, "20260604"); pos = (size_t)-1; fresh = 0; tui_date_field_edit(buf, sizeof buf, &pos, '9', &fresh); CHECK(strcmp(buf, "2026-06-09") == 0); /* fresh + backspace deletes the last digit; fresh + ^U clears */ snprintf(buf, sizeof buf, "2026-06-04"); pos = (size_t)-1; fresh = 1; CHECK(tui_date_field_edit(buf, sizeof buf, &pos, KEY_BACKSPACE, &fresh)); CHECK(strcmp(buf, "2026-06-0") == 0); snprintf(buf, sizeof buf, "2026-06-04"); pos = (size_t)-1; fresh = 1; CHECK(tui_date_field_edit(buf, sizeof buf, &pos, 21, &fresh)); CHECK(strcmp(buf, "") == 0); } static void nav_init(struct tui_list_nav *v, int n) { memset(v, 0, sizeof *v); v->n = n; v->view = 10; } static void test_nav(void) { struct tui_list_nav v; nav_init(&v, 0); CHECK(tui_nav_key(&v, KEY_DOWN, 0, 0, 0, 0) == TUI_NAV_NONE); CHECK(tui_nav_key(&v, KEY_F(5), 0, 0, 0, 0) == -2); CHECK(tui_nav_key(&v, TUI_KEY_CTRL_N, 1, 0, 0, 0) == -4); CHECK(tui_nav_key(&v, TUI_KEY_CTRL_N, 0, 0, 0, 0) == TUI_NAV_NONE); CHECK(tui_nav_key(&v, 'q', 0, 0, 0, 0) == -1); CHECK(tui_nav_key(&v, 27, 0, 0, 0, 0) == -1); nav_init(&v, 5); CHECK(tui_nav_key(&v, KEY_UP, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 0); CHECK(tui_nav_key(&v, KEY_DOWN, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 1); CHECK(tui_nav_key(&v, KEY_END, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 4); CHECK(tui_nav_key(&v, KEY_DOWN, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 4); CHECK(tui_nav_key(&v, KEY_HOME, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 0); CHECK(tui_nav_key(&v, KEY_NPAGE, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 4); CHECK(tui_nav_key(&v, KEY_PPAGE, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 0); /* digits jump in lists, activate in menus */ CHECK(tui_nav_key(&v, '3', 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 2); CHECK(tui_nav_key(&v, '3', 0, 0, 0, 1) == 2); CHECK(tui_nav_key(&v, '9', 0, 0, 0, 0) == TUI_NAV_NONE); /* out of range */ CHECK(tui_nav_key(&v, '\n', 0, 0, 0, 0) == v.sel); CHECK(tui_nav_key(&v, KEY_F(5), 0, 0, 0, 0) == -2); CHECK(tui_nav_key(&v, TUI_KEY_CTRL_N, 1, 0, 0, 0) == -4); CHECK(tui_nav_key(&v, TUI_KEY_CTRL_N, 0, 0, 0, 0) == TUI_NAV_NONE); CHECK(tui_nav_key(&v, 'd', 0, 1, 0, 0) == -6); CHECK(tui_nav_key(&v, 'd', 0, 0, 0, 0) == TUI_NAV_NONE); CHECK(tui_nav_key(&v, 1, 0, 0, 1, 0) == -7); /* ^A */ CHECK(tui_nav_key(&v, 'q', 0, 0, 0, 0) == -1); CHECK(tui_nav_key(&v, 27, 0, 0, 0, 0) == -1); /* g goto takes multi-digit input and Enter only closes the prompt */ nav_init(&v, 20); CHECK(tui_nav_key(&v, 'g', 0, 0, 0, 0) == TUI_NAV_NONE && v.goto_active); CHECK(tui_nav_key(&v, '1', 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 0); CHECK(tui_nav_key(&v, '2', 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 11); CHECK(tui_nav_key(&v, KEY_BACKSPACE, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 0); CHECK(tui_nav_key(&v, '\n', 0, 0, 0, 0) == TUI_NAV_NONE && !v.goto_active); /* the view follows the selection */ nav_init(&v, 100); v.view = 10; tui_nav_key(&v, KEY_END, 0, 0, 0, 0); CHECK(v.top == 90); } static void test_styles(void) { /* without colour every style falls back to attributes */ CHECK(tui_style_attrs(TUI_TITLE, 0, 0) == (int)A_BOLD); CHECK(tui_style_attrs(TUI_HEADING, 0, 0) == (int)A_BOLD); CHECK(tui_style_attrs(TUI_SUBHEADING, 0, 0) == (int)A_BOLD); CHECK(tui_style_attrs(TUI_DIM, 0, 0) == (int)A_DIM); CHECK(tui_style_attrs(TUI_STATUS, 0, 0) == (int)A_DIM); CHECK(tui_style_attrs(TUI_RULE, 0, 0) == (int)A_DIM); CHECK(tui_style_attrs(TUI_POS, 0, 0) == 0); CHECK(tui_style_attrs(TUI_NEG, 0, 0) == 0); /* NO_COLOR forces the same fallback even when colours are available */ CHECK(tui_style_attrs(TUI_POS, 1, 1) == tui_style_attrs(TUI_POS, 0, 0)); CHECK(tui_style_attrs(TUI_RULE, 1, 1) == (int)A_DIM); CHECK(tui_style_attrs(TUI_TITLE, 1, 1) == (int)A_BOLD); /* with colour the amount styles get distinct colour pairs */ int pos = tui_style_attrs(TUI_POS, 1, 0); int neg = tui_style_attrs(TUI_NEG, 1, 0); CHECK((pos & (int)A_COLOR) != 0); CHECK((neg & (int)A_COLOR) != 0); CHECK((pos & (int)A_COLOR) != (neg & (int)A_COLOR)); CHECK((tui_style_attrs(TUI_HEADING, 1, 0) & (int)A_BOLD) != 0); CHECK((tui_style_attrs(TUI_STATUS, 1, 0) & (int)A_DIM) != 0); } static void test_markup(void) { int st = 123; const char *s = tui_markup("vanlig text", &st); CHECK(strcmp(s, "vanlig text") == 0 && st == -1); s = tui_markup("\001Rubrik", &st); CHECK(strcmp(s, "Rubrik") == 0 && st == TUI_HEADING); s = tui_markup("\002dimmad rad", &st); CHECK(strcmp(s, "dimmad rad") == 0 && st == TUI_DIM); s = tui_markup("\003----------------", &st); CHECK(strcmp(s, "----------------") == 0 && st == TUI_RULE); s = tui_markup("", &st); CHECK(s && *s == '\0' && st == -1); s = tui_markup(NULL, &st); CHECK(s && *s == '\0' && st == -1); /* only a leading marker counts; the marker may be NULL-checked away */ s = tui_markup("a\001b", &st); CHECK(strcmp(s, "a\001b") == 0 && st == -1); s = tui_markup("\004text", &st); CHECK(strcmp(s, "\004text") == 0 && st == -1); s = tui_markup("\001x", NULL); CHECK(strcmp(s, "x") == 0); } static const unsigned char SECTION_FLAGS[5] = { 0, 1, 1, 0, 1 }; static void test_nav_sections(void) { struct tui_list_nav v; nav_init(&v, 5); v.selectable = SECTION_FLAGS; v.sel = 0; /* a header: snaps to the first selectable row */ CHECK(tui_nav_key(&v, KEY_DOWN, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 2); CHECK(tui_nav_key(&v, KEY_DOWN, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 4); CHECK(tui_nav_key(&v, KEY_DOWN, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 4); CHECK(tui_nav_key(&v, KEY_UP, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 2); CHECK(tui_nav_key(&v, KEY_HOME, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 1); CHECK(tui_nav_key(&v, KEY_END, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 4); /* digits and goto count selectable rows only */ CHECK(tui_nav_key(&v, '1', 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 1); CHECK(tui_nav_key(&v, '2', 0, 0, 0, 1) == 2 && v.sel == 2); CHECK(tui_nav_key(&v, '3', 0, 0, 0, 1) == 4 && v.sel == 4); CHECK(tui_nav_key(&v, '9', 0, 0, 0, 1) == TUI_NAV_NONE && v.sel == 4); v.sel = 1; CHECK(tui_nav_key(&v, 'g', 0, 0, 0, 0) == TUI_NAV_NONE && v.goto_active); CHECK(tui_nav_key(&v, '2', 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 2); CHECK(tui_nav_key(&v, KEY_BACKSPACE, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 2); CHECK(tui_nav_key(&v, '\n', 0, 0, 0, 0) == TUI_NAV_NONE && !v.goto_active); /* paging lands on selectable rows */ nav_init(&v, 5); v.selectable = SECTION_FLAGS; v.view = 2; CHECK(tui_nav_key(&v, KEY_NPAGE, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 4); CHECK(tui_nav_key(&v, KEY_PPAGE, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 2); } static void test_form_nav(void) { CHECK(TUI_NAV_NONE != TUI_FORM_SUBMIT); CHECK(tui_form_next(1, 3, 'x') == TUI_NAV_NONE); CHECK(tui_form_next(2, 5, KEY_HOME) == 0); CHECK(tui_form_next(2, 5, KEY_END) == 4); CHECK(tui_form_next(0, 5, KEY_NPAGE) == 4); CHECK(tui_form_next(0, 25, KEY_NPAGE) == 10); CHECK(tui_form_next(12, 25, KEY_PPAGE) == 2); CHECK(tui_form_next(3, 25, KEY_PPAGE) == 0); CHECK(tui_form_next(3, 25, KEY_HOME) == 0); CHECK(tui_form_next(0, 3, KEY_UP) == TUI_NAV_NONE); CHECK(tui_form_next(0, 3, KEY_DOWN) == 1); CHECK(tui_form_next(2, 3, KEY_DOWN) == TUI_NAV_NONE); CHECK(tui_form_next(1, 3, KEY_F(5)) == TUI_FORM_REFRESH); CHECK(tui_form_next(1, 3, TUI_KEY_CTRL_ENTER) == TUI_FORM_SUBMIT); CHECK(tui_form_next(1, 3, KEY_F(9)) == TUI_FORM_SUBMIT); CHECK(tui_form_next(1, 3, 27) == TUI_FORM_BACK); CHECK(tui_form_next(1, 3, 'q') == TUI_FORM_BACK); CHECK(tui_form_next(1, 3, 'x') == TUI_NAV_NONE); } /* Fields 0..2, then six action rows: 0 heading, 1/2 selectable, 3 disabled but selectable, 4 heading, 5 selectable. Focus 3..8 is actions 0..5. */ static const struct tui_form_action ACTS[6] = { { "Bokslut bokfört: nej", -1, NULL }, { "Årsredovisning (K2)", 1, NULL }, { "Inkomstdeklaration (INK2/SRU)", 1, NULL }, { "Bokför bokslut", 0, "året är stängt" }, { "rubrik igen", -1, NULL }, { "Bokslutsplan (torrkörning)", 1, NULL }, }; static void test_form_actions(void) { const int nf = 3; CHECK(TUI_FORM_ACTION > 0); CHECK(TUI_FORM_ACTION != TUI_FORM_BACK); CHECK(TUI_FORM_ACTION != TUI_FORM_REFRESH); CHECK(TUI_FORM_ACTION != TUI_FORM_SUBMIT); CHECK(TUI_FORM_ACTION + 5 > nf); /* initial focus: first field, or the first selectable action */ CHECK(tui_form_act_first(nf, ACTS, 6) == 0); CHECK(tui_form_act_first(0, ACTS, 6) == 1); static const struct tui_form_action HEADS[2] = { { "rubrik", -1, NULL }, { "rubrik", -1, NULL }, }; CHECK(tui_form_act_first(0, HEADS, 2) == -1); CHECK(tui_form_act_first(4, ACTS, 6) == 0); /* the ring runs fields -> actions -> back, skipping headings */ CHECK(tui_form_act_step(0, nf, ACTS, 6, 1) == 1); CHECK(tui_form_act_step(1, nf, ACTS, 6, 1) == 2); CHECK(tui_form_act_step(2, nf, ACTS, 6, 1) == 4); /* skip focus 3 */ CHECK(tui_form_act_step(4, nf, ACTS, 6, 1) == 5); CHECK(tui_form_act_step(5, nf, ACTS, 6, 1) == 6); /* disabled, selectable */ CHECK(tui_form_act_step(6, nf, ACTS, 6, 1) == 8); /* skip focus 7 */ CHECK(tui_form_act_step(8, nf, ACTS, 6, 1) == 0); /* wrap */ CHECK(tui_form_act_step(0, nf, ACTS, 6, -1) == 8); /* wrap back */ CHECK(tui_form_act_step(8, nf, ACTS, 6, -1) == 6); CHECK(tui_form_act_step(6, nf, ACTS, 6, -1) == 5); CHECK(tui_form_act_step(5, nf, ACTS, 6, -1) == 4); CHECK(tui_form_act_step(4, nf, ACTS, 6, -1) == 2); /* skip focus 3 */ /* an out-of-range focus snaps back into the ring */ CHECK(tui_form_act_step(99, nf, ACTS, 6, 1) == 1); /* keys: Tab/Shift-Tab/arrows walk the ring, Home/End the ends */ CHECK(tui_form_act_key(0, nf, ACTS, 6, '\t') == 1); CHECK(tui_form_act_key(0, nf, ACTS, 6, KEY_BTAB) == 8); CHECK(tui_form_act_key(0, nf, ACTS, 6, KEY_UP) == 8); CHECK(tui_form_act_key(8, nf, ACTS, 6, KEY_DOWN) == 0); CHECK(tui_form_act_key(4, nf, ACTS, 6, KEY_HOME) == 0); CHECK(tui_form_act_key(0, nf, ACTS, 6, KEY_END) == 8); CHECK(tui_form_act_key(0, nf, ACTS, 6, KEY_NPAGE) == 4); CHECK(tui_form_act_key(8, nf, ACTS, 6, KEY_PPAGE) == 4); CHECK(tui_form_act_key(0, nf, ACTS, 6, KEY_F(5)) == TUI_FORM_REFRESH); CHECK(tui_form_act_key(5, nf, ACTS, 6, TUI_KEY_CTRL_ENTER) == TUI_FORM_SUBMIT); CHECK(tui_form_act_key(5, nf, ACTS, 6, 27) == TUI_FORM_BACK); CHECK(tui_form_act_key(5, nf, ACTS, 6, 'x') == TUI_NAV_NONE); /* labels: plain, disabled with reason, heading, fallback reason */ char buf[128]; tui_form_act_label(&ACTS[1], buf, sizeof buf); CHECK(strcmp(buf, "Årsredovisning (K2)") == 0); tui_form_act_label(&ACTS[3], buf, sizeof buf); CHECK(strcmp(buf, "Bokför bokslut (året är stängt)") == 0); tui_form_act_label(&ACTS[0], buf, sizeof buf); CHECK(strcmp(buf, "Bokslut bokfört: nej") == 0); const struct tui_form_action no_reason = { "Stängd", 0, NULL }; tui_form_act_label(&no_reason, buf, sizeof buf); CHECK(strcmp(buf, "Stängd (otillgänglig)") == 0); tui_form_act_label(NULL, buf, sizeof buf); CHECK(buf[0] == '\0'); /* without fields the ring cycles the selectable actions only */ CHECK(tui_form_act_step(0, 0, ACTS, 6, 1) == 1); CHECK(tui_form_act_step(5, 0, ACTS, 6, 1) == 1); /* wraps past the head */ CHECK(tui_form_act_step(1, 0, ACTS, 6, -1) == 5); } static char rt_cells[8][3][64]; static void rt_cb(void *ud, int row, int col, char **buf, size_t *cap) { (void)ud; *buf = rt_cells[row][col]; *cap = sizeof rt_cells[row][col]; } static void rt_info_cb(void *ud, int row, char *buf, size_t n) { (void)ud; snprintf(buf, n, "info%d", row); } static const struct tui_rt_col RT_COLS[3] = { { "A", 2, 8, TUI_RT_EDIT }, { "N", 12, 8, TUI_RT_INFO }, { "B", 22, 8, TUI_RT_EDIT }, }; static int rt_key_cb(void *ud, int ch) { (void)ud; (void)ch; return TUI_HOOK_NONE; } static void rt_footer_cb(void *ud, char *buf, size_t n) { (void)ud; snprintf(buf, n, "footer"); } static void test_rowtable(void) { struct tui_rt t; memset(rt_cells, 0, sizeof rt_cells); tui_rt_init(&t, 1, 8, 3, RT_COLS, 6, rt_cb, rt_info_cb, NULL); CHECK(t.nrows == 1 && t.neditable == 2 && t.row == 0 && t.col == 0); CHECK(t.key == NULL && t.footer == NULL); tui_rt_set_key(&t, rt_key_cb); tui_rt_set_footer(&t, rt_footer_cb); CHECK(t.key == rt_key_cb && t.footer == rt_footer_cb); /* editable index -> actual column index (INFO columns are skipped) */ CHECK(tui_rt_col_of(&t, 0) == 0); CHECK(tui_rt_col_of(&t, 1) == 2); CHECK(tui_rt_blank_row(&t, 0)); CHECK(tui_rt_move(&t, 0, 1) && t.col == 1); CHECK(tui_rt_move(&t, 0, 1) && t.col == 0); CHECK(tui_rt_move(&t, -1, 0) == 0); CHECK(tui_rt_move(&t, 1, 0) == 0); /* Tab walks the row and wraps to the next row */ t.nrows = 3; t.row = 0; t.col = 0; CHECK(tui_rt_tab(&t, 1) && t.row == 0 && t.col == 1); CHECK(tui_rt_tab(&t, 1) && t.row == 1 && t.col == 0); CHECK(tui_rt_tab(&t, -1) && t.row == 0 && t.col == 1); CHECK(tui_rt_tab(&t, -1) && t.row == 0 && t.col == 0); /* with a single editable column every Tab is a row move */ struct tui_rt one; tui_rt_init(&one, 3, 8, 1, RT_COLS, 6, rt_cb, rt_info_cb, NULL); CHECK(one.neditable == 1); CHECK(tui_rt_tab(&one, 1) && one.row == 1 && one.col == 0); CHECK(tui_rt_tab(&one, -1) && one.row == 0 && one.col == 0); /* data in the blank row appends a new blank row */ tui_rt_normalize(&t); t.row = 0; t.col = 0; snprintf(rt_cells[0][0], 64, "1930"); CHECK(tui_rt_normalize(&t) == 2); snprintf(rt_cells[1][0], 64, "x"); CHECK(tui_rt_normalize(&t) == 3); /* two trailing blank rows collapse into one */ t.nrows = 5; CHECK(tui_rt_normalize(&t) == 3); /* ^X clears the row and re-normalizes */ t.row = 1; t.col = 0; CHECK(tui_rt_clear(&t)); CHECK(t.nrows == 2); CHECK(rt_cells[1][0][0] == '\0'); CHECK(tui_rt_clear(&t)); CHECK(t.nrows == 2); /* the second clear only hit the blank row */ t.row = 0; CHECK(tui_rt_clear(&t)); CHECK(t.nrows == 1); tui_rt_clear(&t); CHECK(t.nrows == 1); /* never below one row */ } static void test_rt_fields(void) { struct tui_rt t; memset(rt_cells, 0, sizeof rt_cells); tui_rt_init(&t, 1, 8, 3, RT_COLS, 6, rt_cb, rt_info_cb, NULL); CHECK(t.focus == -1); struct tui_form_field f[2]; memset(f, 0, sizeof f); f[0].label = "Datum"; f[1].label = "Text"; tui_rt_set_fields(&t, f, 2); CHECK(t.fields == f && t.nfields == 2 && t.focus == 0); tui_rt_set_fields(&t, NULL, 0); CHECK(t.fields == NULL && t.nfields == 0 && t.focus == -1); } static void test_rt_focus(void) { int row, col; /* fields 0..2, then a 2-row table with 3 editable cells each */ row = 0; col = 0; CHECK(tui_rt_focus_step(0, 3, &row, &col, 2, 3, 1) == 1); CHECK(tui_rt_focus_step(1, 3, &row, &col, 2, 3, 1) == 2); CHECK(tui_rt_focus_step(2, 3, &row, &col, 2, 3, 1) == -1); CHECK(row == 0 && col == 0); CHECK(tui_rt_focus_step(-1, 3, &row, &col, 2, 3, 1) == -1); CHECK(row == 0 && col == 1); CHECK(tui_rt_focus_step(-1, 3, &row, &col, 2, 3, 1) == -1); CHECK(row == 0 && col == 2); CHECK(tui_rt_focus_step(-1, 3, &row, &col, 2, 3, 1) == -1); CHECK(row == 1 && col == 0); /* after the last cell Tab wraps back to the first field */ row = 1; col = 2; CHECK(tui_rt_focus_step(-1, 3, &row, &col, 2, 3, 1) == 0); /* Shift-Tab from the first field lands on the last cell */ CHECK(tui_rt_focus_step(0, 3, &row, &col, 2, 3, -1) == -1); CHECK(row == 1 && col == 2); CHECK(tui_rt_focus_step(-1, 3, &row, &col, 2, 3, -1) == -1); CHECK(row == 1 && col == 1); /* Shift-Tab from the first cell wraps back to the last field */ row = 0; col = 0; CHECK(tui_rt_focus_step(-1, 3, &row, &col, 2, 3, -1) == 2); /* without fields the table still cycles through its cells */ row = 0; col = 0; CHECK(tui_rt_focus_step(-1, 0, &row, &col, 2, 3, -1) == -1); CHECK(row == 1 && col == 2); CHECK(tui_rt_focus_step(-1, 0, &row, &col, 2, 3, 1) == -1); CHECK(row == 0 && col == 0); /* an empty table cycles only through the fields */ row = 0; col = 0; CHECK(tui_rt_focus_step(2, 3, &row, &col, 0, 0, 1) == 0); CHECK(tui_rt_focus_step(0, 3, &row, &col, 0, 0, -1) == 2); } static void test_rt_footer(void) { CHECK(tui_rt_footer_row(24) == 22); CHECK(tui_rt_footer_row(4) == 2); CHECK(tui_rt_footer_row(3) == 2); CHECK(tui_rt_footer_row(2) == 2); char f[64]; snprintf(f, sizeof f, "Bilagor: a\nSumma 0,00 I BALANS"); tui_rt_footer_flatten(f); CHECK(strcmp(f, "Bilagor: a Summa 0,00 I BALANS") == 0); snprintf(f, sizeof f, "en enda rad"); tui_rt_footer_flatten(f); CHECK(strcmp(f, "en enda rad") == 0); f[0] = '\0'; tui_rt_footer_flatten(f); CHECK(f[0] == '\0'); } int main(void) { test_disp_width(); test_formats(); test_parse_kr(); test_ledit(); test_field_fresh(); test_date_field(); test_styles(); test_markup(); test_nav(); test_nav_sections(); test_form_nav(); test_form_actions(); test_rowtable(); test_rt_fields(); test_rt_focus(); test_rt_footer(); printf("test_tui: %d checks, %d failures\n", checks, failures); return failures ? 1 : 0; }