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 := >k.Widget{}
202 expectedOmnibox := &component.Omnibox{}
203 expectedWindow := >k.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(>k.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(>k.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 := >k.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}