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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | + | "reflect" | |
| 5 | 6 | "testing" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/bnema/dumber/internal/domain/entity" | |
| 9 | + | "github.com/bnema/puregotk/v4/gtk" | |
| 8 | 10 | ) | |
| 9 | 11 | ||
| 10 | 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 := >k.Widget{} | |
| 44 | + | controller := >k.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 | 75 | func TestHoverHandlerDetachInvalidatesScheduledCallbackAndRemovesSource(t *testing.T) { | |
| 41 | 76 | h := NewHoverHandler(context.Background(), entity.PaneID("pane-1")) | |
| 42 | 77 | 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 := >k.Widget{} | |||
| 44 | + | controller := >k.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) bool | 77 | var scheduled func(uintptr) bool | ||