main 5041353

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