1package ui
2
3import (
4 "testing"
5
6 "github.com/bnema/dumber/internal/domain/entity"
7 "github.com/bnema/dumber/internal/ui/input"
8 "github.com/stretchr/testify/require"
9)
10
11func TestLegendColumnCount(t *testing.T) {
12 for _, tc := range []struct {
13 width, want int
14 }{
15 {0, 1},
16 {200, 1},
17 {legendPanelPadding + legendMinColumnWidth, 1},
18 {legendPanelPadding + legendMinColumnWidth + legendColumnGap + legendMinColumnWidth - 1, 1},
19 {legendPanelPadding + 2*legendMinColumnWidth + legendColumnGap, 2},
20 {legendPanelPadding + 3*legendMinColumnWidth + 2*legendColumnGap, 3},
21 {legendPanelPadding + 4*legendMinColumnWidth + 3*legendColumnGap, 4},
22 {legendMaxWidth, 4},
23 {5000, legendMaxColumns},
24 } {
25 require.Equal(t, tc.want, legendColumnCount(tc.width), "width %d", tc.width)
26 }
27}
28
29func TestLegendColumnCountIsMonotonic(t *testing.T) {
30 previous := 0
31 for width := 0; width <= 2000; width++ {
32 got := legendColumnCount(width)
33 require.GreaterOrEqual(t, got, previous, "width %d", width)
34 require.GreaterOrEqual(t, got, 1)
35 require.LessOrEqual(t, got, legendMaxColumns)
36 previous = got
37 }
38}
39
40func TestLegendBandWidthIsCappedAndNeverFullWidth(t *testing.T) {
41 width, columns := legendBandWidth(2400, 10)
42 require.Equal(t, legendMaxWidth, width)
43 require.Equal(t, 4, columns)
44
45 width, columns = legendBandWidth(800, 10)
46 require.Equal(t, 800, width)
47 require.Equal(t, legendColumnCount(800), columns)
48
49 width, columns = legendBandWidth(300, 10)
50 require.Equal(t, 300, width)
51 require.Equal(t, 1, columns)
52}
53
54func TestLegendBandWidthShrinksForFewGroups(t *testing.T) {
55 width, columns := legendBandWidth(2400, 2)
56 require.Equal(t, 2, columns)
57 require.Less(t, width, legendMaxWidth)
58 require.GreaterOrEqual(t, legendColumnCount(width), columns, "the shrunk band still fits its columns")
59
60 width, columns = legendBandWidth(2400, 1)
61 require.Equal(t, 1, columns)
62 require.GreaterOrEqual(t, width, legendMinColumnWidth+legendPanelPadding)
63}
64
65func TestLegendBandWidthWithoutRoom(t *testing.T) {
66 width, columns := legendBandWidth(0, 5)
67 require.Zero(t, width)
68 require.Zero(t, columns)
69 width, columns = legendBandWidth(500, 0)
70 require.Zero(t, width)
71 require.Zero(t, columns)
72}
73
74func TestLegendBodyMaxHeight(t *testing.T) {
75 require.Equal(t, 1000*45/100-legendChromeHeight, legendBodyMaxHeight(1000))
76 require.Equal(t, legendMinBodyHeight, legendBodyMaxHeight(100), "a tiny window still shows a few rows")
77}
78
79// columnsOf flattens a distribution back into group order.
80func flattenDistribution(columns [][]int) []int {
81 var out []int
82 for _, column := range columns {
83 out = append(out, column...)
84 }
85 return out
86}
87
88func tallest(heights []int, columns [][]int) int {
89 best := 0
90 for _, column := range columns {
91 sum := 0
92 for _, i := range column {
93 sum += heights[i]
94 }
95 best = max(best, sum)
96 }
97 return best
98}
99
100func TestDistributeLegendGroupsKeepsOrderAndNeverSplitsGroups(t *testing.T) {
101 heights := []int{4, 1, 7, 3, 2, 9, 2, 5, 1, 3}
102 want := make([]int, len(heights))
103 for i := range want {
104 want[i] = i
105 }
106 for columns := 1; columns <= legendMaxColumns; columns++ {
107 got := distributeLegendGroups(heights, columns)
108 require.Len(t, got, columns)
109 require.Equal(t, want, flattenDistribution(got), "columns=%d keeps the fixed group order, each group once", columns)
110 for _, column := range got {
111 require.NotEmpty(t, column, "no empty column")
112 }
113 }
114}
115
116func TestDistributeLegendGroupsBalancesColumnHeights(t *testing.T) {
117 // One 8-row group next to four 1-row groups, in two columns: the tall
118 // group alone is the tallest column, the rest share the other.
119 heights := []int{8, 2, 2, 2, 2}
120 got := distributeLegendGroups(heights, 2)
121 require.Equal(t, [][]int{{0}, {1, 2, 3, 4}}, got)
122 require.Equal(t, 8, tallest(heights, got))
123
124 // Even groups split evenly.
125 got = distributeLegendGroups([]int{3, 3, 3, 3}, 2)
126 require.Equal(t, [][]int{{0, 1}, {2, 3}}, got)
127}
128
129func TestDistributeLegendGroupsMinimizesTallestColumn(t *testing.T) {
130 heights := []int{5, 1, 1, 1, 6, 2, 2, 4}
131 for columns := 1; columns <= 4; columns++ {
132 got := distributeLegendGroups(heights, columns)
133 require.Equal(t, bruteForceTallest(heights, columns), tallest(heights, got), "columns=%d", columns)
134 }
135}
136
137// bruteForceTallest tries every way to cut heights into columns runs.
138func bruteForceTallest(heights []int, columns int) int {
139 if columns == 1 {
140 sum := 0
141 for _, h := range heights {
142 sum += h
143 }
144 return sum
145 }
146 best := -1
147 sum := 0
148 for cut := 1; cut <= len(heights)-columns+1; cut++ {
149 sum += heights[cut-1]
150 rest := bruteForceTallest(heights[cut:], columns-1)
151 candidate := max(sum, rest)
152 if best < 0 || candidate < best {
153 best = candidate
154 }
155 }
156 return best
157}
158
159func TestDistributeLegendGroupsIsDeterministic(t *testing.T) {
160 heights := []int{2, 2, 2, 2, 2, 2}
161 first := distributeLegendGroups(heights, 3)
162 for range 20 {
163 require.Equal(t, first, distributeLegendGroups(heights, 3))
164 }
165 require.Equal(t, [][]int{{0, 1}, {2, 3}, {4, 5}}, first)
166}
167
168func TestDistributeLegendGroupsEdgeCases(t *testing.T) {
169 require.Nil(t, distributeLegendGroups(nil, 3))
170 require.Nil(t, distributeLegendGroups([]int{2, 3}, 0))
171 // More columns than groups: one group per column.
172 require.Equal(t, [][]int{{0}, {1}}, distributeLegendGroups([]int{2, 3}, 4))
173 require.Equal(t, [][]int{{0}}, distributeLegendGroups([]int{7}, 3))
174 require.Equal(t, [][]int{{0, 1, 2}}, distributeLegendGroups([]int{1, 1, 1}, 1))
175}
176
177func TestBuildLegendGroupsOrdersGroupsAndRows(t *testing.T) {
178 actions := map[string]entity.ActionBinding{
179 "enter-tab-mode": {Keys: []string{"t"}, Desc: "Tab mode"},
180 "split-right": {Keys: []string{"r"}},
181 "split-left": {Keys: []string{"l"}},
182 "focus-up": {Keys: []string{"k"}},
183 "confirm": {Keys: []string{"enter"}},
184 "close-pane": {Keys: []string{"x"}},
185 "unbound": {},
186 "resize-increase": {Keys: []string{"+"}},
187 }
188 groups := buildLegendGroups(input.ModePane, actions)
189 var names []string
190 for _, group := range groups {
191 names = append(names, group.Name)
192 }
193 require.Equal(t, []string{"SPLIT", "FOCUS", "RESIZE", "MANAGE", "MODES", "EXIT"}, names)
194 require.Equal(t, "split left", groups[0].Rows[0].Desc, "rows are sorted and fall back to the action name")
195 require.Equal(t, "split-left", groups[0].Rows[0].Action)
196 require.Equal(t, "Tab mode", groups[4].Rows[0].Desc)
197 require.Equal(t, []int{3, 2, 2, 2, 2, 2}, legendGroupHeights(groups))
198}
199
200func TestBuildLegendGroupsEmpty(t *testing.T) {
201 require.Empty(t, buildLegendGroups(input.ModeVim, nil))
202 require.Empty(t, buildLegendGroups(input.ModeVim, map[string]entity.ActionBinding{"visual": {}}))
203}
204
205func TestModeToasterVisibility(t *testing.T) {
206 on := entity.WorkspaceStylingConfig{ModeIndicatorToasterEnabled: true, ModeLegend: "delay"}
207 require.True(t, modeToasterVisible(input.ModeVim, on))
208 require.False(t, modeToasterVisible(input.ModeNormal, on))
209
210 // The indicator preference off, legend on: the pill is the legend's header.
211 headerOnly := entity.WorkspaceStylingConfig{ModeLegend: "always"}
212 require.True(t, modeToasterVisible(input.ModePane, headerOnly))
213
214 // Both off: nothing.
215 require.False(t, modeToasterVisible(input.ModePane, entity.WorkspaceStylingConfig{ModeLegend: "off"}))
216 // Legend off, indicator on: plain pill.
217 require.True(t, modeToasterVisible(input.ModePane, entity.WorkspaceStylingConfig{ModeIndicatorToasterEnabled: true, ModeLegend: "off"}))
218}
219
220func TestLegendBodyShown(t *testing.T) {
221 require.True(t, legendBodyShown(true, false, false))
222 require.False(t, legendBodyShown(false, false, false), "show delay not elapsed")
223 require.False(t, legendBodyShown(true, true, false), "collapsed")
224 require.False(t, legendBodyShown(true, false, true), "suspended by a page interaction")
225}
226
227func TestNextModeLegendCollapsed(t *testing.T) {
228 require.True(t, nextModeLegendCollapsed(false))
229 require.False(t, nextModeLegendCollapsed(true))
230}