v0.31.0 bf0756b

standalone_omnibox_test.go RawHistoryFolder

1package ui
2
3import (
4	"context"
5	"testing"
6
7	"github.com/bnema/dumber/internal/ui/component"
8	layoutmocks "github.com/bnema/dumber/internal/ui/layout/mocks"
9	"github.com/bnema/puregotk/v4/gtk"
10	"github.com/bnema/puregotk/v4/layershell"
11)
12
13func TestWrapStandaloneOmniboxNavigate_CallsOriginalThenQuit(t *testing.T) {
14	var calls []string
15	wrapped := wrapStandaloneOmniboxNavigate(func(_ context.Context, url string) error {
16		calls = append(calls, "navigate:"+url)
17		return nil
18	}, func() {
19		calls = append(calls, "quit")
20	})
21
22	if err := wrapped(context.Background(), "https://example.com"); err != nil {
23		t.Fatalf("wrapped navigate returned error: %v", err)
24	}
25
26	if len(calls) != 2 {
27		t.Fatalf("expected 2 calls, got %d: %#v", len(calls), calls)
28	}
29	if calls[0] != "navigate:https://example.com" {
30		t.Fatalf("expected navigate call first, got %#v", calls)
31	}
32	if calls[1] != "quit" {
33		t.Fatalf("expected quit call second, got %#v", calls)
34	}
35}
36
37func TestWrapStandaloneOmniboxNavigate_DoesNotQuitWhenNavigateFails(t *testing.T) {
38	called := false
39	wrapped := wrapStandaloneOmniboxNavigate(func(context.Context, string) error {
40		return context.DeadlineExceeded
41	}, func() {
42		called = true
43	})
44
45	if err := wrapped(context.Background(), "https://example.com"); err == nil {
46		t.Fatal("expected wrapped navigate to return an error")
47	}
48	if called {
49		t.Fatalf("expected quit to be skipped when navigate fails")
50	}
51}
52
53func TestStandaloneOmniboxCloseHandler_QuitsApp(t *testing.T) {
54	called := false
55	onClose := standaloneOmniboxCloseHandler(func() {
56		called = true
57	})
58
59	onClose()
60
61	if !called {
62		t.Fatalf("expected close handler to quit app")
63	}
64}
65
66func TestStandaloneOmniboxWindowDecorated_LayerShellAppliedReturnsFalse(t *testing.T) {
67	if standaloneOmniboxWindowDecorated(true) {
68		t.Fatalf("expected layer-shell window to be undecorated")
69	}
70}
71
72func TestStandaloneOmniboxWindowDecorated_FallbackReturnsTrue(t *testing.T) {
73	if !standaloneOmniboxWindowDecorated(false) {
74		t.Fatalf("expected fallback window to remain decorated")
75	}
76}
77
78func TestStandaloneOmniboxWindowResizable_LayerShellAppliedReturnsFalse(t *testing.T) {
79	if standaloneOmniboxWindowResizable(true) {
80		t.Fatalf("expected layer-shell window to stop keeping stale height allocations")
81	}
82}
83
84func TestStandaloneOmniboxWindowResizable_FallbackReturnsTrue(t *testing.T) {
85	if !standaloneOmniboxWindowResizable(false) {
86		t.Fatalf("expected fallback window to remain resizable")
87	}
88}
89
90func TestStandaloneOmniboxHostLayout_LayerShellUsesNaturalSizingWithoutDefaultSize(t *testing.T) {
91	layoutCfg := standaloneOmniboxHostLayout(true)
92
93	if layoutCfg.UseDefaultSize {
94		t.Fatalf("expected layer-shell host to avoid fixed default size")
95	}
96	if layoutCfg.OmniboxSize.FixedWidth != 800 {
97		t.Fatalf("expected standalone omnibox fixed width 800, got %d", layoutCfg.OmniboxSize.FixedWidth)
98	}
99	if !layoutCfg.OmniboxSize.UseFixedTopMargin {
100		t.Fatalf("expected standalone omnibox to use a fixed top margin")
101	}
102	if layoutCfg.OmniboxSize.FixedTopMargin != 0 {
103		t.Fatalf("expected standalone omnibox top margin 0, got %d", layoutCfg.OmniboxSize.FixedTopMargin)
104	}
105}
106
107func TestStandaloneOmniboxHostLayout_FallbackUsesDefaultSize(t *testing.T) {
108	layoutCfg := standaloneOmniboxHostLayout(false)
109
110	if !layoutCfg.UseDefaultSize {
111		t.Fatalf("expected non-layer-shell host to keep fixed fallback size")
112	}
113	if layoutCfg.OmniboxSize != standaloneOmniboxSizeConfig() {
114		t.Fatalf("expected fallback host to use standalone omnibox sizing, got %#v", layoutCfg.OmniboxSize)
115	}
116}
117
118func TestAttachStandaloneOmniboxHostWidget_UsesPrimaryChild(t *testing.T) {
119	host := layoutmocks.NewMockOverlayWidget(t)
120	child := layoutmocks.NewMockWidget(t)
121
122	host.EXPECT().SetChild(child).Once()
123
124	attachStandaloneOmniboxHostWidget(host, child)
125}
126
127func TestStandaloneOmniboxSizeConfig_UsesFixedGeometry(t *testing.T) {
128	cfg := standaloneOmniboxSizeConfig()
129
130	if cfg != (component.ModalSizeConfig{
131		WidthPct:          component.OmniboxSizeDefaults.WidthPct,
132		MaxWidth:          component.OmniboxSizeDefaults.MaxWidth,
133		TopMarginPct:      component.OmniboxSizeDefaults.TopMarginPct,
134		FallbackWidth:     component.OmniboxSizeDefaults.FallbackWidth,
135		FallbackHeight:    component.OmniboxSizeDefaults.FallbackHeight,
136		FixedWidth:        800,
137		FixedTopMargin:    0,
138		UseFixedTopMargin: true,
139	}) {
140		t.Fatalf("unexpected standalone omnibox size config: %#v", cfg)
141	}
142}
143
144func TestStandaloneWindowConfig_Defaults(t *testing.T) {
145	cfg := DefaultStandaloneOmniboxWindowConfig()
146	if cfg.Namespace == "" {
147		t.Fatalf("expected namespace")
148	}
149	if cfg.Title == "" {
150		t.Fatalf("expected title")
151	}
152}
153
154func TestStandaloneOmniboxArgv_RemovesGoTestFlags(t *testing.T) {
155	argv := standaloneOmniboxArgv([]string{"/tmp/testbin", "-test.v", "-test.run", "TestFoo", "--gtk-debug"})
156
157	if len(argv) != 2 {
158		t.Fatalf("expected 2 args, got %d: %#v", len(argv), argv)
159	}
160	if argv[0] != "/tmp/testbin" {
161		t.Fatalf("expected binary path to be preserved, got %q", argv[0])
162	}
163	if argv[1] != "--gtk-debug" {
164		t.Fatalf("expected non-test arg to be preserved, got %#v", argv)
165	}
166}
167
168func TestStandaloneOmniboxArgv_RemovesOnlyLeadingOmniboxSubcommand(t *testing.T) {
169	argv := standaloneOmniboxArgv([]string{"/tmp/testbin", "omnibox", "https://example.com/omnibox", "omnibox"})
170
171	if len(argv) != 3 {
172		t.Fatalf("expected 3 args, got %d: %#v", len(argv), argv)
173	}
174	if argv[0] != "/tmp/testbin" {
175		t.Fatalf("expected binary path to be preserved, got %q", argv[0])
176	}
177	if argv[1] != "https://example.com/omnibox" {
178		t.Fatalf("expected non-subcommand omnibox value to be preserved, got %#v", argv)
179	}
180	if argv[2] != "omnibox" {
181		t.Fatalf("expected later omnibox arg to be preserved, got %#v", argv)
182	}
183}
184
185func TestActivateStandaloneOmnibox_RetainsObjectsUntilReleased(t *testing.T) {
186	originalBuildHost := buildStandaloneOmniboxHostFn
187	originalNewWindow := newStandaloneOmniboxWindow
188	originalConfigureWindow := configureStandaloneOmniboxWindowFn
189	originalPresentWindow := presentStandaloneOmniboxWindow
190	originalLogHostAllocation := logStandaloneOmniboxHostAllocationFn
191	originalShowOmnibox := showStandaloneOmniboxFn
192	t.Cleanup(func() {
193		buildStandaloneOmniboxHostFn = originalBuildHost
194		newStandaloneOmniboxWindow = originalNewWindow
195		configureStandaloneOmniboxWindowFn = originalConfigureWindow
196		presentStandaloneOmniboxWindow = originalPresentWindow
197		logStandaloneOmniboxHostAllocationFn = originalLogHostAllocation
198		showStandaloneOmniboxFn = originalShowOmnibox
199	})
200
201	expectedHost := &gtk.Widget{}
202	expectedOmnibox := &component.Omnibox{}
203	expectedWindow := &gtk.ApplicationWindow{}
204	refs := &standaloneOmniboxActivationRetention{}
205	runtimeCfg := &StandaloneOmniboxRuntime{}
206	windowCfg := DefaultStandaloneOmniboxWindowConfig()
207
208	buildStandaloneOmniboxHostFn = func(_ context.Context, _ *StandaloneOmniboxRuntime, _ *gtk.Application) (*gtk.Widget, *component.Omnibox) {
209		return expectedHost, expectedOmnibox
210	}
211	newStandaloneOmniboxWindow = func(_ *gtk.Application) *gtk.ApplicationWindow {
212		return expectedWindow
213	}
214	configureStandaloneOmniboxWindowFn = func(window *gtk.ApplicationWindow, cfg StandaloneOmniboxWindowConfig, child *gtk.Widget) {
215		if window != expectedWindow || child != expectedHost || cfg != windowCfg {
216			t.Fatalf("unexpected configure arguments: window=%p child=%p cfg=%#v", window, child, cfg)
217		}
218	}
219	presentStandaloneOmniboxWindow = func(window *gtk.ApplicationWindow) {
220		if window != expectedWindow {
221			t.Fatalf("unexpected window presented: %p", window)
222		}
223	}
224	logStandaloneOmniboxHostAllocationFn = func(_ context.Context, host *gtk.Widget) {
225		if host != expectedHost {
226			t.Fatalf("unexpected host logged: %p", host)
227		}
228	}
229	showStandaloneOmniboxFn = func(_ context.Context, omnibox *component.Omnibox) {
230		if omnibox != expectedOmnibox {
231			t.Fatalf("unexpected omnibox shown: %p", omnibox)
232		}
233		if refs.window != expectedWindow || refs.host != expectedHost || refs.omnibox != expectedOmnibox {
234			t.Fatalf("expected standalone activation refs to retain GTK objects during show, got %#v", refs)
235		}
236	}
237
238	activateStandaloneOmnibox(context.Background(), runtimeCfg, nil, windowCfg, refs)
239
240	if refs.window != expectedWindow || refs.host != expectedHost || refs.omnibox != expectedOmnibox {
241		t.Fatalf("expected standalone activation refs to retain GTK objects, got %#v", refs)
242	}
243
244	refs.release()
245
246	if refs.window != nil || refs.host != nil || refs.omnibox != nil {
247		t.Fatalf("expected standalone activation refs to release GTK objects, got %#v", refs)
248	}
249}
250
251func TestTryInitLayerShell_NilWindowReturnsFalse(t *testing.T) {
252	if tryInitLayerShell(nil, DefaultStandaloneOmniboxWindowConfig()) {
253		t.Fatalf("expected nil window to skip layer-shell")
254	}
255}
256
257func TestTryInitLayerShell_UnsupportedReturnsFalse(t *testing.T) {
258	originalLibraryAvailable := layerShellLibraryAvailable
259	original := layerShellIsSupported
260	layerShellLibraryAvailable = func() bool { return true }
261	layerShellIsSupported = func() bool { return false }
262	t.Cleanup(func() {
263		layerShellLibraryAvailable = originalLibraryAvailable
264		layerShellIsSupported = original
265	})
266
267	if tryInitLayerShell(&gtk.ApplicationWindow{}, DefaultStandaloneOmniboxWindowConfig()) {
268		t.Fatalf("expected unsupported layer-shell to skip initialization")
269	}
270}
271
272func TestTryInitLayerShell_MissingLibraryReturnsFalseWithoutSymbolCalls(t *testing.T) {
273	originalLibraryAvailable := layerShellLibraryAvailable
274	originalIsSupported := layerShellIsSupported
275	layerShellLibraryAvailable = func() bool { return false }
276	layerShellIsSupported = func() bool {
277		t.Fatalf("expected layer-shell support check to be skipped when library is missing")
278		return true
279	}
280	t.Cleanup(func() {
281		layerShellLibraryAvailable = originalLibraryAvailable
282		layerShellIsSupported = originalIsSupported
283	})
284
285	if tryInitLayerShell(&gtk.ApplicationWindow{}, DefaultStandaloneOmniboxWindowConfig()) {
286		t.Fatalf("expected missing library to skip layer-shell initialization")
287	}
288}
289
290func TestSafeLayerShellSupported_PreloadedLibrarySkipsPathProbe(t *testing.T) {
291	originalLibraryAvailable := layerShellLibraryAvailable
292	originalIsSupported := layerShellIsSupported
293	t.Setenv("LD_PRELOAD", "/custom/libgtk4-layer-shell.so.0")
294	layerShellLibraryAvailable = func() bool {
295		t.Fatalf("expected path probe to be skipped when layer-shell is already preloaded")
296		return false
297	}
298	layerShellIsSupported = func() bool { return true }
299	t.Cleanup(func() {
300		layerShellLibraryAvailable = originalLibraryAvailable
301		layerShellIsSupported = originalIsSupported
302	})
303
304	if !safeLayerShellSupported() {
305		t.Fatalf("expected preloaded layer-shell library to be treated as supported")
306	}
307}
308
309func TestTryInitLayerShell_SupportedConfiguresWindow(t *testing.T) {
310	originalLibraryAvailable := layerShellLibraryAvailable
311	originalIsSupported := layerShellIsSupported
312	originalInitForWindow := layerShellInitForWindow
313	originalSetLayer := layerShellSetLayer
314	originalSetExclusiveZone := layerShellSetExclusiveZone
315	originalSetNamespace := layerShellSetNamespace
316	originalSetKeyboardMode := layerShellSetKeyboardMode
317	t.Cleanup(func() {
318		layerShellLibraryAvailable = originalLibraryAvailable
319		layerShellIsSupported = originalIsSupported
320		layerShellInitForWindow = originalInitForWindow
321		layerShellSetLayer = originalSetLayer
322		layerShellSetExclusiveZone = originalSetExclusiveZone
323		layerShellSetNamespace = originalSetNamespace
324		layerShellSetKeyboardMode = originalSetKeyboardMode
325	})
326
327	window := &gtk.ApplicationWindow{}
328	cfg := DefaultStandaloneOmniboxWindowConfig()
329	var callOrder []string
330
331	layerShellLibraryAvailable = func() bool {
332		callOrder = append(callOrder, "library")
333		return true
334	}
335	layerShellIsSupported = func() bool {
336		callOrder = append(callOrder, "supported")
337		return true
338	}
339	layerShellInitForWindow = func(got *gtk.ApplicationWindow) {
340		if got != window {
341			t.Fatalf("expected init window %p, got %p", window, got)
342		}
343		callOrder = append(callOrder, "init")
344	}
345	layerShellSetLayer = func(got *gtk.ApplicationWindow, layer layershell.Layer) {
346		if got != window {
347			t.Fatalf("expected layer window %p, got %p", window, got)
348		}
349		if layer != layershell.LayerOverlayValue {
350			t.Fatalf("expected overlay layer, got %v", layer)
351		}
352		callOrder = append(callOrder, "layer")
353	}
354	layerShellSetExclusiveZone = func(got *gtk.ApplicationWindow, zone int) {
355		if got != window {
356			t.Fatalf("expected exclusive zone window %p, got %p", window, got)
357		}
358		if zone != 0 {
359			t.Fatalf("expected exclusive zone 0, got %d", zone)
360		}
361		callOrder = append(callOrder, "zone")
362	}
363	layerShellSetNamespace = func(got *gtk.ApplicationWindow, namespace *string) {
364		if got != window {
365			t.Fatalf("expected namespace window %p, got %p", window, got)
366		}
367		if namespace == nil || *namespace != cfg.Namespace {
368			t.Fatalf("expected namespace %q, got %#v", cfg.Namespace, namespace)
369		}
370		callOrder = append(callOrder, "namespace")
371	}
372	layerShellSetKeyboardMode = func(got *gtk.ApplicationWindow, mode layershell.KeyboardMode) {
373		if got != window {
374			t.Fatalf("expected keyboard mode window %p, got %p", window, got)
375		}
376		if mode != layershell.KeyboardModeExclusiveValue {
377			t.Fatalf("expected exclusive keyboard mode, got %v", mode)
378		}
379		callOrder = append(callOrder, "keyboard")
380	}
381
382	if !tryInitLayerShell(window, cfg) {
383		t.Fatalf("expected supported layer-shell to initialize")
384	}
385
386	wantOrder := []string{"library", "supported", "init", "layer", "zone", "namespace", "keyboard"}
387	if len(callOrder) != len(wantOrder) {
388		t.Fatalf("expected %d calls, got %d: %#v", len(wantOrder), len(callOrder), callOrder)
389	}
390	for i := range wantOrder {
391		if callOrder[i] != wantOrder[i] {
392			t.Fatalf("expected call %d to be %q, got %q", i, wantOrder[i], callOrder[i])
393		}
394	}
395}

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