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