summaryrefslogtreecommitdiff
path: root/internal/tui/common/table_test.go
blob: fa104d1803c6cdd26cc0724ef599395d674bb2e5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
package common

import (
	"strings"
	"testing"

	"charm.land/lipgloss/v2"
)

// TestRenderTableHeaderProducesColumns verifies that RenderTableHeader creates a
// styled row containing each column title padded to its declared width.
func TestRenderTableHeaderProducesColumns(t *testing.T) {
	cols := []TableColumn{
		{Title: "PID", Width: 8},
		{Title: "COMM", Width: 12},
	}
	out := RenderTableHeader(cols)
	if !strings.Contains(out, "PID") {
		t.Fatalf("expected header to contain PID, got: %q", out)
	}
	if !strings.Contains(out, "COMM") {
		t.Fatalf("expected header to contain COMM, got: %q", out)
	}
}

// TestRenderTableRowSelectedAndUnselected verifies that selected rows include
// highlighted style and unselected rows use the base style.
func TestRenderTableRowSelectedAndUnselected(t *testing.T) {
	cols := []TableColumn{
		{Title: "SYS", Width: 10},
		{Title: "CNT", Width: 6},
	}
	cells := []string{"openat", "42"}
	base := lipgloss.NewStyle()

	unselected := RenderTableRow(cols, cells, false, 0, base)
	if !strings.Contains(unselected, "openat") {
		t.Fatalf("unselected row missing cell content, got: %q", unselected)
	}

	// Selected row: all cells present, no base style rendering branch.
	selected := RenderTableRow(cols, cells, true, 1, base)
	if !strings.Contains(selected, "openat") {
		t.Fatalf("selected row missing first cell, got: %q", selected)
	}
	if !strings.Contains(selected, "42") {
		t.Fatalf("selected row missing second cell, got: %q", selected)
	}
}

// TestRenderTableRowMissingCells verifies that missing cells default to empty
// strings rather than panicking.
func TestRenderTableRowMissingCells(t *testing.T) {
	cols := []TableColumn{
		{Title: "A", Width: 4},
		{Title: "B", Width: 4},
	}
	// Provide only one cell for two columns.
	out := RenderTableRow(cols, []string{"x"}, false, 0, lipgloss.NewStyle())
	if !strings.Contains(out, "x") {
		t.Fatalf("expected cell content x in output, got: %q", out)
	}
}

// TestHandleTableNavigationKeyMovement verifies all supported key strings
// produce the expected row/col changes.
func TestHandleTableNavigationKeyMovement(t *testing.T) {
	cases := []struct {
		key        string
		initRow    int
		initCol    int
		wantRow    int
		wantCol    int
		rowCount   int
		colCount   int
		pageStep   int
		wantResult bool
	}{
		{"down", 0, 0, 1, 0, 5, 3, 1, true},
		{"j", 2, 0, 3, 0, 5, 3, 1, true},
		{"up", 2, 0, 1, 0, 5, 3, 1, true},
		{"k", 1, 0, 0, 0, 5, 3, 1, true},
		{"right", 0, 0, 0, 1, 5, 3, 1, true},
		{"l", 0, 1, 0, 2, 5, 3, 1, true},
		{"left", 0, 2, 0, 1, 5, 3, 1, true},
		{"h", 0, 1, 0, 0, 5, 3, 1, true},
		{"g", 3, 1, 0, 1, 5, 3, 1, true},
		{"G", 0, 1, 4, 1, 5, 3, 1, true},
		{"pgup", 4, 0, 2, 0, 5, 3, 2, true},
		{"pageup", 4, 0, 2, 0, 5, 3, 2, true},
		{"pgdown", 0, 0, 2, 0, 5, 3, 2, true},
		{"pgdn", 0, 0, 2, 0, 5, 3, 2, true},
		{"pagedown", 0, 0, 2, 0, 5, 3, 2, true},
		{"x", 0, 0, 0, 0, 5, 3, 1, false},
	}

	for _, tc := range cases {
		row, col := tc.initRow, tc.initCol
		got := HandleTableNavigationKey(tc.key, &row, &col, tc.rowCount, tc.colCount, tc.pageStep)
		if got != tc.wantResult {
			t.Errorf("key=%q: handled=%v, want %v", tc.key, got, tc.wantResult)
		}
		if tc.wantResult && (row != tc.wantRow || col != tc.wantCol) {
			t.Errorf("key=%q: row=%d col=%d, want row=%d col=%d",
				tc.key, row, col, tc.wantRow, tc.wantCol)
		}
	}
}

// TestHandleTableNavigationKeyZeroPageStep verifies that a zero page step is
// treated as 1 to avoid staying in place.
func TestHandleTableNavigationKeyZeroPageStep(t *testing.T) {
	row, col := 3, 0
	HandleTableNavigationKey("pgup", &row, &col, 5, 3, 0)
	if row != 2 {
		t.Fatalf("expected row=2 after pgup with pageStep=0 (clamped to 1), got %d", row)
	}
}

// TestVisibleTableWindowEdgeCases verifies boundary conditions for the visible
// window calculation.
func TestVisibleTableWindowEdgeCases(t *testing.T) {
	// Empty row set: returns (0,0).
	s, e := VisibleTableWindow(0, 0, 10)
	if s != 0 || e != 0 {
		t.Fatalf("empty rows: want (0,0), got (%d,%d)", s, e)
	}

	// All rows fit in the visible window.
	s, e = VisibleTableWindow(2, 5, 10)
	if s != 0 || e != 5 {
		t.Fatalf("all fit: want (0,5), got (%d,%d)", s, e)
	}

	// Selection near start.
	s, e = VisibleTableWindow(0, 10, 4)
	if s != 0 || e != 4 {
		t.Fatalf("start: want (0,4), got (%d,%d)", s, e)
	}

	// Selection near end.
	s, e = VisibleTableWindow(9, 10, 4)
	if s != 6 || e != 10 {
		t.Fatalf("end: want (6,10), got (%d,%d)", s, e)
	}

	// Middle selection.
	s, e = VisibleTableWindow(5, 10, 4)
	if s != 3 || e != 7 {
		t.Fatalf("middle: want (3,7), got (%d,%d)", s, e)
	}
}

// TestClampTableCol verifies ClampTableCol constrains within [0, colCount-1].
func TestClampTableCol(t *testing.T) {
	if got := ClampTableCol(-1, 5); got != 0 {
		t.Fatalf("ClampTableCol(-1,5) = %d, want 0", got)
	}
	if got := ClampTableCol(10, 5); got != 4 {
		t.Fatalf("ClampTableCol(10,5) = %d, want 4", got)
	}
	if got := ClampTableCol(2, 5); got != 2 {
		t.Fatalf("ClampTableCol(2,5) = %d, want 2", got)
	}
	if got := ClampTableCol(0, 0); got != 0 {
		t.Fatalf("ClampTableCol(0,0) = %d, want 0", got)
	}
}

// TestRenderTableCellTruncation verifies that cells wider than their column are
// truncated with an ellipsis.
func TestRenderTableCellTruncation(t *testing.T) {
	// renderTableCell is unexported; exercise it via RenderTableHeader.
	cols := []TableColumn{{Title: "ABCDEFGHIJ", Width: 6}}
	out := RenderTableHeader(cols)
	// The title should be truncated to fit within width=6: "ABC..."
	if !strings.Contains(out, "ABC...") {
		t.Fatalf("expected truncated header ABC..., got: %q", out)
	}
}

// TestRenderTableCellZeroWidth verifies that a zero-width column renders an
// empty string without panicking.
func TestRenderTableCellZeroWidth(t *testing.T) {
	cols := []TableColumn{{Title: "X", Width: 0}}
	out := RenderTableHeader(cols)
	// Expect nothing meaningful — just no panic and no content.
	_ = out
}

// TestSanitizeAndTruncateCellEmbeddedNewlines verifies that embedded newlines
// and tabs in cell values are replaced by spaces.
func TestSanitizeAndTruncateCellEmbeddedNewlines(t *testing.T) {
	cols := []TableColumn{{Title: "A\nB\tC", Width: 10}}
	out := RenderTableHeader(cols)
	if strings.Contains(out, "\n") || strings.Contains(out, "\t") {
		t.Fatalf("expected newlines and tabs stripped from cell, got: %q", out)
	}
}

// TestPickerShortHelpReturnsThreeBindings verifies that PickerShortHelp from
// the KeyMap returns three entries (Enter, Refresh, Esc).
func TestPickerShortHelpReturnsThreeBindings(t *testing.T) {
	keys := DefaultKeyMap()
	bindings := keys.PickerShortHelp()
	if len(bindings) != 3 {
		t.Fatalf("PickerShortHelp len = %d, want 3", len(bindings))
	}
}