detached d1de38f

test(hover): require native detach ownership

commit
d1de38f3b11dad6339a9400c8191a4dcda24de59
author
brice · · 2026-07-14 07:12 UTC
committer
brice · · 2026-07-14 09:32 UTC
parent
7c3858b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +35 -0
M internal/ui/input/hover_test.go +35 -0
@@ -2,9 +2,11 @@ package input
22
33 import (
44 "context"
5+ "reflect"
56 "testing"
67
78 "github.com/bnema/dumber/internal/domain/entity"
9+ "github.com/bnema/puregotk/v4/gtk"
810 )
911
1012 func TestHoverHandlerSchedulesCallbackOnGLibAndRejectsStaleGeneration(t *testing.T) {
@@ -37,6 +39,39 @@ func TestHoverHandlerSchedulesCallbackOnGLibAndRejectsStaleGeneration(t *testing
3739 }
3840 }
3941
42+func TestHoverHandlerDetachRemovesNativeControllerAndSignalsExactlyOnce(t *testing.T) {
43+ widget := &gtk.Widget{}
44+ controller := &gtk.EventControllerMotion{}
45+ h := NewHoverHandler(context.Background(), entity.PaneID("pane-1"))
46+ h.attachedWidget = widget
47+ h.motionCtrl = controller
48+ h.enterSignalID = 1
49+ h.leaveSignalID = 2
50+ h.motionSignalID = 3
51+ var disconnected []uint
52+ var removed int
53+ h.disconnectSignal = func(_ *gtk.EventControllerMotion, id uint) { disconnected = append(disconnected, id) }
54+ h.removeController = func(gotWidget *gtk.Widget, gotController *gtk.EventController) {
55+ if gotWidget != widget || gotController != &controller.EventController {
56+ t.Fatal("detach removed the wrong native controller")
57+ }
58+ removed++
59+ }
60+
61+ h.Detach()
62+ h.Detach()
63+
64+ if got, want := disconnected, []uint{1, 2, 3}; !reflect.DeepEqual(got, want) {
65+ t.Fatalf("disconnected signals = %v, want %v", got, want)
66+ }
67+ if removed != 1 {
68+ t.Fatalf("native controllers removed = %d, want 1", removed)
69+ }
70+ if h.attachedWidget != nil || h.motionCtrl != nil || h.enterCb != nil || h.leaveCb != nil || h.motionCb != nil {
71+ t.Fatal("detach must clear native and callback ownership")
72+ }
73+}
74+
4075 func TestHoverHandlerDetachInvalidatesScheduledCallbackAndRemovesSource(t *testing.T) {
4176 h := NewHoverHandler(context.Background(), entity.PaneID("pane-1"))
4277 var scheduled func(uintptr) bool
@@ -2,9 +2,11 @@ package input
2 2
3 import (3 import (
4 "context"4 "context"
5+ "reflect"
5 "testing"6 "testing"
6 7
7 "github.com/bnema/dumber/internal/domain/entity"8 "github.com/bnema/dumber/internal/domain/entity"
9+ "github.com/bnema/puregotk/v4/gtk"
8 )10 )
9 11
10 func TestHoverHandlerSchedulesCallbackOnGLibAndRejectsStaleGeneration(t *testing.T) {12 func TestHoverHandlerSchedulesCallbackOnGLibAndRejectsStaleGeneration(t *testing.T) {
@@ -37,6 +39,39 @@ func TestHoverHandlerSchedulesCallbackOnGLibAndRejectsStaleGeneration(t *testing
37 }39 }
38 }40 }
39 41
42+func TestHoverHandlerDetachRemovesNativeControllerAndSignalsExactlyOnce(t *testing.T) {
43+ widget := &gtk.Widget{}
44+ controller := &gtk.EventControllerMotion{}
45+ h := NewHoverHandler(context.Background(), entity.PaneID("pane-1"))
46+ h.attachedWidget = widget
47+ h.motionCtrl = controller
48+ h.enterSignalID = 1
49+ h.leaveSignalID = 2
50+ h.motionSignalID = 3
51+ var disconnected []uint
52+ var removed int
53+ h.disconnectSignal = func(_ *gtk.EventControllerMotion, id uint) { disconnected = append(disconnected, id) }
54+ h.removeController = func(gotWidget *gtk.Widget, gotController *gtk.EventController) {
55+ if gotWidget != widget || gotController != &controller.EventController {
56+ t.Fatal("detach removed the wrong native controller")
57+ }
58+ removed++
59+ }
60+
61+ h.Detach()
62+ h.Detach()
63+
64+ if got, want := disconnected, []uint{1, 2, 3}; !reflect.DeepEqual(got, want) {
65+ t.Fatalf("disconnected signals = %v, want %v", got, want)
66+ }
67+ if removed != 1 {
68+ t.Fatalf("native controllers removed = %d, want 1", removed)
69+ }
70+ if h.attachedWidget != nil || h.motionCtrl != nil || h.enterCb != nil || h.leaveCb != nil || h.motionCb != nil {
71+ t.Fatal("detach must clear native and callback ownership")
72+ }
73+}
74+
40 func TestHoverHandlerDetachInvalidatesScheduledCallbackAndRemovesSource(t *testing.T) {75 func TestHoverHandlerDetachInvalidatesScheduledCallbackAndRemovesSource(t *testing.T) {
41 h := NewHoverHandler(context.Background(), entity.PaneID("pane-1"))76 h := NewHoverHandler(context.Background(), entity.PaneID("pane-1"))
42 var scheduled func(uintptr) bool77 var scheduled func(uintptr) bool

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