v0.33.1 9577fee

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		UseMonitorHeight:  true,
140	}) {
141		t.Fatalf("unexpected standalone omnibox size config: %#v", cfg)
142	}
143}
144
145func TestStandaloneWindowConfig_Defaults(t *testing.T) {
146	cfg := DefaultStandaloneOmniboxWindowConfig()
147	if cfg.Namespace == "" {
148		t.Fatalf("expected namespace")
149	}
150	if cfg.Title == "" {
151		t.Fatalf("expected title")
152	}
153}
154
155func TestStandaloneOmniboxArgv_RemovesGoTestFlags(t *testing.T) {
156	argv := standaloneOmniboxArgv([]string{"/tmp/testbin", "-test.v", "-test.run", "TestFoo", "--gtk-debug"})
157
158	if len(argv) != 2 {
159		t.Fatalf("expected 2 args, got %d: %#v", len(argv), argv)
160	}
161	if argv[0] != "/tmp/testbin" {
162		t.Fatalf("expected binary path to be preserved, got %q", argv[0])
163	}
164	if argv[1] != "--gtk-debug" {
165		t.Fatalf("expected non-test arg to be preserved, got %#v", argv)
166	}
167}
168
169func TestStandaloneOmniboxArgv_RemovesOnlyLeadingOmniboxSubcommand(t *testing.T) {
170	argv := standaloneOmniboxArgv([]string{"/tmp/testbin", "omnibox", "https://example.com/omnibox", "omnibox"})
171
172	if len(argv) != 3 {
173		t.Fatalf("expected 3 args, got %d: %#v", len(argv), argv)
174	}
175	if argv[0] != "/tmp/testbin" {
176		t.Fatalf("expected binary path to be preserved, got %q", argv[0])
177	}
178	if argv[1] != "https://example.com/omnibox" {
179		t.Fatalf("expected non-subcommand omnibox value to be preserved, got %#v", argv)
180	}
181	if argv[2] != "omnibox" {
182		t.Fatalf("expected later omnibox arg to be preserved, got %#v", argv)
183	}
184}
185
186func TestActivateStandaloneOmnibox_RetainsObjectsUntilReleased(t *testing.T) {
187	originalBuildHost := buildStandaloneOmniboxHostFn
188	originalNewWindow := newStandaloneOmniboxWindow
189	originalConfigureWindow := configureStandaloneOmniboxWindowFn
190	originalPresentWindow := presentStandaloneOmniboxWindow
191	originalLogHostAllocation := logStandaloneOmniboxHostAllocationFn
192	originalShowOmnibox := showStandaloneOmniboxFn
193	t.Cleanup(func() {
194		buildStandaloneOmniboxHostFn = originalBuildHost
195		newStandaloneOmniboxWindow = originalNewWindow
196		configureStandaloneOmniboxWindowFn = originalConfigureWindow
197		presentStandaloneOmniboxWindow = originalPresentWindow
198		logStandaloneOmniboxHostAllocationFn = originalLogHostAllocation
199		showStandaloneOmniboxFn = originalShowOmnibox
200	})
201
202	expectedHost := &gtk.Widget{}
203	expectedOmnibox := &component.Omnibox{}
204	expectedWindow := &gtk.ApplicationWindow{}
205	refs := &standaloneOmniboxActivationRetention{}
206	runtimeCfg := &StandaloneOmniboxRuntime{}
207	windowCfg := DefaultStandaloneOmniboxWindowConfig()
208
209	buildStandaloneOmniboxHostFn = func(_ context.Context, _ *StandaloneOmniboxRuntime, _ *gtk.Application) (*gtk.Widget, *component.Omnibox) {
210		return expectedHost, expectedOmnibox
211	}
212	newStandaloneOmniboxWindow = func(_ *gtk.Application) *gtk.ApplicationWindow {
213		return expectedWindow
214	}
215	configureStandaloneOmniboxWindowFn = func(window *gtk.ApplicationWindow, cfg StandaloneOmniboxWindowConfig, child *gtk.Widget) {
216		if window != expectedWindow || child != expectedHost || cfg != windowCfg {
217			t.Fatalf("unexpected configure arguments: window=%p child=%p cfg=%#v", window, child, cfg)
218		}
219	}
220	presentStandaloneOmniboxWindow = func(window *gtk.ApplicationWindow) {
221		if window != expectedWindow {
222			t.Fatalf("unexpected window presented: %p", window)
223		}
224	}
225	logStandaloneOmniboxHostAllocationFn = func(_ context.Context, host *gtk.Widget) {
226		if host != expectedHost {
227			t.Fatalf("unexpected host logged: %p", host)
228		}
229	}
230	showStandaloneOmniboxFn = func(_ context.Context, omnibox *component.Omnibox) {
231		if omnibox != expectedOmnibox {
232			t.Fatalf("unexpected omnibox shown: %p", omnibox)
233		}
234		if refs.window != expectedWindow || refs.host != expectedHost || refs.omnibox != expectedOmnibox {
235			t.Fatalf("expected standalone activation refs to retain GTK objects during show, got %#v", refs)
236		}
237	}
238
239	activateStandaloneOmnibox(context.Background(), runtimeCfg, nil, windowCfg, refs)
240
241	if refs.window != expectedWindow || refs.host != expectedHost || refs.omnibox != expectedOmnibox {
242		t.Fatalf("expected standalone activation refs to retain GTK objects, got %#v", refs)
243	}
244
245	refs.release()
246
247	if refs.window != nil || refs.host != nil || refs.omnibox != nil {
248		t.Fatalf("expected standalone activation refs to release GTK objects, got %#v", refs)
249	}
250}
251
252func TestTryInitLayerShell_NilWindowReturnsFalse(t *testing.T) {
253	if tryInitLayerShell(nil, DefaultStandaloneOmniboxWindowConfig()) {
254		t.Fatalf("expected nil window to skip layer-shell")
255	}
256}
257
258func TestTryInitLayerShell_UnsupportedReturnsFalse(t *testing.T) {
259	originalLibraryAvailable := layerShellLibraryAvailable
260	original := layerShellIsSupported
261	layerShellLibraryAvailable = func() bool { return true }
262	layerShellIsSupported = func() bool { return false }
263	t.Cleanup(func() {
264		layerShellLibraryAvailable = originalLibraryAvailable
265		layerShellIsSupported = original
266	})
267
268	if tryInitLayerShell(&gtk.ApplicationWindow{}, DefaultStandaloneOmniboxWindowConfig()) {
269		t.Fatalf("expected unsupported layer-shell to skip initialization")
270	}
271}
272
273func TestTryInitLayerShell_MissingLibraryReturnsFalseWithoutSymbolCalls(t *testing.T) {
274	originalLibraryAvailable := layerShellLibraryAvailable
275	originalIsSupported := layerShellIsSupported
276	layerShellLibraryAvailable = func() bool { return false }
277	layerShellIsSupported = func() bool {
278		t.Fatalf("expected layer-shell support check to be skipped when library is missing")
279		return true
280	}
281	t.Cleanup(func() {
282		layerShellLibraryAvailable = originalLibraryAvailable
283		layerShellIsSupported = originalIsSupported
284	})
285
286	if tryInitLayerShell(&gtk.ApplicationWindow{}, DefaultStandaloneOmniboxWindowConfig()) {
287		t.Fatalf("expected missing library to skip layer-shell initialization")
288	}
289}
290
291func TestSafeLayerShellSupported_PreloadedLibrarySkipsPathProbe(t *testing.T) {
292	originalLibraryAvailable := layerShellLibraryAvailable
293	originalIsSupported := layerShellIsSupported
294	t.Setenv("LD_PRELOAD", "/custom/libgtk4-layer-shell.so.0")
295	layerShellLibraryAvailable = func() bool {
296		t.Fatalf("expected path probe to be skipped when layer-shell is already preloaded")
297		return false
298	}
299	layerShellIsSupported = func() bool { return true }
300	t.Cleanup(func() {
301		layerShellLibraryAvailable = originalLibraryAvailable
302		layerShellIsSupported = originalIsSupported
303	})
304
305	if !safeLayerShellSupported() {
306		t.Fatalf("expected preloaded layer-shell library to be treated as supported")
307	}
308}
309
310func TestTryInitLayerShell_SupportedConfiguresWindow(t *testing.T) {
311	originalLibraryAvailable := layerShellLibraryAvailable
312	originalIsSupported := layerShellIsSupported
313	originalInitForWindow := layerShellInitForWindow
314	originalSetLayer := layerShellSetLayer
315	originalSetExclusiveZone := layerShellSetExclusiveZone
316	originalSetNamespace := layerShellSetNamespace
317	originalSetKeyboardMode := layerShellSetKeyboardMode
318	t.Cleanup(func() {
319		layerShellLibraryAvailable = originalLibraryAvailable
320		layerShellIsSupported = originalIsSupported
321		layerShellInitForWindow = originalInitForWindow
322		layerShellSetLayer = originalSetLayer
323		layerShellSetExclusiveZone = originalSetExclusiveZone
324		layerShellSetNamespace = originalSetNamespace
325		layerShellSetKeyboardMode = originalSetKeyboardMode
326	})
327
328	window := &gtk.ApplicationWindow{}
329	cfg := DefaultStandaloneOmniboxWindowConfig()
330	var callOrder []string
331
332	layerShellLibraryAvailable = func() bool {
333		callOrder = append(callOrder, "library")
334		return true
335	}
336	layerShellIsSupported = func() bool {
337		callOrder = append(callOrder, "supported")
338		return true
339	}
340	layerShellInitForWindow = func(got *gtk.ApplicationWindow) {
341		if got != window {
342			t.Fatalf("expected init window %p, got %p", window, got)
343		}
344		callOrder = append(callOrder, "init")
345	}
346	layerShellSetLayer = func(got *gtk.ApplicationWindow, layer layershell.Layer) {
347		if got != window {
348			t.Fatalf("expected layer window %p, got %p", window, got)
349		}
350		if layer != layershell.LayerOverlayValue {
351			t.Fatalf("expected overlay layer, got %v", layer)
352		}
353		callOrder = append(callOrder, "layer")
354	}
355	layerShellSetExclusiveZone = func(got *gtk.ApplicationWindow, zone int) {
356		if got != window {
357			t.Fatalf("expected exclusive zone window %p, got %p", window, got)
358		}
359		if zone != 0 {
360			t.Fatalf("expected exclusive zone 0, got %d", zone)
361		}
362		callOrder = append(callOrder, "zone")
363	}
364	layerShellSetNamespace = func(got *gtk.ApplicationWindow, namespace *string) {
365		if got != window {
366			t.Fatalf("expected namespace window %p, got %p", window, got)
367		}
368		if namespace == nil || *namespace != cfg.Namespace {
369			t.Fatalf("expected namespace %q, got %#v", cfg.Namespace, namespace)
370		}
371		callOrder = append(callOrder, "namespace")
372	}
373	layerShellSetKeyboardMode = func(got *gtk.ApplicationWindow, mode layershell.KeyboardMode) {
374		if got != window {
375			t.Fatalf("expected keyboard mode window %p, got %p", window, got)
376		}
377		if mode != layershell.KeyboardModeExclusiveValue {
378			t.Fatalf("expected exclusive keyboard mode, got %v", mode)
379		}
380		callOrder = append(callOrder, "keyboard")
381	}
382
383	if !tryInitLayerShell(window, cfg) {
384		t.Fatalf("expected supported layer-shell to initialize")
385	}
386
387	wantOrder := []string{"library", "supported", "init", "layer", "zone", "namespace", "keyboard"}
388	if len(callOrder) != len(wantOrder) {
389		t.Fatalf("expected %d calls, got %d: %#v", len(wantOrder), len(callOrder), callOrder)
390	}
391	for i := range wantOrder {
392		if callOrder[i] != wantOrder[i] {
393			t.Fatalf("expected call %d to be %q, got %q", i, wantOrder[i], callOrder[i])
394		}
395	}
396}

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