v0.31.0 bf0756b

modal_sizing_test.go RawHistoryFolder

1package component
2
3import (
4	"testing"
5
6	layoutmocks "github.com/bnema/dumber/internal/ui/layout/mocks"
7	"github.com/stretchr/testify/assert"
8)
9
10func TestCalculateModalDimensions_NilParent(t *testing.T) {
11	cfg := ModalSizeConfig{
12		WidthPct:       0.6,
13		MaxWidth:       600,
14		TopMarginPct:   0.15,
15		FallbackWidth:  800,
16		FallbackHeight: 600,
17	}
18
19	width, marginTop := CalculateModalDimensions(nil, cfg)
20
21	// Should use fallback values
22	expectedWidth := int(float64(cfg.FallbackWidth) * cfg.WidthPct)
23	expectedMargin := int(float64(cfg.FallbackHeight) * cfg.TopMarginPct)
24
25	assert.Equal(t, expectedWidth, width)
26	assert.Equal(t, expectedMargin, marginTop)
27}
28
29func TestCalculateModalDimensions_ZeroAllocatedSize(t *testing.T) {
30	cfg := ModalSizeConfig{
31		WidthPct:       0.5,
32		MaxWidth:       500,
33		TopMarginPct:   0.2,
34		FallbackWidth:  1000,
35		FallbackHeight: 800,
36	}
37
38	parent := layoutmocks.NewMockOverlayWidget(t)
39	parent.EXPECT().GetAllocatedWidth().Return(0)
40	parent.EXPECT().GetAllocatedHeight().Return(0)
41
42	width, marginTop := CalculateModalDimensions(parent, cfg)
43
44	// Should use fallback values when allocated size is 0
45	expectedWidth := int(float64(cfg.FallbackWidth) * cfg.WidthPct)
46	expectedMargin := int(float64(cfg.FallbackHeight) * cfg.TopMarginPct)
47
48	assert.Equal(t, expectedWidth, width)
49	assert.Equal(t, expectedMargin, marginTop)
50}
51
52func TestCalculateModalDimensions_WidthCapped(t *testing.T) {
53	cfg := ModalSizeConfig{
54		WidthPct:       0.8,
55		MaxWidth:       600,
56		TopMarginPct:   0.1,
57		FallbackWidth:  800,
58		FallbackHeight: 600,
59	}
60
61	parent := layoutmocks.NewMockOverlayWidget(t)
62	parent.EXPECT().GetAllocatedWidth().Return(1920) // Large screen
63	parent.EXPECT().GetAllocatedHeight().Return(1080)
64
65	width, marginTop := CalculateModalDimensions(parent, cfg)
66
67	// 0.8 * 1920 = 1536, but capped at MaxWidth 600
68	assert.Equal(t, cfg.MaxWidth, width)
69	assert.Equal(t, int(float64(1080)*cfg.TopMarginPct), marginTop)
70}
71
72func TestCalculateModalDimensions_WidthPercentage(t *testing.T) {
73	cfg := ModalSizeConfig{
74		WidthPct:       0.6,
75		MaxWidth:       1000, // High cap, won't be hit
76		TopMarginPct:   0.15,
77		FallbackWidth:  800,
78		FallbackHeight: 600,
79	}
80
81	parent := layoutmocks.NewMockOverlayWidget(t)
82	parent.EXPECT().GetAllocatedWidth().Return(800)
83	parent.EXPECT().GetAllocatedHeight().Return(600)
84
85	width, marginTop := CalculateModalDimensions(parent, cfg)
86
87	assert.Equal(t, 480, width)
88	assert.Equal(t, 90, marginTop)
89}
90
91func TestCalculateModalDimensions_FixedWidthAndTopMargin(t *testing.T) {
92	cfg := ModalSizeConfig{
93		WidthPct:          0.8,
94		MaxWidth:          600,
95		TopMarginPct:      0.2,
96		FallbackWidth:     1000,
97		FallbackHeight:    800,
98		FixedWidth:        800,
99		FixedTopMargin:    0,
100		UseFixedTopMargin: true,
101	}
102
103	parent := layoutmocks.NewMockOverlayWidget(t)
104	parent.EXPECT().GetAllocatedWidth().Return(1920)
105	parent.EXPECT().GetAllocatedHeight().Return(1080)
106
107	width, marginTop := CalculateModalDimensions(parent, cfg)
108
109	assert.Equal(t, 800, width)
110	assert.Equal(t, 0, marginTop)
111}
112
113func TestResolveModalSizeConfig_MergesDefaultsAndOverrides(t *testing.T) {
114	defaults := OmniboxSizeDefaults
115	override := ModalSizeConfig{
116		FixedWidth:        800,
117		FixedTopMargin:    0,
118		UseFixedTopMargin: true,
119	}
120
121	resolved := ResolveModalSizeConfig(override, defaults)
122
123	assert.InDelta(t, defaults.WidthPct, resolved.WidthPct, 0)
124	assert.Equal(t, defaults.MaxWidth, resolved.MaxWidth)
125	assert.Equal(t, defaults.FallbackWidth, resolved.FallbackWidth)
126	assert.Equal(t, defaults.FallbackHeight, resolved.FallbackHeight)
127	assert.Equal(t, 800, resolved.FixedWidth)
128	assert.True(t, resolved.UseFixedTopMargin)
129	assert.Equal(t, 0, resolved.FixedTopMargin)
130}
131
132func TestScaleValue_DefaultScale(t *testing.T) {
133	// When scale is 0 or negative, should default to 1.0
134	assert.Equal(t, 50, ScaleValue(50, 0))
135	assert.Equal(t, 50, ScaleValue(50, -1.0))
136}
137
138func TestScaleValue_NormalScale(t *testing.T) {
139	assert.Equal(t, 50, ScaleValue(50, 1.0))
140}
141
142func TestScaleValue_Scaling(t *testing.T) {
143	// 1.5x scale
144	assert.Equal(t, 75, ScaleValue(50, 1.5))
145	// 2x scale
146	assert.Equal(t, 100, ScaleValue(50, 2.0))
147	// 1.2x scale (common UI scale)
148	assert.Equal(t, 60, ScaleValue(50, 1.2))
149}
150
151func TestMeasureWidgetHeight_NilWidget(t *testing.T) {
152	height := MeasureWidgetHeight(nil, 100)
153	assert.Equal(t, 0, height)
154}
155
156func TestDefaultRowHeights_Values(t *testing.T) {
157	// Ensure defaults are sensible
158	assert.Positive(t, DefaultRowHeights.Standard)
159	assert.Positive(t, DefaultRowHeights.Compact)
160	assert.Positive(t, DefaultRowHeights.Divider)
161	// Standard should be larger than compact
162	assert.Greater(t, DefaultRowHeights.Standard, DefaultRowHeights.Compact)
163}
164
165func TestOmniboxSizeDefaults_Values(t *testing.T) {
166	assert.InDelta(t, 0.8, OmniboxSizeDefaults.WidthPct, 0.0001)
167	assert.Equal(t, 800, OmniboxSizeDefaults.MaxWidth)
168	assert.InDelta(t, 0.2, OmniboxSizeDefaults.TopMarginPct, 0.0001)
169	assert.Equal(t, 600, OmniboxSizeDefaults.FallbackHeight)
170}
171
172func TestSessionManagerSizeDefaults_Values(t *testing.T) {
173	assert.Greater(t, SessionManagerSizeDefaults.WidthPct, 0.0)
174	assert.LessOrEqual(t, SessionManagerSizeDefaults.WidthPct, 1.0)
175	assert.Positive(t, SessionManagerSizeDefaults.MaxWidth)
176	assert.Greater(t, SessionManagerSizeDefaults.TopMarginPct, 0.0)
177	assert.LessOrEqual(t, SessionManagerSizeDefaults.TopMarginPct, 1.0)
178}
179
180func TestPermissionPopupSizeDefaults_Values(t *testing.T) {
181	assert.Greater(t, PermissionPopupSizeDefaults.WidthPct, 0.0)
182	assert.LessOrEqual(t, PermissionPopupSizeDefaults.WidthPct, 1.0)
183	assert.Positive(t, PermissionPopupSizeDefaults.MaxWidth)
184	assert.Greater(t, PermissionPopupSizeDefaults.TopMarginPct, 0.0)
185	assert.LessOrEqual(t, PermissionPopupSizeDefaults.TopMarginPct, 1.0)
186	assert.Equal(t, 300, PermissionPopupSizeDefaults.FallbackHeight)
187}
188
189func TestListDisplayDefaults_Values(t *testing.T) {
190	assert.Positive(t, OmniboxListDefaults.MaxVisibleRows)
191	assert.Positive(t, OmniboxListDefaults.MaxResults)
192	assert.Positive(t, SessionManagerListDefaults.MaxVisibleRows)
193	assert.Positive(t, SessionManagerListDefaults.MaxResults)
194}
195
196func TestEffectiveMaxRows(t *testing.T) {
197	sizeCfg := ModalSizeConfig{TopMarginPct: 0.2}
198	defaults := ListDisplayDefaults{
199		MaxVisibleRows:      10,
200		MaxResults:          10,
201		SmallMaxVisibleRows: 5,
202	}
203
204	tests := []struct {
205		name         string
206		parentHeight int
207		rowHeight    int
208		want         int
209	}{
210		{
211			name:         "tall pane returns MaxVisibleRows",
212			parentHeight: 1200,
213			rowHeight:    72,
214			want:         10,
215		},
216		{
217			name:         "short pane returns SmallMaxVisibleRows",
218			parentHeight: 800,
219			rowHeight:    72,
220			want:         5,
221		},
222		{
223			name:         "zero parentHeight returns MaxVisibleRows",
224			parentHeight: 0,
225			rowHeight:    72,
226			want:         10,
227		},
228		{
229			name:         "zero rowHeight returns MaxVisibleRows",
230			parentHeight: 800,
231			rowHeight:    0,
232			want:         10,
233		},
234		{
235			name:         "negative parentHeight returns MaxVisibleRows",
236			parentHeight: -100,
237			rowHeight:    72,
238			want:         10,
239		},
240		{
241			name:         "exact fit returns MaxVisibleRows",
242			parentHeight: 1150, // available = 1150 - 230 - 144 = 776, needed = 720
243			rowHeight:    72,
244			want:         10,
245		},
246		{
247			name:         "boundary: available equals needed still fits",
248			parentHeight: 1080, // available = 1080 - 216 - 144 = 720, needed = 720 → fits
249			rowHeight:    72,
250			want:         10,
251		},
252		{
253			name:         "boundary: one pixel under triggers small",
254			parentHeight: 1079, // available = 1079 - 215 - 144 = 720, needed = 720 → check
255			rowHeight:    72,
256			want:         10, // int(1079*0.2) = 215, 1079-215-144 = 720 → still fits
257		},
258		{
259			name:         "just below boundary triggers small",
260			parentHeight: 1078, // available = 1078 - 215 - 144 = 719, needed = 720 → doesn't fit
261			rowHeight:    72,
262			want:         5,
263		},
264	}
265
266	for _, tt := range tests {
267		t.Run(tt.name, func(t *testing.T) {
268			got := EffectiveMaxRows(tt.parentHeight, tt.rowHeight, sizeCfg, defaults)
269			assert.Equal(t, tt.want, got)
270		})
271	}
272}
273
274func TestEffectiveMaxRows_NoAdaptation(t *testing.T) {
275	sizeCfg := ModalSizeConfig{TopMarginPct: 0.2}
276	defaults := ListDisplayDefaults{
277		MaxVisibleRows:      10,
278		MaxResults:          10,
279		SmallMaxVisibleRows: 0, // adaptation disabled
280	}
281
282	// Even with a short pane, should return MaxVisibleRows when SmallMaxVisibleRows is 0
283	got := EffectiveMaxRows(500, 72, sizeCfg, defaults)
284	assert.Equal(t, 10, got)
285}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close