detached dccb0ab

test(ui): skip gtk window tests without display

commit
dccb0ab13030ef8150047d731cf8924a88eb9585
author
brice · · 2026-04-16 03:30 UTC
parent
802dc32
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +60 -48
M internal/ui/app_browser_launch_test.go +60 -48
@@ -3,7 +3,10 @@ package ui
33 import (
44 "context"
55 "io"
6+ "os"
7+ "path/filepath"
68 "reflect"
9+ "strings"
710 "testing"
811 "unsafe"
912
@@ -21,6 +24,7 @@ import (
2124 "github.com/bnema/dumber/internal/ui/layout"
2225 layoutmocks "github.com/bnema/dumber/internal/ui/layout/mocks"
2326 "github.com/bnema/dumber/internal/ui/window"
27+ "github.com/bnema/puregotk/v4/gdk"
2428 "github.com/bnema/puregotk/v4/gio"
2529 "github.com/bnema/puregotk/v4/gtk"
2630 )
@@ -69,6 +73,54 @@ func (r *fakeSessionStateRepo) GetAllSnapshots(context.Context) ([]*entity.Sessi
6973
7074 func (r *fakeSessionStateRepo) GetTotalSnapshotsSize(context.Context) (int64, error) { return 0, nil }
7175
76+func testHasUsableGTKDisplay() bool {
77+ if waylandDisplay := os.Getenv("WAYLAND_DISPLAY"); waylandDisplay != "" {
78+ candidates := []string{waylandDisplay}
79+ if runtimeDir := os.Getenv("XDG_RUNTIME_DIR"); runtimeDir != "" && !filepath.IsAbs(waylandDisplay) {
80+ candidates = append([]string{filepath.Join(runtimeDir, waylandDisplay)}, candidates...)
81+ }
82+ for _, candidate := range candidates {
83+ if _, err := os.Stat(candidate); err == nil {
84+ return true
85+ }
86+ }
87+ }
88+
89+ if display := os.Getenv("DISPLAY"); display != "" {
90+ if strings.HasPrefix(display, ":") {
91+ displayNum := strings.TrimPrefix(display, ":")
92+ displayNum = strings.SplitN(displayNum, ".", 2)[0]
93+ if displayNum == "" {
94+ return false
95+ }
96+ if _, err := os.Stat(filepath.Join("/tmp/.X11-unix", "X"+displayNum)); err == nil {
97+ return true
98+ }
99+ return false
100+ }
101+ return true
102+ }
103+
104+ return false
105+}
106+
107+func requireGTKDisplayApp(t *testing.T) *gtk.Application {
108+ t.Helper()
109+ if !testHasUsableGTKDisplay() {
110+ t.Skip("GTK display not available")
111+ }
112+ EnsureAdwaitaInitialized()
113+ if gdk.DisplayGetDefault() == nil {
114+ t.Skip("GTK display not available")
115+ }
116+ appID := AppID
117+ gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)
118+ if gtkApp == nil {
119+ t.Fatal("gtk application creation failed")
120+ }
121+ return gtkApp
122+}
123+
72124 // setDependencyField uses reflection to reach unexported dependency fields for tests.
73125 // Keep this test-only so production visibility stays narrow.
74126 func setDependencyField(t *testing.T, deps *Dependencies, field string, value any) {
@@ -283,12 +335,7 @@ func TestApp_OpenFreshWindowRecordsTabOwnership(t *testing.T) {
283335 }
284336
285337 func TestApp_RestoreSessionClearsStaleUIStateBeforeApplyingRestoredTabs(t *testing.T) {
286- EnsureAdwaitaInitialized()
287- appID := AppID
288- gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)
289- if gtkApp == nil {
290- t.Fatal("gtk application creation failed")
291- }
338+ gtkApp := requireGTKDisplayApp(t)
292339 defer gtkApp.Unref()
293340
294341 mainWindow, err := window.New(context.Background(), gtkApp, "top")
@@ -344,12 +391,7 @@ func TestApp_RestoreSessionClearsStaleUIStateBeforeApplyingRestoredTabs(t *testi
344391 }
345392
346393 func TestApp_RestoreSessionWiresStackedPaneTitleBarCallbacks(t *testing.T) {
347- EnsureAdwaitaInitialized()
348- appID := AppID
349- gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)
350- if gtkApp == nil {
351- t.Fatal("gtk application creation failed")
352- }
394+ gtkApp := requireGTKDisplayApp(t)
353395 defer gtkApp.Unref()
354396
355397 mainWindow, err := window.New(context.Background(), gtkApp, "top")
@@ -613,12 +655,7 @@ func TestApp_OwnerOrLastFocusedBrowserWindowPrefersPaneOwner(t *testing.T) {
613655 }
614656
615657 func TestApp_HandlePaneWindowTitleChangedTargetsOwningWindow(t *testing.T) {
616- EnsureAdwaitaInitialized()
617- appID := AppID
618- gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)
619- if gtkApp == nil {
620- t.Fatal("gtk application creation failed")
621- }
658+ gtkApp := requireGTKDisplayApp(t)
622659 defer gtkApp.Unref()
623660
624661 firstMainWindow, err := window.New(context.Background(), gtkApp, "top")
@@ -657,12 +694,7 @@ func TestApp_HandlePaneWindowTitleChangedTargetsOwningWindow(t *testing.T) {
657694 }
658695
659696 func TestApp_ActivateBrowserWindowResyncsTitleFromBackgroundPane(t *testing.T) {
660- EnsureAdwaitaInitialized()
661- appID := AppID
662- gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)
663- if gtkApp == nil {
664- t.Fatal("gtk application creation failed")
665- }
697+ gtkApp := requireGTKDisplayApp(t)
666698 defer gtkApp.Unref()
667699
668700 firstMainWindow, err := window.New(context.Background(), gtkApp, "top")
@@ -707,12 +739,7 @@ func TestApp_ActivateBrowserWindowResyncsTitleFromBackgroundPane(t *testing.T) {
707739 }
708740
709741 func TestApp_HandlePaneFullscreenChangedTargetsOwningWindow(t *testing.T) {
710- EnsureAdwaitaInitialized()
711- appID := AppID
712- gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)
713- if gtkApp == nil {
714- t.Fatal("gtk application creation failed")
715- }
742+ gtkApp := requireGTKDisplayApp(t)
716743 defer gtkApp.Unref()
717744
718745 firstMainWindow, err := window.New(context.Background(), gtkApp, "top")
@@ -759,12 +786,7 @@ func TestApp_HandlePaneFullscreenChangedTargetsOwningWindow(t *testing.T) {
759786 }
760787
761788 func TestApp_HandlePaneFullscreenChangedIgnoresUnresolvedPane(t *testing.T) {
762- EnsureAdwaitaInitialized()
763- appID := AppID
764- gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)
765- if gtkApp == nil {
766- t.Fatal("gtk application creation failed")
767- }
789+ gtkApp := requireGTKDisplayApp(t)
768790 defer gtkApp.Unref()
769791
770792 firstMainWindow, err := window.New(context.Background(), gtkApp, "top")
@@ -881,12 +903,7 @@ func TestApp_MoveActivePaneToTabFromBrowserWindowActivatesTargetOwnerForCrossWin
881903 }
882904
883905 func TestApp_InitKeyboardHandlerDoesNotReattachExistingWindowInput(t *testing.T) {
884- EnsureAdwaitaInitialized()
885- appID := AppID
886- gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)
887- if gtkApp == nil {
888- t.Fatal("gtk application creation failed")
889- }
906+ gtkApp := requireGTKDisplayApp(t)
890907 defer gtkApp.Unref()
891908
892909 mainWindow, err := window.New(context.Background(), gtkApp, "top")
@@ -946,12 +963,7 @@ func (*testFocusedInputProvider) GetFocusedInput() port.TextInputTarget { return
946963 func (*testFocusedInputProvider) SetFocusedInput(port.TextInputTarget) {}
947964
948965 func TestApp_UpdateBrowserWindowTabBarVisibilityHonorsHideWhenSingleTabDisabled(t *testing.T) {
949- EnsureAdwaitaInitialized()
950- appID := AppID
951- gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)
952- if gtkApp == nil {
953- t.Fatal("gtk application creation failed")
954- }
966+ gtkApp := requireGTKDisplayApp(t)
955967 defer gtkApp.Unref()
956968
957969 mainWindow := &window.MainWindow{}
@@ -3,7 +3,10 @@ package ui
3 import (3 import (
4 "context"4 "context"
5 "io"5 "io"
6+ "os"
7+ "path/filepath"
6 "reflect"8 "reflect"
9+ "strings"
7 "testing"10 "testing"
8 "unsafe"11 "unsafe"
9 12
@@ -21,6 +24,7 @@ import (
21 "github.com/bnema/dumber/internal/ui/layout"24 "github.com/bnema/dumber/internal/ui/layout"
22 layoutmocks "github.com/bnema/dumber/internal/ui/layout/mocks"25 layoutmocks "github.com/bnema/dumber/internal/ui/layout/mocks"
23 "github.com/bnema/dumber/internal/ui/window"26 "github.com/bnema/dumber/internal/ui/window"
27+ "github.com/bnema/puregotk/v4/gdk"
24 "github.com/bnema/puregotk/v4/gio"28 "github.com/bnema/puregotk/v4/gio"
25 "github.com/bnema/puregotk/v4/gtk"29 "github.com/bnema/puregotk/v4/gtk"
26 )30 )
@@ -69,6 +73,54 @@ func (r *fakeSessionStateRepo) GetAllSnapshots(context.Context) ([]*entity.Sessi
69 73
70 func (r *fakeSessionStateRepo) GetTotalSnapshotsSize(context.Context) (int64, error) { return 0, nil }74 func (r *fakeSessionStateRepo) GetTotalSnapshotsSize(context.Context) (int64, error) { return 0, nil }
71 75
76+func testHasUsableGTKDisplay() bool {
77+ if waylandDisplay := os.Getenv("WAYLAND_DISPLAY"); waylandDisplay != "" {
78+ candidates := []string{waylandDisplay}
79+ if runtimeDir := os.Getenv("XDG_RUNTIME_DIR"); runtimeDir != "" && !filepath.IsAbs(waylandDisplay) {
80+ candidates = append([]string{filepath.Join(runtimeDir, waylandDisplay)}, candidates...)
81+ }
82+ for _, candidate := range candidates {
83+ if _, err := os.Stat(candidate); err == nil {
84+ return true
85+ }
86+ }
87+ }
88+
89+ if display := os.Getenv("DISPLAY"); display != "" {
90+ if strings.HasPrefix(display, ":") {
91+ displayNum := strings.TrimPrefix(display, ":")
92+ displayNum = strings.SplitN(displayNum, ".", 2)[0]
93+ if displayNum == "" {
94+ return false
95+ }
96+ if _, err := os.Stat(filepath.Join("/tmp/.X11-unix", "X"+displayNum)); err == nil {
97+ return true
98+ }
99+ return false
100+ }
101+ return true
102+ }
103+
104+ return false
105+}
106+
107+func requireGTKDisplayApp(t *testing.T) *gtk.Application {
108+ t.Helper()
109+ if !testHasUsableGTKDisplay() {
110+ t.Skip("GTK display not available")
111+ }
112+ EnsureAdwaitaInitialized()
113+ if gdk.DisplayGetDefault() == nil {
114+ t.Skip("GTK display not available")
115+ }
116+ appID := AppID
117+ gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)
118+ if gtkApp == nil {
119+ t.Fatal("gtk application creation failed")
120+ }
121+ return gtkApp
122+}
123+
72 // setDependencyField uses reflection to reach unexported dependency fields for tests.124 // setDependencyField uses reflection to reach unexported dependency fields for tests.
73 // Keep this test-only so production visibility stays narrow.125 // Keep this test-only so production visibility stays narrow.
74 func setDependencyField(t *testing.T, deps *Dependencies, field string, value any) {126 func setDependencyField(t *testing.T, deps *Dependencies, field string, value any) {
@@ -283,12 +335,7 @@ func TestApp_OpenFreshWindowRecordsTabOwnership(t *testing.T) {
283 }335 }
284 336
285 func TestApp_RestoreSessionClearsStaleUIStateBeforeApplyingRestoredTabs(t *testing.T) {337 func TestApp_RestoreSessionClearsStaleUIStateBeforeApplyingRestoredTabs(t *testing.T) {
286- EnsureAdwaitaInitialized()338+ gtkApp := requireGTKDisplayApp(t)
287- appID := AppID
288- gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)
289- if gtkApp == nil {
290- t.Fatal("gtk application creation failed")
291- }
292 defer gtkApp.Unref()339 defer gtkApp.Unref()
293 340
294 mainWindow, err := window.New(context.Background(), gtkApp, "top")341 mainWindow, err := window.New(context.Background(), gtkApp, "top")
@@ -344,12 +391,7 @@ func TestApp_RestoreSessionClearsStaleUIStateBeforeApplyingRestoredTabs(t *testi
344 }391 }
345 392
346 func TestApp_RestoreSessionWiresStackedPaneTitleBarCallbacks(t *testing.T) {393 func TestApp_RestoreSessionWiresStackedPaneTitleBarCallbacks(t *testing.T) {
347- EnsureAdwaitaInitialized()394+ gtkApp := requireGTKDisplayApp(t)
348- appID := AppID
349- gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)
350- if gtkApp == nil {
351- t.Fatal("gtk application creation failed")
352- }
353 defer gtkApp.Unref()395 defer gtkApp.Unref()
354 396
355 mainWindow, err := window.New(context.Background(), gtkApp, "top")397 mainWindow, err := window.New(context.Background(), gtkApp, "top")
@@ -613,12 +655,7 @@ func TestApp_OwnerOrLastFocusedBrowserWindowPrefersPaneOwner(t *testing.T) {
613 }655 }
614 656
615 func TestApp_HandlePaneWindowTitleChangedTargetsOwningWindow(t *testing.T) {657 func TestApp_HandlePaneWindowTitleChangedTargetsOwningWindow(t *testing.T) {
616- EnsureAdwaitaInitialized()658+ gtkApp := requireGTKDisplayApp(t)
617- appID := AppID
618- gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)
619- if gtkApp == nil {
620- t.Fatal("gtk application creation failed")
621- }
622 defer gtkApp.Unref()659 defer gtkApp.Unref()
623 660
624 firstMainWindow, err := window.New(context.Background(), gtkApp, "top")661 firstMainWindow, err := window.New(context.Background(), gtkApp, "top")
@@ -657,12 +694,7 @@ func TestApp_HandlePaneWindowTitleChangedTargetsOwningWindow(t *testing.T) {
657 }694 }
658 695
659 func TestApp_ActivateBrowserWindowResyncsTitleFromBackgroundPane(t *testing.T) {696 func TestApp_ActivateBrowserWindowResyncsTitleFromBackgroundPane(t *testing.T) {
660- EnsureAdwaitaInitialized()697+ gtkApp := requireGTKDisplayApp(t)
661- appID := AppID
662- gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)
663- if gtkApp == nil {
664- t.Fatal("gtk application creation failed")
665- }
666 defer gtkApp.Unref()698 defer gtkApp.Unref()
667 699
668 firstMainWindow, err := window.New(context.Background(), gtkApp, "top")700 firstMainWindow, err := window.New(context.Background(), gtkApp, "top")
@@ -707,12 +739,7 @@ func TestApp_ActivateBrowserWindowResyncsTitleFromBackgroundPane(t *testing.T) {
707 }739 }
708 740
709 func TestApp_HandlePaneFullscreenChangedTargetsOwningWindow(t *testing.T) {741 func TestApp_HandlePaneFullscreenChangedTargetsOwningWindow(t *testing.T) {
710- EnsureAdwaitaInitialized()742+ gtkApp := requireGTKDisplayApp(t)
711- appID := AppID
712- gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)
713- if gtkApp == nil {
714- t.Fatal("gtk application creation failed")
715- }
716 defer gtkApp.Unref()743 defer gtkApp.Unref()
717 744
718 firstMainWindow, err := window.New(context.Background(), gtkApp, "top")745 firstMainWindow, err := window.New(context.Background(), gtkApp, "top")
@@ -759,12 +786,7 @@ func TestApp_HandlePaneFullscreenChangedTargetsOwningWindow(t *testing.T) {
759 }786 }
760 787
761 func TestApp_HandlePaneFullscreenChangedIgnoresUnresolvedPane(t *testing.T) {788 func TestApp_HandlePaneFullscreenChangedIgnoresUnresolvedPane(t *testing.T) {
762- EnsureAdwaitaInitialized()789+ gtkApp := requireGTKDisplayApp(t)
763- appID := AppID
764- gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)
765- if gtkApp == nil {
766- t.Fatal("gtk application creation failed")
767- }
768 defer gtkApp.Unref()790 defer gtkApp.Unref()
769 791
770 firstMainWindow, err := window.New(context.Background(), gtkApp, "top")792 firstMainWindow, err := window.New(context.Background(), gtkApp, "top")
@@ -881,12 +903,7 @@ func TestApp_MoveActivePaneToTabFromBrowserWindowActivatesTargetOwnerForCrossWin
881 }903 }
882 904
883 func TestApp_InitKeyboardHandlerDoesNotReattachExistingWindowInput(t *testing.T) {905 func TestApp_InitKeyboardHandlerDoesNotReattachExistingWindowInput(t *testing.T) {
884- EnsureAdwaitaInitialized()906+ gtkApp := requireGTKDisplayApp(t)
885- appID := AppID
886- gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)
887- if gtkApp == nil {
888- t.Fatal("gtk application creation failed")
889- }
890 defer gtkApp.Unref()907 defer gtkApp.Unref()
891 908
892 mainWindow, err := window.New(context.Background(), gtkApp, "top")909 mainWindow, err := window.New(context.Background(), gtkApp, "top")
@@ -946,12 +963,7 @@ func (*testFocusedInputProvider) GetFocusedInput() port.TextInputTarget { return
946 func (*testFocusedInputProvider) SetFocusedInput(port.TextInputTarget) {}963 func (*testFocusedInputProvider) SetFocusedInput(port.TextInputTarget) {}
947 964
948 func TestApp_UpdateBrowserWindowTabBarVisibilityHonorsHideWhenSingleTabDisabled(t *testing.T) {965 func TestApp_UpdateBrowserWindowTabBarVisibilityHonorsHideWhenSingleTabDisabled(t *testing.T) {
949- EnsureAdwaitaInitialized()966+ gtkApp := requireGTKDisplayApp(t)
950- appID := AppID
951- gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)
952- if gtkApp == nil {
953- t.Fatal("gtk application creation failed")
954- }
955 defer gtkApp.Unref()967 defer gtkApp.Unref()
956 968
957 mainWindow := &window.MainWindow{}969 mainWindow := &window.MainWindow{}

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