modal_sizing_test.go RawHistoryFolder

1package component
2
3import (
4	"math"
5	"testing"
6
7	layoutmocks "github.com/bnema/dumber/internal/ui/layout/mocks"
8	"github.com/stretchr/testify/assert"
9)
10
11func TestCalculateModalDimensions_NilParent(t *testing.T) {
12	cfg := ModalSizeConfig{
13		WidthPct:       0.6,
14		MaxWidth:       600,
15		TopMarginPct:   0.15,
16		FallbackWidth:  800,
17		FallbackHeight: 600,
18	}
19
20	width, marginTop := CalculateModalDimensions(nil, cfg)
21
22	// Should use fallback values
23	expectedWidth := int(float64(cfg.FallbackWidth) * cfg.WidthPct)
24	expectedMargin := int(float64(cfg.FallbackHeight) * cfg.TopMarginPct)
25
26	assert.Equal(t, expectedWidth, width)
27	assert.Equal(t, expectedMargin, marginTop)
28}
29
30func TestCalculateModalDimensions_ZeroAllocatedSize(t *testing.T) {
31	cfg := ModalSizeConfig{
32		WidthPct:       0.5,
33		MaxWidth:       500,
34		TopMarginPct:   0.2,
35		FallbackWidth:  1000,
36		FallbackHeight: 800,
37	}
38
39	parent := layoutmocks.NewMockOverlayWidget(t)
40	parent.EXPECT().GetAllocatedWidth().Return(0)
41	parent.EXPECT().GetAllocatedHeight().Return(0)
42
43	width, marginTop := CalculateModalDimensions(parent, cfg)
44
45	// Should use fallback values when allocated size is 0
46	expectedWidth := int(float64(cfg.FallbackWidth) * cfg.WidthPct)
47	expectedMargin := int(float64(cfg.FallbackHeight) * cfg.TopMarginPct)
48
49	assert.Equal(t, expectedWidth, width)
50	assert.Equal(t, expectedMargin, marginTop)
51}
52
53func TestCalculateModalDimensions_WidthCapped(t *testing.T) {
54	cfg := ModalSizeConfig{
55		WidthPct:       0.8,
56		MaxWidth:       600,
57		TopMarginPct:   0.1,
58		FallbackWidth:  800,
59		FallbackHeight: 600,
60	}
61
62	parent := layoutmocks.NewMockOverlayWidget(t)
63	parent.EXPECT().GetAllocatedWidth().Return(1920) // Large screen
64	parent.EXPECT().GetAllocatedHeight().Return(1080)
65
66	width, marginTop := CalculateModalDimensions(parent, cfg)
67
68	// 0.8 * 1920 = 1536, but capped at MaxWidth 600
69	assert.Equal(t, cfg.MaxWidth, width)
70	assert.Equal(t, int(float64(1080)*cfg.TopMarginPct), marginTop)
71}
72
73func TestCalculateModalDimensions_WidthPercentage(t *testing.T) {
74	cfg := ModalSizeConfig{
75		WidthPct:       0.6,
76		MaxWidth:       1000, // High cap, won't be hit
77		TopMarginPct:   0.15,
78		FallbackWidth:  800,
79		FallbackHeight: 600,
80	}
81
82	parent := layoutmocks.NewMockOverlayWidget(t)
83	parent.EXPECT().GetAllocatedWidth().Return(800)
84	parent.EXPECT().GetAllocatedHeight().Return(600)
85
86	width, marginTop := CalculateModalDimensions(parent, cfg)
87
88	assert.Equal(t, 480, width)
89	assert.Equal(t, 90, marginTop)
90}
91
92func TestCalculateModalDimensions_FixedWidthAndTopMargin(t *testing.T) {
93	cfg := ModalSizeConfig{
94		WidthPct:          0.8,
95		MaxWidth:          600,
96		TopMarginPct:      0.2,
97		FallbackWidth:     1000,
98		FallbackHeight:    800,
99		FixedWidth:        800,
100		FixedTopMargin:    0,
101		UseFixedTopMargin: true,
102	}
103
104	parent := layoutmocks.NewMockOverlayWidget(t)
105	parent.EXPECT().GetAllocatedWidth().Return(1920)
106	parent.EXPECT().GetAllocatedHeight().Return(1080)
107
108	width, marginTop := CalculateModalDimensions(parent, cfg)
109
110	assert.Equal(t, 800, width)
111	assert.Equal(t, 0, marginTop)
112}
113
114func TestResolveModalSizeConfig_MergesDefaultsAndOverrides(t *testing.T) {
115	defaults := OmniboxSizeDefaults
116	override := ModalSizeConfig{
117		FixedWidth:        800,
118		FixedTopMargin:    0,
119		UseFixedTopMargin: true,
120	}
121
122	resolved := ResolveModalSizeConfig(override, defaults)
123
124	assert.InDelta(t, defaults.WidthPct, resolved.WidthPct, 0)
125	assert.Equal(t, defaults.MaxWidth, resolved.MaxWidth)
126	assert.Equal(t, defaults.FallbackWidth, resolved.FallbackWidth)
127	assert.Equal(t, defaults.FallbackHeight, resolved.FallbackHeight)
128	assert.Equal(t, 800, resolved.FixedWidth)
129	assert.True(t, resolved.UseFixedTopMargin)
130	assert.Equal(t, 0, resolved.FixedTopMargin)
131}
132
133func TestScaleValue_DefaultScale(t *testing.T) {
134	// When scale is 0 or negative, should default to 1.0
135	assert.Equal(t, 50, ScaleValue(50, 0))
136	assert.Equal(t, 50, ScaleValue(50, -1.0))
137}
138
139func TestScaleValue_NormalScale(t *testing.T) {
140	assert.Equal(t, 50, ScaleValue(50, 1.0))
141}
142
143func TestScaleValue_Scaling(t *testing.T) {
144	// 1.5x scale
145	assert.Equal(t, 75, ScaleValue(50, 1.5))
146	// 2x scale
147	assert.Equal(t, 100, ScaleValue(50, 2.0))
148	// 1.2x scale (common UI scale)
149	assert.Equal(t, 60, ScaleValue(50, 1.2))
150}
151
152func TestMeasureWidgetHeight_NilWidget(t *testing.T) {
153	height := MeasureWidgetHeight(nil, 100)
154	assert.Equal(t, 0, height)
155}
156
157func TestDefaultRowHeights_Values(t *testing.T) {
158	// Ensure defaults are sensible
159	assert.Positive(t, DefaultRowHeights.Standard)
160	assert.Positive(t, DefaultRowHeights.Compact)
161	assert.Positive(t, DefaultRowHeights.Divider)
162	// Standard should be larger than compact
163	assert.Greater(t, DefaultRowHeights.Standard, DefaultRowHeights.Compact)
164}
165
166func TestOmniboxSizeDefaults_Values(t *testing.T) {
167	assert.InDelta(t, 0.8, OmniboxSizeDefaults.WidthPct, 0.0001)
168	assert.Equal(t, 800, OmniboxSizeDefaults.MaxWidth)
169	assert.InDelta(t, 0.2, OmniboxSizeDefaults.TopMarginPct, 0.0001)
170	assert.Equal(t, 600, OmniboxSizeDefaults.FallbackHeight)
171}
172
173func TestSessionManagerSizeDefaults_Values(t *testing.T) {
174	assert.Greater(t, SessionManagerSizeDefaults.WidthPct, 0.0)
175	assert.LessOrEqual(t, SessionManagerSizeDefaults.WidthPct, 1.0)
176	assert.Positive(t, SessionManagerSizeDefaults.MaxWidth)
177	assert.Greater(t, SessionManagerSizeDefaults.TopMarginPct, 0.0)
178	assert.LessOrEqual(t, SessionManagerSizeDefaults.TopMarginPct, 1.0)
179}
180
181func TestPermissionPopupSizeDefaults_Values(t *testing.T) {
182	assert.Greater(t, PermissionPopupSizeDefaults.WidthPct, 0.0)
183	assert.LessOrEqual(t, PermissionPopupSizeDefaults.WidthPct, 1.0)
184	assert.Positive(t, PermissionPopupSizeDefaults.MaxWidth)
185	assert.Greater(t, PermissionPopupSizeDefaults.TopMarginPct, 0.0)
186	assert.LessOrEqual(t, PermissionPopupSizeDefaults.TopMarginPct, 1.0)
187	assert.Equal(t, 300, PermissionPopupSizeDefaults.FallbackHeight)
188}
189
190func TestListDisplayDefaults_Values(t *testing.T) {
191	assert.Positive(t, OmniboxListDefaults.MaxVisibleRows)
192	assert.Positive(t, OmniboxListDefaults.MaxResults)
193	assert.Positive(t, SessionManagerListDefaults.MaxVisibleRows)
194	assert.Positive(t, SessionManagerListDefaults.MaxResults)
195}
196
197func TestEffectiveMaxRows(t *testing.T) {
198	sizeCfg := ModalSizeConfig{TopMarginPct: 0.2}
199	defaults := ListDisplayDefaults{
200		MaxVisibleRows:      10,
201		MaxResults:          10,
202		SmallMaxVisibleRows: 5,
203	}
204
205	tests := []struct {
206		name         string
207		parentHeight int
208		rowHeight    int
209		want         int
210	}{
211		{
212			name:         "tall pane returns MaxVisibleRows",
213			parentHeight: 1200,
214			rowHeight:    72,
215			want:         10,
216		},
217		{
218			name:         "short pane returns SmallMaxVisibleRows",
219			parentHeight: 800,
220			rowHeight:    72,
221			want:         5,
222		},
223		{
224			name:         "zero parentHeight returns MaxVisibleRows",
225			parentHeight: 0,
226			rowHeight:    72,
227			want:         10,
228		},
229		{
230			name:         "zero rowHeight returns MaxVisibleRows",
231			parentHeight: 800,
232			rowHeight:    0,
233			want:         10,
234		},
235		{
236			name:         "negative parentHeight returns MaxVisibleRows",
237			parentHeight: -100,
238			rowHeight:    72,
239			want:         10,
240		},
241		{
242			name:         "exact fit returns MaxVisibleRows",
243			parentHeight: 1150, // available = 1150 - 230 - 144 = 776, needed = 720
244			rowHeight:    72,
245			want:         10,
246		},
247		{
248			name:         "boundary: available equals needed still fits",
249			parentHeight: 1080, // available = 1080 - 216 - 144 = 720, needed = 720 → fits
250			rowHeight:    72,
251			want:         10,
252		},
253		{
254			name:         "boundary: one pixel under triggers small",
255			parentHeight: 1079, // available = 1079 - 215 - 144 = 720, needed = 720 → check
256			rowHeight:    72,
257			want:         10, // int(1079*0.2) = 215, 1079-215-144 = 720 → still fits
258		},
259		{
260			name:         "just below boundary triggers small",
261			parentHeight: 1078, // available = 1078 - 215 - 144 = 719, needed = 720 → doesn't fit
262			rowHeight:    72,
263			want:         5,
264		},
265	}
266
267	for _, tt := range tests {
268		t.Run(tt.name, func(t *testing.T) {
269			got := EffectiveMaxRows(tt.parentHeight, tt.rowHeight, sizeCfg, defaults)
270			assert.Equal(t, tt.want, got)
271		})
272	}
273}
274
275func TestEffectiveMaxRows_NoAdaptation(t *testing.T) {
276	sizeCfg := ModalSizeConfig{TopMarginPct: 0.2}
277	defaults := ListDisplayDefaults{
278		MaxVisibleRows:      10,
279		MaxResults:          10,
280		SmallMaxVisibleRows: 0, // adaptation disabled
281	}
282
283	// Even with a short pane, should return MaxVisibleRows when SmallMaxVisibleRows is 0
284	got := EffectiveMaxRows(500, 72, sizeCfg, defaults)
285	assert.Equal(t, 10, got)
286}
287
288func mockParent(t *testing.T, w, h int) *layoutmocks.MockOverlayWidget {
289	t.Helper()
290	parent := layoutmocks.NewMockOverlayWidget(t)
291	parent.EXPECT().GetAllocatedWidth().Return(w).Maybe()
292	parent.EXPECT().GetAllocatedHeight().Return(h).Maybe()
293	return parent
294}
295
296func TestCalculateModalDimensionsWithScale_ScaleOneUnchanged(t *testing.T) {
297	cfg := ModalSizeConfig{
298		WidthPct:       0.8,
299		MaxWidth:       800,
300		TopMarginPct:   0.2,
301		FallbackWidth:  800,
302		FallbackHeight: 600,
303	}
304	parent := mockParent(t, 1920, 1080)
305	w, m := CalculateModalDimensionsWithScale(parent, cfg, 1)
306	wLegacy, mLegacy := CalculateModalDimensions(parent, cfg)
307	assert.Equal(t, wLegacy, w)
308	assert.Equal(t, mLegacy, m)
309	assert.Equal(t, 800, w)
310	assert.Equal(t, 216, m)
311}
312
313func TestCalculateModalDimensionsWithScale_LargeParent(t *testing.T) {
314	cfg := OmniboxSizeDefaults
315	for _, tt := range []struct {
316		scale float64
317		want  int
318	}{
319		{1.25, 1000},
320		{1.5, 1200},
321		{2.0, 1600},
322	} {
323		parent := mockParent(t, 3000, 1200)
324		w, m := CalculateModalDimensionsWithScale(parent, cfg, tt.scale)
325		assert.Equal(t, tt.want, w, "scale %v", tt.scale)
326		assert.Equal(t, 240, m, "top margin unchanged")
327	}
328}
329
330func TestCalculateModalDimensionsWithScale_SmallParentPercentageBound(t *testing.T) {
331	cfg := SessionManagerSizeDefaults // 0.6 * w, max 600
332	parent := mockParent(t, 500, 800)
333	w, m := CalculateModalDimensionsWithScale(parent, cfg, 2.0)
334	// 0.6*500=300 < 600*2 → percentage bound wins
335	assert.Equal(t, 300, w)
336	assert.Equal(t, int(float64(800)*cfg.TopMarginPct), m)
337}
338
339func TestCalculateModalDimensionsWithScale_ScaleBelowOne(t *testing.T) {
340	cfg := OmniboxSizeDefaults
341	parent := mockParent(t, 3000, 1080)
342	w, _ := CalculateModalDimensionsWithScale(parent, cfg, 0.5)
343	assert.Equal(t, 400, w)
344}
345
346func TestCalculateModalDimensionsWithScale_NormalizesBadScale(t *testing.T) {
347	cfg := OmniboxSizeDefaults
348	for _, scale := range []float64{0, -1, -0.5, nan(), inf(1), inf(-1)} {
349		parent := mockParent(t, 3000, 1080)
350		w, m := CalculateModalDimensionsWithScale(parent, cfg, scale)
351		assert.Equal(t, 800, w, "scale %v", scale)
352		assert.Equal(t, 216, m, "scale %v", scale)
353	}
354}
355
356func TestCalculateModalDimensionsWithScale_FallbackScaled(t *testing.T) {
357	cfg := OmniboxSizeDefaults
358	parent := mockParent(t, 0, 0)
359	w, m := CalculateModalDimensionsWithScale(parent, cfg, 1.5)
360	// fallback 800*1.5=1200 → pct 0.8*1200=960 vs max 1200 → 960
361	assert.Equal(t, 960, w)
362	assert.Equal(t, int(float64(600)*cfg.TopMarginPct), m)
363}
364
365func TestCalculateModalDimensionsWithScale_FixedScalesOnceBeyondAllocation(t *testing.T) {
366	cfg := ResolveModalSizeConfig(ModalSizeConfig{
367		FixedWidth:        800,
368		FixedTopMargin:    0,
369		UseFixedTopMargin: true,
370	}, OmniboxSizeDefaults)
371	// Child-sized host allocation (small) must not clamp the fixed request.
372	parent := mockParent(t, 120, 200)
373	w, m := CalculateModalDimensionsWithScale(parent, cfg, 1.5)
374	assert.Equal(t, 1200, w)
375	assert.Equal(t, 0, m)
376	// Config must not be mutated by repeated calls.
377	assert.Equal(t, 800, cfg.FixedWidth)
378	w2, _ := CalculateModalDimensionsWithScale(parent, cfg, 1.5)
379	assert.Equal(t, w, w2)
380	assert.Equal(t, 800, cfg.FixedWidth)
381	assert.Equal(t, 800, OmniboxSizeDefaults.MaxWidth)
382}
383
384func TestCalculateModalDimensionsWithScale_NoDefaultMutation(t *testing.T) {
385	before := OmniboxSizeDefaults
386	parent := mockParent(t, 3000, 1200)
387	_, _ = CalculateModalDimensionsWithScale(parent, OmniboxSizeDefaults, 2.0)
388	assert.Equal(t, before, OmniboxSizeDefaults)
389}
390
391func TestOmniboxRequestedDimensions_UsesScale(t *testing.T) {
392	cfg := ResolveModalSizeConfig(ModalSizeConfig{
393		FixedWidth:        800,
394		FixedTopMargin:    0,
395		UseFixedTopMargin: true,
396	}, OmniboxSizeDefaults)
397	o := &Omnibox{parentOverlay: mockParent(t, 120, 200), sizeCfg: cfg, uiScale: 1.5}
398	w, m := o.requestedDimensions()
399	assert.Equal(t, 1200, w)
400	assert.Equal(t, 0, m)
401}
402
403func TestSessionManagerRequestedDimensions_UsesScale(t *testing.T) {
404	sm := &SessionManager{parentOverlay: mockParent(t, 3000, 1200), uiScale: 1.5}
405	w, m := sm.requestedDimensions()
406	// 0.6*3000=1800 vs 600*1.5=900 → 900
407	assert.Equal(t, 900, w)
408	assert.Equal(t, int(float64(1200)*SessionManagerSizeDefaults.TopMarginPct), m)
409}
410
411func nan() float64 { return math.NaN() }
412
413func inf(sign int) float64 { return math.Inf(sign) }

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