fix(context-menu): preserve fallback error paths
- commit
- 3e836caf563f8a35249b9d12af007198f617ee51
- author
- brice · · 2026-04-14 06:14 UTC
- committer
- brice · · 2026-04-14 12:06 UTC
- parent
- 366c2e5
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +158 -4
M internal/infrastructure/cef/context_menu.go +1 -2
| @@ -68,8 +68,7 @@ func (h *handlerSet) RunContextMenu( | |||
| 68 | 68 | } | |
| 69 | 69 | } | |
| 70 | 70 | if h.wv == nil || h.wv.pipeline == nil || h.wv.pipeline.glArea == nil { | |
| 71 | - | callback.Cancel() | |
| 72 | - | return 1 | |
| 71 | + | return 0 | |
| 73 | 72 | } | |
| 74 | 73 | ||
| 75 | 74 | x, y := contextMenuAnchorPosition(params, h.wv.pipeline.scale) | |
| @@ -68,8 +68,7 @@ func (h *handlerSet) RunContextMenu( | |||||
| 68 | } | 68 | } | ||
| 69 | } | 69 | } | ||
| 70 | if h.wv == nil || h.wv.pipeline == nil || h.wv.pipeline.glArea == nil { | 70 | if h.wv == nil || h.wv.pipeline == nil || h.wv.pipeline.glArea == nil { | ||
| 71 | - | callback.Cancel() | 71 | + | return 0 |
| 72 | - | return 1 | |||
| 73 | } | 72 | } | ||
| 74 | 73 | ||||
| 75 | x, y := contextMenuAnchorPosition(params, h.wv.pipeline.scale) | 74 | x, y := contextMenuAnchorPosition(params, h.wv.pipeline.scale) | ||
A internal/infrastructure/cef/context_menu_run_context_menu_test.go +117 -0
| @@ -0,0 +1,117 @@ | |||
| 1 | + | package cef | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "testing" | |
| 6 | + | "unsafe" | |
| 7 | + | ||
| 8 | + | purecef "github.com/bnema/purego-cef/cef" | |
| 9 | + | ||
| 10 | + | "github.com/bnema/dumber/internal/application/port" | |
| 11 | + | "github.com/stretchr/testify/require" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | type stubMenuModel struct{} | |
| 15 | + | ||
| 16 | + | func (stubMenuModel) IsSubMenu() bool { return false } | |
| 17 | + | func (stubMenuModel) Clear() int32 { panic("unexpected call") } | |
| 18 | + | func (stubMenuModel) GetCount() int { return 1 } | |
| 19 | + | func (stubMenuModel) AddSeparator() int32 { panic("unexpected call") } | |
| 20 | + | func (stubMenuModel) AddItem(commandID int32, label string) int32 { panic("unexpected call") } | |
| 21 | + | func (stubMenuModel) AddCheckItem(commandID int32, label string) int32 { panic("unexpected call") } | |
| 22 | + | func (stubMenuModel) AddRadioItem(commandID int32, label string, groupID int32) int32 { | |
| 23 | + | panic("unexpected call") | |
| 24 | + | } | |
| 25 | + | func (stubMenuModel) AddSubMenu(commandID int32, label string) purecef.MenuModel { | |
| 26 | + | panic("unexpected call") | |
| 27 | + | } | |
| 28 | + | func (stubMenuModel) InsertSeparatorAt(index int) int32 { panic("unexpected call") } | |
| 29 | + | func (stubMenuModel) InsertItemAt(index int, commandID int32, label string) int32 { | |
| 30 | + | panic("unexpected call") | |
| 31 | + | } | |
| 32 | + | func (stubMenuModel) InsertCheckItemAt(index int, commandID int32, label string) int32 { | |
| 33 | + | panic("unexpected call") | |
| 34 | + | } | |
| 35 | + | func (stubMenuModel) InsertRadioItemAt(index int, commandID int32, label string, groupID int32) int32 { | |
| 36 | + | panic("unexpected call") | |
| 37 | + | } | |
| 38 | + | func (stubMenuModel) InsertSubMenuAt(index int, commandID int32, label string) purecef.MenuModel { | |
| 39 | + | panic("unexpected call") | |
| 40 | + | } | |
| 41 | + | func (stubMenuModel) Remove(commandID int32) int32 { panic("unexpected call") } | |
| 42 | + | func (stubMenuModel) RemoveAt(index int) int32 { panic("unexpected call") } | |
| 43 | + | func (stubMenuModel) GetIndexOf(commandID int32) int32 { panic("unexpected call") } | |
| 44 | + | func (stubMenuModel) GetCommandIDAt(index int) int32 { return 101 } | |
| 45 | + | func (stubMenuModel) SetCommandIDAt(index int, commandID int32) int32 { panic("unexpected call") } | |
| 46 | + | func (stubMenuModel) GetLabel(commandID int32) string { panic("unexpected call") } | |
| 47 | + | func (stubMenuModel) GetLabelAt(index int) string { return "Copy" } | |
| 48 | + | func (stubMenuModel) SetLabel(commandID int32, label string) int32 { panic("unexpected call") } | |
| 49 | + | func (stubMenuModel) SetLabelAt(index int, label string) int32 { panic("unexpected call") } | |
| 50 | + | func (stubMenuModel) GetType(commandID int32) purecef.MenuItemType { panic("unexpected call") } | |
| 51 | + | func (stubMenuModel) GetTypeAt(index int) purecef.MenuItemType { | |
| 52 | + | return purecef.MenuItemTypeMenuitemtypeCommand | |
| 53 | + | } | |
| 54 | + | func (stubMenuModel) GetGroupID(commandID int32) int32 { panic("unexpected call") } | |
| 55 | + | func (stubMenuModel) GetGroupIDAt(index int) int32 { panic("unexpected call") } | |
| 56 | + | func (stubMenuModel) SetGroupID(commandID int32, groupID int32) int32 { panic("unexpected call") } | |
| 57 | + | func (stubMenuModel) SetGroupIDAt(index int, groupID int32) int32 { panic("unexpected call") } | |
| 58 | + | func (stubMenuModel) GetSubMenu(commandID int32) purecef.MenuModel { panic("unexpected call") } | |
| 59 | + | func (stubMenuModel) GetSubMenuAt(index int) purecef.MenuModel { panic("unexpected call") } | |
| 60 | + | func (stubMenuModel) IsVisible(commandID int32) bool { panic("unexpected call") } | |
| 61 | + | func (stubMenuModel) IsVisibleAt(index int) bool { panic("unexpected call") } | |
| 62 | + | func (stubMenuModel) SetVisible(commandID int32, visible int32) int32 { panic("unexpected call") } | |
| 63 | + | func (stubMenuModel) SetVisibleAt(index int, visible int32) int32 { panic("unexpected call") } | |
| 64 | + | func (stubMenuModel) IsEnabled(commandID int32) bool { panic("unexpected call") } | |
| 65 | + | func (stubMenuModel) IsEnabledAt(index int) bool { panic("unexpected call") } | |
| 66 | + | func (stubMenuModel) SetEnabled(commandID int32, enabled int32) int32 { panic("unexpected call") } | |
| 67 | + | func (stubMenuModel) SetEnabledAt(index int, enabled int32) int32 { panic("unexpected call") } | |
| 68 | + | func (stubMenuModel) IsChecked(commandID int32) bool { panic("unexpected call") } | |
| 69 | + | func (stubMenuModel) IsCheckedAt(index int) bool { panic("unexpected call") } | |
| 70 | + | func (stubMenuModel) SetChecked(commandID int32, checked int32) int32 { panic("unexpected call") } | |
| 71 | + | func (stubMenuModel) SetCheckedAt(index int, checked int32) int32 { panic("unexpected call") } | |
| 72 | + | func (stubMenuModel) HasAccelerator(commandID int32) bool { panic("unexpected call") } | |
| 73 | + | func (stubMenuModel) HasAcceleratorAt(index int) bool { panic("unexpected call") } | |
| 74 | + | func (stubMenuModel) SetAccelerator(commandID int32, keyCode int32, shiftPressed int32, ctrlPressed int32, altPressed int32) int32 { | |
| 75 | + | panic("unexpected call") | |
| 76 | + | } | |
| 77 | + | func (stubMenuModel) SetAcceleratorAt(index int, keyCode int32, shiftPressed int32, ctrlPressed int32, altPressed int32) int32 { | |
| 78 | + | panic("unexpected call") | |
| 79 | + | } | |
| 80 | + | func (stubMenuModel) RemoveAccelerator(commandID int32) int32 { panic("unexpected call") } | |
| 81 | + | func (stubMenuModel) RemoveAcceleratorAt(index int) int32 { panic("unexpected call") } | |
| 82 | + | func (stubMenuModel) GetAccelerator(commandID int32, keyCode unsafe.Pointer, shiftPressed unsafe.Pointer, ctrlPressed unsafe.Pointer, altPressed unsafe.Pointer) int32 { | |
| 83 | + | panic("unexpected call") | |
| 84 | + | } | |
| 85 | + | func (stubMenuModel) GetAcceleratorAt(index int, keyCode unsafe.Pointer, shiftPressed unsafe.Pointer, ctrlPressed unsafe.Pointer, altPressed unsafe.Pointer) int32 { | |
| 86 | + | panic("unexpected call") | |
| 87 | + | } | |
| 88 | + | func (stubMenuModel) SetColor(commandID int32, colorType purecef.MenuColorType, color uintptr) int32 { | |
| 89 | + | panic("unexpected call") | |
| 90 | + | } | |
| 91 | + | func (stubMenuModel) SetColorAt(index int32, colorType purecef.MenuColorType, color uintptr) int32 { | |
| 92 | + | panic("unexpected call") | |
| 93 | + | } | |
| 94 | + | func (stubMenuModel) GetColor(commandID int32, colorType purecef.MenuColorType, color uintptr) int32 { | |
| 95 | + | panic("unexpected call") | |
| 96 | + | } | |
| 97 | + | func (stubMenuModel) GetColorAt(index int32, colorType purecef.MenuColorType, color uintptr) int32 { | |
| 98 | + | panic("unexpected call") | |
| 99 | + | } | |
| 100 | + | func (stubMenuModel) SetFontList(commandID int32, fontList string) int32 { panic("unexpected call") } | |
| 101 | + | func (stubMenuModel) SetFontListAt(index int32, fontList string) int32 { panic("unexpected call") } | |
| 102 | + | ||
| 103 | + | type stubContextMenuBuilder struct{ items []port.MenuItem } | |
| 104 | + | ||
| 105 | + | func (b stubContextMenuBuilder) Build(context.Context, port.MenuContext) []port.MenuItem { | |
| 106 | + | return b.items | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | func TestRunContextMenuReturnsZeroWhenAnchorMissing(t *testing.T) { | |
| 110 | + | h := &handlerSet{wv: &WebView{engine: &Engine{ctxMenuBuilder: stubContextMenuBuilder{items: []port.MenuItem{{Action: port.MenuActionCopySelection, Label: "Copy"}}}}}} | |
| 111 | + | callback := &stubRunContextMenuCallback{} | |
| 112 | + | ||
| 113 | + | result := h.RunContextMenu(nil, nil, stubContextMenuParams{}, stubMenuModel{}, callback) | |
| 114 | + | ||
| 115 | + | require.Equal(t, int32(0), result) | |
| 116 | + | require.Zero(t, callback.cancelCalls) | |
| 117 | + | } | |
| @@ -0,0 +1,117 @@ | |||||
| 1 | + | package cef | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | "unsafe" | |||
| 7 | + | ||||
| 8 | + | purecef "github.com/bnema/purego-cef/cef" | |||
| 9 | + | ||||
| 10 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 11 | + | "github.com/stretchr/testify/require" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | type stubMenuModel struct{} | |||
| 15 | + | ||||
| 16 | + | func (stubMenuModel) IsSubMenu() bool { return false } | |||
| 17 | + | func (stubMenuModel) Clear() int32 { panic("unexpected call") } | |||
| 18 | + | func (stubMenuModel) GetCount() int { return 1 } | |||
| 19 | + | func (stubMenuModel) AddSeparator() int32 { panic("unexpected call") } | |||
| 20 | + | func (stubMenuModel) AddItem(commandID int32, label string) int32 { panic("unexpected call") } | |||
| 21 | + | func (stubMenuModel) AddCheckItem(commandID int32, label string) int32 { panic("unexpected call") } | |||
| 22 | + | func (stubMenuModel) AddRadioItem(commandID int32, label string, groupID int32) int32 { | |||
| 23 | + | panic("unexpected call") | |||
| 24 | + | } | |||
| 25 | + | func (stubMenuModel) AddSubMenu(commandID int32, label string) purecef.MenuModel { | |||
| 26 | + | panic("unexpected call") | |||
| 27 | + | } | |||
| 28 | + | func (stubMenuModel) InsertSeparatorAt(index int) int32 { panic("unexpected call") } | |||
| 29 | + | func (stubMenuModel) InsertItemAt(index int, commandID int32, label string) int32 { | |||
| 30 | + | panic("unexpected call") | |||
| 31 | + | } | |||
| 32 | + | func (stubMenuModel) InsertCheckItemAt(index int, commandID int32, label string) int32 { | |||
| 33 | + | panic("unexpected call") | |||
| 34 | + | } | |||
| 35 | + | func (stubMenuModel) InsertRadioItemAt(index int, commandID int32, label string, groupID int32) int32 { | |||
| 36 | + | panic("unexpected call") | |||
| 37 | + | } | |||
| 38 | + | func (stubMenuModel) InsertSubMenuAt(index int, commandID int32, label string) purecef.MenuModel { | |||
| 39 | + | panic("unexpected call") | |||
| 40 | + | } | |||
| 41 | + | func (stubMenuModel) Remove(commandID int32) int32 { panic("unexpected call") } | |||
| 42 | + | func (stubMenuModel) RemoveAt(index int) int32 { panic("unexpected call") } | |||
| 43 | + | func (stubMenuModel) GetIndexOf(commandID int32) int32 { panic("unexpected call") } | |||
| 44 | + | func (stubMenuModel) GetCommandIDAt(index int) int32 { return 101 } | |||
| 45 | + | func (stubMenuModel) SetCommandIDAt(index int, commandID int32) int32 { panic("unexpected call") } | |||
| 46 | + | func (stubMenuModel) GetLabel(commandID int32) string { panic("unexpected call") } | |||
| 47 | + | func (stubMenuModel) GetLabelAt(index int) string { return "Copy" } | |||
| 48 | + | func (stubMenuModel) SetLabel(commandID int32, label string) int32 { panic("unexpected call") } | |||
| 49 | + | func (stubMenuModel) SetLabelAt(index int, label string) int32 { panic("unexpected call") } | |||
| 50 | + | func (stubMenuModel) GetType(commandID int32) purecef.MenuItemType { panic("unexpected call") } | |||
| 51 | + | func (stubMenuModel) GetTypeAt(index int) purecef.MenuItemType { | |||
| 52 | + | return purecef.MenuItemTypeMenuitemtypeCommand | |||
| 53 | + | } | |||
| 54 | + | func (stubMenuModel) GetGroupID(commandID int32) int32 { panic("unexpected call") } | |||
| 55 | + | func (stubMenuModel) GetGroupIDAt(index int) int32 { panic("unexpected call") } | |||
| 56 | + | func (stubMenuModel) SetGroupID(commandID int32, groupID int32) int32 { panic("unexpected call") } | |||
| 57 | + | func (stubMenuModel) SetGroupIDAt(index int, groupID int32) int32 { panic("unexpected call") } | |||
| 58 | + | func (stubMenuModel) GetSubMenu(commandID int32) purecef.MenuModel { panic("unexpected call") } | |||
| 59 | + | func (stubMenuModel) GetSubMenuAt(index int) purecef.MenuModel { panic("unexpected call") } | |||
| 60 | + | func (stubMenuModel) IsVisible(commandID int32) bool { panic("unexpected call") } | |||
| 61 | + | func (stubMenuModel) IsVisibleAt(index int) bool { panic("unexpected call") } | |||
| 62 | + | func (stubMenuModel) SetVisible(commandID int32, visible int32) int32 { panic("unexpected call") } | |||
| 63 | + | func (stubMenuModel) SetVisibleAt(index int, visible int32) int32 { panic("unexpected call") } | |||
| 64 | + | func (stubMenuModel) IsEnabled(commandID int32) bool { panic("unexpected call") } | |||
| 65 | + | func (stubMenuModel) IsEnabledAt(index int) bool { panic("unexpected call") } | |||
| 66 | + | func (stubMenuModel) SetEnabled(commandID int32, enabled int32) int32 { panic("unexpected call") } | |||
| 67 | + | func (stubMenuModel) SetEnabledAt(index int, enabled int32) int32 { panic("unexpected call") } | |||
| 68 | + | func (stubMenuModel) IsChecked(commandID int32) bool { panic("unexpected call") } | |||
| 69 | + | func (stubMenuModel) IsCheckedAt(index int) bool { panic("unexpected call") } | |||
| 70 | + | func (stubMenuModel) SetChecked(commandID int32, checked int32) int32 { panic("unexpected call") } | |||
| 71 | + | func (stubMenuModel) SetCheckedAt(index int, checked int32) int32 { panic("unexpected call") } | |||
| 72 | + | func (stubMenuModel) HasAccelerator(commandID int32) bool { panic("unexpected call") } | |||
| 73 | + | func (stubMenuModel) HasAcceleratorAt(index int) bool { panic("unexpected call") } | |||
| 74 | + | func (stubMenuModel) SetAccelerator(commandID int32, keyCode int32, shiftPressed int32, ctrlPressed int32, altPressed int32) int32 { | |||
| 75 | + | panic("unexpected call") | |||
| 76 | + | } | |||
| 77 | + | func (stubMenuModel) SetAcceleratorAt(index int, keyCode int32, shiftPressed int32, ctrlPressed int32, altPressed int32) int32 { | |||
| 78 | + | panic("unexpected call") | |||
| 79 | + | } | |||
| 80 | + | func (stubMenuModel) RemoveAccelerator(commandID int32) int32 { panic("unexpected call") } | |||
| 81 | + | func (stubMenuModel) RemoveAcceleratorAt(index int) int32 { panic("unexpected call") } | |||
| 82 | + | func (stubMenuModel) GetAccelerator(commandID int32, keyCode unsafe.Pointer, shiftPressed unsafe.Pointer, ctrlPressed unsafe.Pointer, altPressed unsafe.Pointer) int32 { | |||
| 83 | + | panic("unexpected call") | |||
| 84 | + | } | |||
| 85 | + | func (stubMenuModel) GetAcceleratorAt(index int, keyCode unsafe.Pointer, shiftPressed unsafe.Pointer, ctrlPressed unsafe.Pointer, altPressed unsafe.Pointer) int32 { | |||
| 86 | + | panic("unexpected call") | |||
| 87 | + | } | |||
| 88 | + | func (stubMenuModel) SetColor(commandID int32, colorType purecef.MenuColorType, color uintptr) int32 { | |||
| 89 | + | panic("unexpected call") | |||
| 90 | + | } | |||
| 91 | + | func (stubMenuModel) SetColorAt(index int32, colorType purecef.MenuColorType, color uintptr) int32 { | |||
| 92 | + | panic("unexpected call") | |||
| 93 | + | } | |||
| 94 | + | func (stubMenuModel) GetColor(commandID int32, colorType purecef.MenuColorType, color uintptr) int32 { | |||
| 95 | + | panic("unexpected call") | |||
| 96 | + | } | |||
| 97 | + | func (stubMenuModel) GetColorAt(index int32, colorType purecef.MenuColorType, color uintptr) int32 { | |||
| 98 | + | panic("unexpected call") | |||
| 99 | + | } | |||
| 100 | + | func (stubMenuModel) SetFontList(commandID int32, fontList string) int32 { panic("unexpected call") } | |||
| 101 | + | func (stubMenuModel) SetFontListAt(index int32, fontList string) int32 { panic("unexpected call") } | |||
| 102 | + | ||||
| 103 | + | type stubContextMenuBuilder struct{ items []port.MenuItem } | |||
| 104 | + | ||||
| 105 | + | func (b stubContextMenuBuilder) Build(context.Context, port.MenuContext) []port.MenuItem { | |||
| 106 | + | return b.items | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | func TestRunContextMenuReturnsZeroWhenAnchorMissing(t *testing.T) { | |||
| 110 | + | h := &handlerSet{wv: &WebView{engine: &Engine{ctxMenuBuilder: stubContextMenuBuilder{items: []port.MenuItem{{Action: port.MenuActionCopySelection, Label: "Copy"}}}}}} | |||
| 111 | + | callback := &stubRunContextMenuCallback{} | |||
| 112 | + | ||||
| 113 | + | result := h.RunContextMenu(nil, nil, stubContextMenuParams{}, stubMenuModel{}, callback) | |||
| 114 | + | ||||
| 115 | + | require.Equal(t, int32(0), result) | |||
| 116 | + | require.Zero(t, callback.cancelCalls) | |||
| 117 | + | } | |||
M internal/infrastructure/clipboard/clipboard.go +12 -0
| @@ -150,6 +150,7 @@ func selectSystemClipboard() (copyCmd, pasteCmd string) { | |||
| 150 | 150 | func (a *Adapter) WriteText(ctx context.Context, text string) error { | |
| 151 | 151 | log := logging.FromContext(ctx) | |
| 152 | 152 | var commandErr error | |
| 153 | + | var toolkitErr error | |
| 153 | 154 | if a.copyCmd != "" { | |
| 154 | 155 | err := a.writeTextWithCommand(ctx, text, log) | |
| 155 | 156 | if err == nil { | |
| @@ -163,11 +164,16 @@ func (a *Adapter) WriteText(ctx context.Context, text string) error { | |||
| 163 | 164 | log.Debug().Str("backend", "toolkit").Int("len", len(text)).Msg("clipboard write success") | |
| 164 | 165 | return nil | |
| 165 | 166 | } | |
| 167 | + | toolkitErr = err | |
| 166 | 168 | log.Debug().Err(err).Msg("toolkit clipboard write failed; falling back") | |
| 167 | 169 | } | |
| 168 | 170 | if commandErr != nil { | |
| 169 | 171 | return commandErr | |
| 170 | 172 | } | |
| 173 | + | if toolkitErr != nil { | |
| 174 | + | log.Error().Err(toolkitErr).Msg("clipboard write failed") | |
| 175 | + | return toolkitErr | |
| 176 | + | } | |
| 171 | 177 | err := fmt.Errorf("no clipboard tool available (install wl-clipboard or xclip)") | |
| 172 | 178 | log.Error().Err(err).Msg("clipboard write failed") | |
| 173 | 179 | return err | |
| @@ -179,6 +185,7 @@ func (a *Adapter) WriteText(ctx context.Context, text string) error { | |||
| 179 | 185 | func (a *Adapter) WriteImage(ctx context.Context, image entity.ImageData) error { | |
| 180 | 186 | log := logging.FromContext(ctx) | |
| 181 | 187 | var commandErr error | |
| 188 | + | var toolkitErr error | |
| 182 | 189 | ||
| 183 | 190 | if len(image.Bytes) == 0 { | |
| 184 | 191 | err := fmt.Errorf("empty image data") | |
| @@ -195,6 +202,7 @@ func (a *Adapter) WriteImage(ctx context.Context, image entity.ImageData) error | |||
| 195 | 202 | Msg("clipboard image write success") | |
| 196 | 203 | return nil | |
| 197 | 204 | } | |
| 205 | + | toolkitErr = err | |
| 198 | 206 | log.Debug().Err(err).Msg("toolkit clipboard image write failed; falling back") | |
| 199 | 207 | } | |
| 200 | 208 | if a.copyCmd != "" { | |
| @@ -207,6 +215,10 @@ func (a *Adapter) WriteImage(ctx context.Context, image entity.ImageData) error | |||
| 207 | 215 | if commandErr != nil { | |
| 208 | 216 | return commandErr | |
| 209 | 217 | } | |
| 218 | + | if toolkitErr != nil { | |
| 219 | + | log.Error().Err(toolkitErr).Msg("clipboard image write failed") | |
| 220 | + | return toolkitErr | |
| 221 | + | } | |
| 210 | 222 | err := fmt.Errorf("no clipboard tool available (install wl-clipboard or xclip)") | |
| 211 | 223 | log.Error().Err(err).Msg("clipboard image write failed") | |
| 212 | 224 | return err | |
| @@ -150,6 +150,7 @@ func selectSystemClipboard() (copyCmd, pasteCmd string) { | |||||
| 150 | func (a *Adapter) WriteText(ctx context.Context, text string) error { | 150 | func (a *Adapter) WriteText(ctx context.Context, text string) error { | ||
| 151 | log := logging.FromContext(ctx) | 151 | log := logging.FromContext(ctx) | ||
| 152 | var commandErr error | 152 | var commandErr error | ||
| 153 | + | var toolkitErr error | |||
| 153 | if a.copyCmd != "" { | 154 | if a.copyCmd != "" { | ||
| 154 | err := a.writeTextWithCommand(ctx, text, log) | 155 | err := a.writeTextWithCommand(ctx, text, log) | ||
| 155 | if err == nil { | 156 | if err == nil { | ||
| @@ -163,11 +164,16 @@ func (a *Adapter) WriteText(ctx context.Context, text string) error { | |||||
| 163 | log.Debug().Str("backend", "toolkit").Int("len", len(text)).Msg("clipboard write success") | 164 | log.Debug().Str("backend", "toolkit").Int("len", len(text)).Msg("clipboard write success") | ||
| 164 | return nil | 165 | return nil | ||
| 165 | } | 166 | } | ||
| 167 | + | toolkitErr = err | |||
| 166 | log.Debug().Err(err).Msg("toolkit clipboard write failed; falling back") | 168 | log.Debug().Err(err).Msg("toolkit clipboard write failed; falling back") | ||
| 167 | } | 169 | } | ||
| 168 | if commandErr != nil { | 170 | if commandErr != nil { | ||
| 169 | return commandErr | 171 | return commandErr | ||
| 170 | } | 172 | } | ||
| 173 | + | if toolkitErr != nil { | |||
| 174 | + | log.Error().Err(toolkitErr).Msg("clipboard write failed") | |||
| 175 | + | return toolkitErr | |||
| 176 | + | } | |||
| 171 | err := fmt.Errorf("no clipboard tool available (install wl-clipboard or xclip)") | 177 | err := fmt.Errorf("no clipboard tool available (install wl-clipboard or xclip)") | ||
| 172 | log.Error().Err(err).Msg("clipboard write failed") | 178 | log.Error().Err(err).Msg("clipboard write failed") | ||
| 173 | return err | 179 | return err | ||
| @@ -179,6 +185,7 @@ func (a *Adapter) WriteText(ctx context.Context, text string) error { | |||||
| 179 | func (a *Adapter) WriteImage(ctx context.Context, image entity.ImageData) error { | 185 | func (a *Adapter) WriteImage(ctx context.Context, image entity.ImageData) error { | ||
| 180 | log := logging.FromContext(ctx) | 186 | log := logging.FromContext(ctx) | ||
| 181 | var commandErr error | 187 | var commandErr error | ||
| 188 | + | var toolkitErr error | |||
| 182 | 189 | ||||
| 183 | if len(image.Bytes) == 0 { | 190 | if len(image.Bytes) == 0 { | ||
| 184 | err := fmt.Errorf("empty image data") | 191 | err := fmt.Errorf("empty image data") | ||
| @@ -195,6 +202,7 @@ func (a *Adapter) WriteImage(ctx context.Context, image entity.ImageData) error | |||||
| 195 | Msg("clipboard image write success") | 202 | Msg("clipboard image write success") | ||
| 196 | return nil | 203 | return nil | ||
| 197 | } | 204 | } | ||
| 205 | + | toolkitErr = err | |||
| 198 | log.Debug().Err(err).Msg("toolkit clipboard image write failed; falling back") | 206 | log.Debug().Err(err).Msg("toolkit clipboard image write failed; falling back") | ||
| 199 | } | 207 | } | ||
| 200 | if a.copyCmd != "" { | 208 | if a.copyCmd != "" { | ||
| @@ -207,6 +215,10 @@ func (a *Adapter) WriteImage(ctx context.Context, image entity.ImageData) error | |||||
| 207 | if commandErr != nil { | 215 | if commandErr != nil { | ||
| 208 | return commandErr | 216 | return commandErr | ||
| 209 | } | 217 | } | ||
| 218 | + | if toolkitErr != nil { | |||
| 219 | + | log.Error().Err(toolkitErr).Msg("clipboard image write failed") | |||
| 220 | + | return toolkitErr | |||
| 221 | + | } | |||
| 210 | err := fmt.Errorf("no clipboard tool available (install wl-clipboard or xclip)") | 222 | err := fmt.Errorf("no clipboard tool available (install wl-clipboard or xclip)") | ||
| 211 | log.Error().Err(err).Msg("clipboard image write failed") | 223 | log.Error().Err(err).Msg("clipboard image write failed") | ||
| 212 | return err | 224 | return err | ||
M internal/infrastructure/clipboard/clipboard_test.go +28 -2
| @@ -17,18 +17,44 @@ type fakeToolkitClipboard struct { | |||
| 17 | 17 | writeImageCalls int | |
| 18 | 18 | text string | |
| 19 | 19 | image entity.ImageData | |
| 20 | + | writeTextErr error | |
| 21 | + | writeImageErr error | |
| 20 | 22 | } | |
| 21 | 23 | ||
| 22 | 24 | func (f *fakeToolkitClipboard) WriteText(_ context.Context, text string) error { | |
| 23 | 25 | f.writeTextCalls++ | |
| 24 | 26 | f.text = text | |
| 25 | - | return nil | |
| 27 | + | return f.writeTextErr | |
| 26 | 28 | } | |
| 27 | 29 | ||
| 28 | 30 | func (f *fakeToolkitClipboard) WriteImage(_ context.Context, image entity.ImageData) error { | |
| 29 | 31 | f.writeImageCalls++ | |
| 30 | 32 | f.image = image | |
| 31 | - | return nil | |
| 33 | + | return f.writeImageErr | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | func TestAdapter_WriteTextReturnsToolkitErrorWhenNoSystemTool(t *testing.T) { | |
| 37 | + | sentinel := errors.New("toolkit text failed") | |
| 38 | + | fake := &fakeToolkitClipboard{writeTextErr: sentinel} | |
| 39 | + | adapter := &Adapter{toolkit: fake} | |
| 40 | + | ||
| 41 | + | err := adapter.WriteText(context.Background(), "hello") | |
| 42 | + | ||
| 43 | + | require.Equal(t, sentinel, err) | |
| 44 | + | require.Equal(t, 1, fake.writeTextCalls) | |
| 45 | + | require.Equal(t, "hello", fake.text) | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | func TestAdapter_WriteImageReturnsToolkitErrorWhenNoSystemTool(t *testing.T) { | |
| 49 | + | sentinel := errors.New("toolkit image failed") | |
| 50 | + | fake := &fakeToolkitClipboard{writeImageErr: sentinel} | |
| 51 | + | adapter := &Adapter{toolkit: fake} | |
| 52 | + | ||
| 53 | + | err := adapter.WriteImage(context.Background(), entity.ImageData{Bytes: []byte{1}, MimeType: "image/png"}) | |
| 54 | + | ||
| 55 | + | require.Equal(t, sentinel, err) | |
| 56 | + | require.Equal(t, 1, fake.writeImageCalls) | |
| 57 | + | require.Equal(t, entity.ImageData{Bytes: []byte{1}, MimeType: "image/png"}, fake.image) | |
| 32 | 58 | } | |
| 33 | 59 | ||
| 34 | 60 | func TestNew_FallsBackToToolkitClipboardWhenSystemToolsUnavailable(t *testing.T) { | |
| @@ -17,18 +17,44 @@ type fakeToolkitClipboard struct { | |||||
| 17 | writeImageCalls int | 17 | writeImageCalls int | ||
| 18 | text string | 18 | text string | ||
| 19 | image entity.ImageData | 19 | image entity.ImageData | ||
| 20 | + | writeTextErr error | |||
| 21 | + | writeImageErr error | |||
| 20 | } | 22 | } | ||
| 21 | 23 | ||||
| 22 | func (f *fakeToolkitClipboard) WriteText(_ context.Context, text string) error { | 24 | func (f *fakeToolkitClipboard) WriteText(_ context.Context, text string) error { | ||
| 23 | f.writeTextCalls++ | 25 | f.writeTextCalls++ | ||
| 24 | f.text = text | 26 | f.text = text | ||
| 25 | - | return nil | 27 | + | return f.writeTextErr |
| 26 | } | 28 | } | ||
| 27 | 29 | ||||
| 28 | func (f *fakeToolkitClipboard) WriteImage(_ context.Context, image entity.ImageData) error { | 30 | func (f *fakeToolkitClipboard) WriteImage(_ context.Context, image entity.ImageData) error { | ||
| 29 | f.writeImageCalls++ | 31 | f.writeImageCalls++ | ||
| 30 | f.image = image | 32 | f.image = image | ||
| 31 | - | return nil | 33 | + | return f.writeImageErr |
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | func TestAdapter_WriteTextReturnsToolkitErrorWhenNoSystemTool(t *testing.T) { | |||
| 37 | + | sentinel := errors.New("toolkit text failed") | |||
| 38 | + | fake := &fakeToolkitClipboard{writeTextErr: sentinel} | |||
| 39 | + | adapter := &Adapter{toolkit: fake} | |||
| 40 | + | ||||
| 41 | + | err := adapter.WriteText(context.Background(), "hello") | |||
| 42 | + | ||||
| 43 | + | require.Equal(t, sentinel, err) | |||
| 44 | + | require.Equal(t, 1, fake.writeTextCalls) | |||
| 45 | + | require.Equal(t, "hello", fake.text) | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | func TestAdapter_WriteImageReturnsToolkitErrorWhenNoSystemTool(t *testing.T) { | |||
| 49 | + | sentinel := errors.New("toolkit image failed") | |||
| 50 | + | fake := &fakeToolkitClipboard{writeImageErr: sentinel} | |||
| 51 | + | adapter := &Adapter{toolkit: fake} | |||
| 52 | + | ||||
| 53 | + | err := adapter.WriteImage(context.Background(), entity.ImageData{Bytes: []byte{1}, MimeType: "image/png"}) | |||
| 54 | + | ||||
| 55 | + | require.Equal(t, sentinel, err) | |||
| 56 | + | require.Equal(t, 1, fake.writeImageCalls) | |||
| 57 | + | require.Equal(t, entity.ImageData{Bytes: []byte{1}, MimeType: "image/png"}, fake.image) | |||
| 32 | } | 58 | } | ||
| 33 | 59 | ||||
| 34 | func TestNew_FallsBackToToolkitClipboardWhenSystemToolsUnavailable(t *testing.T) { | 60 | func TestNew_FallsBackToToolkitClipboardWhenSystemToolsUnavailable(t *testing.T) { | ||