detached a987475

fix: update tests for signal disconnection in stacked panes

commit
a9874757a79ae249fe42258a30e1f8693f57f87c
author
brice · · 2026-01-01 14:19 UTC
parent
f9984f3
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +27 -13
M internal/ui/layout/stacked.go +23 -13
@@ -216,28 +216,38 @@ func (sv *StackedView) connectTitleBarHandlers(tb titleBarComponents, paneID str
216216
217217 // disconnectPaneSignals disconnects signal handlers from a pane's buttons.
218218 // This prevents memory leaks when panes are removed from the stack.
219+// Note: This is a no-op when using mock widgets in tests (GtkWidget returns nil).
219220 func (sv *StackedView) disconnectPaneSignals(pane *stackedPane) {
220221 if pane == nil {
221222 return
222223 }
223224
224225 // Disconnect title button click signal
225- if pane.titleButton != nil && pane.titleClickSignalID != 0 {
226- gtkWidget := pane.titleButton.GtkWidget()
227- if gtkWidget != nil {
228- obj := gobject.ObjectNewFromInternalPtr(gtkWidget.GoPointer())
229- gobject.SignalHandlerDisconnect(obj, pane.titleClickSignalID)
230- }
231- }
226+ disconnectButtonSignal(pane.titleButton, pane.titleClickSignalID)
232227
233228 // Disconnect close button click signal
234- if pane.closeButton != nil && pane.closeClickSignalID != 0 {
235- gtkWidget := pane.closeButton.GtkWidget()
236- if gtkWidget != nil {
237- obj := gobject.ObjectNewFromInternalPtr(gtkWidget.GoPointer())
238- gobject.SignalHandlerDisconnect(obj, pane.closeClickSignalID)
239- }
229+ disconnectButtonSignal(pane.closeButton, pane.closeClickSignalID)
230+}
231+
232+// disconnectButtonSignal safely disconnects a signal from a button widget.
233+// Returns silently if button is nil, signal ID is 0, or widget doesn't support GTK operations.
234+func disconnectButtonSignal(btn ButtonWidget, signalID uint32) {
235+ if btn == nil || signalID == 0 {
236+ return
240237 }
238+
239+ gtkWidget := btn.GtkWidget()
240+ if gtkWidget == nil {
241+ return
242+ }
243+
244+ ptr := gtkWidget.GoPointer()
245+ if ptr == 0 {
246+ return
247+ }
248+
249+ obj := gobject.ObjectNewFromInternalPtr(ptr)
250+ gobject.SignalHandlerDisconnect(obj, signalID)
241251 }
242252
243253 // InsertPaneAfter inserts a new pane after the specified index position.
@@ -216,28 +216,38 @@ func (sv *StackedView) connectTitleBarHandlers(tb titleBarComponents, paneID str
216 216
217 // disconnectPaneSignals disconnects signal handlers from a pane's buttons.217 // disconnectPaneSignals disconnects signal handlers from a pane's buttons.
218 // This prevents memory leaks when panes are removed from the stack.218 // This prevents memory leaks when panes are removed from the stack.
219+// Note: This is a no-op when using mock widgets in tests (GtkWidget returns nil).
219 func (sv *StackedView) disconnectPaneSignals(pane *stackedPane) {220 func (sv *StackedView) disconnectPaneSignals(pane *stackedPane) {
220 if pane == nil {221 if pane == nil {
221 return222 return
222 }223 }
223 224
224 // Disconnect title button click signal225 // Disconnect title button click signal
225- if pane.titleButton != nil && pane.titleClickSignalID != 0 {226+ disconnectButtonSignal(pane.titleButton, pane.titleClickSignalID)
226- gtkWidget := pane.titleButton.GtkWidget()
227- if gtkWidget != nil {
228- obj := gobject.ObjectNewFromInternalPtr(gtkWidget.GoPointer())
229- gobject.SignalHandlerDisconnect(obj, pane.titleClickSignalID)
230- }
231- }
232 227
233 // Disconnect close button click signal228 // Disconnect close button click signal
234- if pane.closeButton != nil && pane.closeClickSignalID != 0 {229+ disconnectButtonSignal(pane.closeButton, pane.closeClickSignalID)
235- gtkWidget := pane.closeButton.GtkWidget()230+}
236- if gtkWidget != nil {231+
237- obj := gobject.ObjectNewFromInternalPtr(gtkWidget.GoPointer())232+// disconnectButtonSignal safely disconnects a signal from a button widget.
238- gobject.SignalHandlerDisconnect(obj, pane.closeClickSignalID)233+// Returns silently if button is nil, signal ID is 0, or widget doesn't support GTK operations.
239- }234+func disconnectButtonSignal(btn ButtonWidget, signalID uint32) {
235+ if btn == nil || signalID == 0 {
236+ return
240 }237 }
238+
239+ gtkWidget := btn.GtkWidget()
240+ if gtkWidget == nil {
241+ return
242+ }
243+
244+ ptr := gtkWidget.GoPointer()
245+ if ptr == 0 {
246+ return
247+ }
248+
249+ obj := gobject.ObjectNewFromInternalPtr(ptr)
250+ gobject.SignalHandlerDisconnect(obj, signalID)
241 }251 }
242 252
243 // InsertPaneAfter inserts a new pane after the specified index position.253 // InsertPaneAfter inserts a new pane after the specified index position.
M internal/ui/layout/stacked_test.go +4 -0
@@ -75,6 +75,10 @@ func setupPaneMocks(t *testing.T, mockFactory *mocks.MockWidgetFactory, mockBox
7575 mockButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(1)).Once()
7676 mockCloseButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(2)).Once()
7777
78+ // Signal disconnection calls GtkWidget() - return nil to skip actual GTK operations in tests
79+ mockButton.EXPECT().GtkWidget().Return(nil).Maybe()
80+ mockCloseButton.EXPECT().GtkWidget().Return(nil).Maybe()
81+
7882 // Adding to main box
7983 mockBox.EXPECT().Append(mockButton).Once()
8084 mockBox.EXPECT().Append(mockContainer).Once()
@@ -75,6 +75,10 @@ func setupPaneMocks(t *testing.T, mockFactory *mocks.MockWidgetFactory, mockBox
75 mockButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(1)).Once()75 mockButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(1)).Once()
76 mockCloseButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(2)).Once()76 mockCloseButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(2)).Once()
77 77
78+ // Signal disconnection calls GtkWidget() - return nil to skip actual GTK operations in tests
79+ mockButton.EXPECT().GtkWidget().Return(nil).Maybe()
80+ mockCloseButton.EXPECT().GtkWidget().Return(nil).Maybe()
81+
78 // Adding to main box82 // Adding to main box
79 mockBox.EXPECT().Append(mockButton).Once()83 mockBox.EXPECT().Append(mockButton).Once()
80 mockBox.EXPECT().Append(mockContainer).Once()84 mockBox.EXPECT().Append(mockContainer).Once()

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