fix(context-menu): harden popup and clipboard fallbacks
- commit
- 4d39f292aa79d6012bb1218b0b50cde50e9b74a0
- author
- brice · · 2026-04-14 07:41 UTC
- committer
- brice · · 2026-04-14 12:06 UTC
- parent
- e55f218
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +129 -7
M internal/infrastructure/cef/context_menu.go +10 -2
| @@ -122,10 +122,11 @@ func logContextMenuPopupRequest( | |||
| 122 | 122 | if h == nil || h.wv == nil || h.wv.ctx == nil || h.wv.pipeline == nil || glArea == nil { | |
| 123 | 123 | return | |
| 124 | 124 | } | |
| 125 | + | rawX, rawY := contextMenuRawPosition(params) | |
| 125 | 126 | parent := glArea.GetParent() | |
| 126 | 127 | logging.FromContext(h.wv.ctx).Debug(). | |
| 127 | - | Int32("raw_x", params.GetXcoord()). | |
| 128 | - | Int32("raw_y", params.GetYcoord()). | |
| 128 | + | Int32("raw_x", rawX). | |
| 129 | + | Int32("raw_y", rawY). | |
| 129 | 130 | Int32("popup_x", x). | |
| 130 | 131 | Int32("popup_y", y). | |
| 131 | 132 | Int32("scale", h.wv.pipeline.scale). | |
| @@ -136,6 +137,13 @@ func logContextMenuPopupRequest( | |||
| 136 | 137 | Msg("cef: context menu popup request") | |
| 137 | 138 | } | |
| 138 | 139 | ||
| 140 | + | func contextMenuRawPosition(params purecef.ContextMenuParams) (int32, int32) { | |
| 141 | + | if params == nil { | |
| 142 | + | return 0, 0 | |
| 143 | + | } | |
| 144 | + | return params.GetXcoord(), params.GetYcoord() | |
| 145 | + | } | |
| 146 | + | ||
| 139 | 147 | func cefWidgetAllocatedWidth(widget *gtk.Widget) int { | |
| 140 | 148 | if widget == nil { | |
| 141 | 149 | return 0 | |
| @@ -122,10 +122,11 @@ func logContextMenuPopupRequest( | |||||
| 122 | if h == nil || h.wv == nil || h.wv.ctx == nil || h.wv.pipeline == nil || glArea == nil { | 122 | if h == nil || h.wv == nil || h.wv.ctx == nil || h.wv.pipeline == nil || glArea == nil { | ||
| 123 | return | 123 | return | ||
| 124 | } | 124 | } | ||
| 125 | + | rawX, rawY := contextMenuRawPosition(params) | |||
| 125 | parent := glArea.GetParent() | 126 | parent := glArea.GetParent() | ||
| 126 | logging.FromContext(h.wv.ctx).Debug(). | 127 | logging.FromContext(h.wv.ctx).Debug(). | ||
| 127 | - | Int32("raw_x", params.GetXcoord()). | 128 | + | Int32("raw_x", rawX). |
| 128 | - | Int32("raw_y", params.GetYcoord()). | 129 | + | Int32("raw_y", rawY). |
| 129 | Int32("popup_x", x). | 130 | Int32("popup_x", x). | ||
| 130 | Int32("popup_y", y). | 131 | Int32("popup_y", y). | ||
| 131 | Int32("scale", h.wv.pipeline.scale). | 132 | Int32("scale", h.wv.pipeline.scale). | ||
| @@ -136,6 +137,13 @@ func logContextMenuPopupRequest( | |||||
| 136 | Msg("cef: context menu popup request") | 137 | Msg("cef: context menu popup request") | ||
| 137 | } | 138 | } | ||
| 138 | 139 | ||||
| 140 | + | func contextMenuRawPosition(params purecef.ContextMenuParams) (int32, int32) { | |||
| 141 | + | if params == nil { | |||
| 142 | + | return 0, 0 | |||
| 143 | + | } | |||
| 144 | + | return params.GetXcoord(), params.GetYcoord() | |||
| 145 | + | } | |||
| 146 | + | ||||
| 139 | func cefWidgetAllocatedWidth(widget *gtk.Widget) int { | 147 | func cefWidgetAllocatedWidth(widget *gtk.Widget) int { | ||
| 140 | if widget == nil { | 148 | if widget == nil { | ||
| 141 | return 0 | 149 | return 0 | ||
M internal/infrastructure/cef/context_menu_adapter_test.go +7 -0
| @@ -172,3 +172,10 @@ func TestContextMenuAnchorPositionScalesCEFCoordinates(t *testing.T) { | |||
| 172 | 172 | require.Equal(t, int32(160), x) | |
| 173 | 173 | require.Equal(t, int32(90), y) | |
| 174 | 174 | } | |
| 175 | + | ||
| 176 | + | func TestContextMenuRawPositionDefaultsToZeroWhenParamsNil(t *testing.T) { | |
| 177 | + | x, y := contextMenuRawPosition(nil) | |
| 178 | + | ||
| 179 | + | require.Zero(t, x) | |
| 180 | + | require.Zero(t, y) | |
| 181 | + | } | |
| @@ -172,3 +172,10 @@ func TestContextMenuAnchorPositionScalesCEFCoordinates(t *testing.T) { | |||||
| 172 | require.Equal(t, int32(160), x) | 172 | require.Equal(t, int32(160), x) | ||
| 173 | require.Equal(t, int32(90), y) | 173 | require.Equal(t, int32(90), y) | ||
| 174 | } | 174 | } | ||
| 175 | + | ||||
| 176 | + | func TestContextMenuRawPositionDefaultsToZeroWhenParamsNil(t *testing.T) { | |||
| 177 | + | x, y := contextMenuRawPosition(nil) | |||
| 178 | + | ||||
| 179 | + | require.Zero(t, x) | |||
| 180 | + | require.Zero(t, y) | |||
| 181 | + | } | |||
M internal/infrastructure/clipboard/clipboard.go +83 -5
| @@ -13,11 +13,13 @@ import ( | |||
| 13 | 13 | "github.com/bnema/dumber/internal/domain/entity" | |
| 14 | 14 | "github.com/bnema/dumber/internal/logging" | |
| 15 | 15 | "github.com/bnema/puregotk/v4/gdk" | |
| 16 | + | "github.com/bnema/puregotk/v4/gio" | |
| 16 | 17 | "github.com/bnema/puregotk/v4/glib" | |
| 17 | 18 | "github.com/rs/zerolog" | |
| 18 | 19 | ) | |
| 19 | 20 | ||
| 20 | 21 | type toolkitClipboard interface { | |
| 22 | + | ReadText(ctx context.Context) (string, error) | |
| 21 | 23 | WriteText(ctx context.Context, text string) error | |
| 22 | 24 | WriteImage(ctx context.Context, image entity.ImageData) error | |
| 23 | 25 | } | |
| @@ -82,6 +84,49 @@ func (c *gdkToolkitClipboard) WriteText(ctx context.Context, text string) error | |||
| 82 | 84 | }) | |
| 83 | 85 | } | |
| 84 | 86 | ||
| 87 | + | func (c *gdkToolkitClipboard) ReadText(ctx context.Context) (string, error) { | |
| 88 | + | var text string | |
| 89 | + | err := withToolkitClipboard(ctx, func() error { | |
| 90 | + | if c == nil || c.clipboard == nil { | |
| 91 | + | return fmt.Errorf("toolkit clipboard not available") | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | resultCh := make(chan struct { | |
| 95 | + | text string | |
| 96 | + | err error | |
| 97 | + | }, 1) | |
| 98 | + | cb := gio.AsyncReadyCallback(func(_ uintptr, result uintptr, _ uintptr) { | |
| 99 | + | asyncResult := &gio.AsyncResultBase{} | |
| 100 | + | asyncResult.SetGoPointer(result) | |
| 101 | + | readText, readErr := c.clipboard.ReadTextFinish(asyncResult) | |
| 102 | + | resultCh <- struct { | |
| 103 | + | text string | |
| 104 | + | err error | |
| 105 | + | }{text: readText, err: readErr} | |
| 106 | + | }) | |
| 107 | + | c.clipboard.ReadTextAsync(nil, &cb, 0) | |
| 108 | + | ||
| 109 | + | mainContext := glib.MainContextDefault() | |
| 110 | + | if mainContext == nil { | |
| 111 | + | return fmt.Errorf("toolkit main context not available") | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | for { | |
| 115 | + | select { | |
| 116 | + | case result := <-resultCh: | |
| 117 | + | text = result.text | |
| 118 | + | return result.err | |
| 119 | + | default: | |
| 120 | + | } | |
| 121 | + | if ctx.Err() != nil { | |
| 122 | + | return ctx.Err() | |
| 123 | + | } | |
| 124 | + | mainContext.Iteration(true) | |
| 125 | + | } | |
| 126 | + | }) | |
| 127 | + | return text, err | |
| 128 | + | } | |
| 129 | + | ||
| 85 | 130 | func (c *gdkToolkitClipboard) WriteImage(ctx context.Context, image entity.ImageData) error { | |
| 86 | 131 | return withToolkitClipboard(ctx, func() error { | |
| 87 | 132 | if c == nil || c.clipboard == nil { | |
| @@ -288,10 +333,7 @@ func (a *Adapter) writeImageWithCommand(ctx context.Context, image entity.ImageD | |||
| 288 | 333 | return nil | |
| 289 | 334 | } | |
| 290 | 335 | ||
| 291 | - | // ReadText reads text from the clipboard. | |
| 292 | - | func (a *Adapter) ReadText(ctx context.Context) (string, error) { | |
| 293 | - | log := logging.FromContext(ctx) | |
| 294 | - | ||
| 336 | + | func (a *Adapter) readTextWithCommand(ctx context.Context, log *zerolog.Logger) (string, error) { | |
| 295 | 337 | if a.pasteCmd == "" { | |
| 296 | 338 | err := fmt.Errorf("no clipboard tool available (install wl-clipboard or xclip)") | |
| 297 | 339 | log.Error().Err(err).Msg("clipboard read failed") | |
| @@ -313,7 +355,8 @@ func (a *Adapter) ReadText(ctx context.Context) (string, error) { | |||
| 313 | 355 | ||
| 314 | 356 | out, err := cmd.Output() | |
| 315 | 357 | if err != nil { | |
| 316 | - | log.Debug().Err(err).Str("tool", a.pasteCmd).Msg("clipboard read failed (may be empty)") | |
| 358 | + | err = fmt.Errorf("run clipboard tool %s: %w", a.pasteCmd, err) | |
| 359 | + | log.Debug().Err(err).Str("tool", a.pasteCmd).Msg("clipboard read failed") | |
| 317 | 360 | return "", err | |
| 318 | 361 | } | |
| 319 | 362 | ||
| @@ -321,6 +364,41 @@ func (a *Adapter) ReadText(ctx context.Context) (string, error) { | |||
| 321 | 364 | return string(out), nil | |
| 322 | 365 | } | |
| 323 | 366 | ||
| 367 | + | // ReadText reads text from the clipboard. | |
| 368 | + | func (a *Adapter) ReadText(ctx context.Context) (string, error) { | |
| 369 | + | log := logging.FromContext(ctx) | |
| 370 | + | var commandErr error | |
| 371 | + | ||
| 372 | + | if a.pasteCmd != "" { | |
| 373 | + | text, err := a.readTextWithCommand(ctx, log) | |
| 374 | + | if err == nil { | |
| 375 | + | return text, nil | |
| 376 | + | } | |
| 377 | + | commandErr = err | |
| 378 | + | log.Debug().Err(err).Msg("system clipboard read failed; falling back") | |
| 379 | + | } | |
| 380 | + | ||
| 381 | + | if toolkit := a.ensureToolkitClipboard(); toolkit != nil { | |
| 382 | + | text, err := toolkit.ReadText(ctx) | |
| 383 | + | if err == nil { | |
| 384 | + | log.Debug().Str("backend", "toolkit").Int("len", len(text)).Msg("clipboard read success") | |
| 385 | + | return text, nil | |
| 386 | + | } | |
| 387 | + | log.Debug().Err(err).Msg("toolkit clipboard read failed") | |
| 388 | + | if commandErr != nil { | |
| 389 | + | return "", commandErr | |
| 390 | + | } | |
| 391 | + | return "", err | |
| 392 | + | } | |
| 393 | + | ||
| 394 | + | if commandErr != nil { | |
| 395 | + | return "", commandErr | |
| 396 | + | } | |
| 397 | + | err := fmt.Errorf("no clipboard tool available (install wl-clipboard or xclip)") | |
| 398 | + | log.Error().Err(err).Msg("clipboard read failed") | |
| 399 | + | return "", err | |
| 400 | + | } | |
| 401 | + | ||
| 324 | 402 | // Clear clears the clipboard contents. | |
| 325 | 403 | func (a *Adapter) Clear(ctx context.Context) error { | |
| 326 | 404 | return a.WriteText(ctx, "") | |
| @@ -13,11 +13,13 @@ import ( | |||||
| 13 | "github.com/bnema/dumber/internal/domain/entity" | 13 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 14 | "github.com/bnema/dumber/internal/logging" | 14 | "github.com/bnema/dumber/internal/logging" | ||
| 15 | "github.com/bnema/puregotk/v4/gdk" | 15 | "github.com/bnema/puregotk/v4/gdk" | ||
| 16 | + | "github.com/bnema/puregotk/v4/gio" | |||
| 16 | "github.com/bnema/puregotk/v4/glib" | 17 | "github.com/bnema/puregotk/v4/glib" | ||
| 17 | "github.com/rs/zerolog" | 18 | "github.com/rs/zerolog" | ||
| 18 | ) | 19 | ) | ||
| 19 | 20 | ||||
| 20 | type toolkitClipboard interface { | 21 | type toolkitClipboard interface { | ||
| 22 | + | ReadText(ctx context.Context) (string, error) | |||
| 21 | WriteText(ctx context.Context, text string) error | 23 | WriteText(ctx context.Context, text string) error | ||
| 22 | WriteImage(ctx context.Context, image entity.ImageData) error | 24 | WriteImage(ctx context.Context, image entity.ImageData) error | ||
| 23 | } | 25 | } | ||
| @@ -82,6 +84,49 @@ func (c *gdkToolkitClipboard) WriteText(ctx context.Context, text string) error | |||||
| 82 | }) | 84 | }) | ||
| 83 | } | 85 | } | ||
| 84 | 86 | ||||
| 87 | + | func (c *gdkToolkitClipboard) ReadText(ctx context.Context) (string, error) { | |||
| 88 | + | var text string | |||
| 89 | + | err := withToolkitClipboard(ctx, func() error { | |||
| 90 | + | if c == nil || c.clipboard == nil { | |||
| 91 | + | return fmt.Errorf("toolkit clipboard not available") | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | resultCh := make(chan struct { | |||
| 95 | + | text string | |||
| 96 | + | err error | |||
| 97 | + | }, 1) | |||
| 98 | + | cb := gio.AsyncReadyCallback(func(_ uintptr, result uintptr, _ uintptr) { | |||
| 99 | + | asyncResult := &gio.AsyncResultBase{} | |||
| 100 | + | asyncResult.SetGoPointer(result) | |||
| 101 | + | readText, readErr := c.clipboard.ReadTextFinish(asyncResult) | |||
| 102 | + | resultCh <- struct { | |||
| 103 | + | text string | |||
| 104 | + | err error | |||
| 105 | + | }{text: readText, err: readErr} | |||
| 106 | + | }) | |||
| 107 | + | c.clipboard.ReadTextAsync(nil, &cb, 0) | |||
| 108 | + | ||||
| 109 | + | mainContext := glib.MainContextDefault() | |||
| 110 | + | if mainContext == nil { | |||
| 111 | + | return fmt.Errorf("toolkit main context not available") | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | for { | |||
| 115 | + | select { | |||
| 116 | + | case result := <-resultCh: | |||
| 117 | + | text = result.text | |||
| 118 | + | return result.err | |||
| 119 | + | default: | |||
| 120 | + | } | |||
| 121 | + | if ctx.Err() != nil { | |||
| 122 | + | return ctx.Err() | |||
| 123 | + | } | |||
| 124 | + | mainContext.Iteration(true) | |||
| 125 | + | } | |||
| 126 | + | }) | |||
| 127 | + | return text, err | |||
| 128 | + | } | |||
| 129 | + | ||||
| 85 | func (c *gdkToolkitClipboard) WriteImage(ctx context.Context, image entity.ImageData) error { | 130 | func (c *gdkToolkitClipboard) WriteImage(ctx context.Context, image entity.ImageData) error { | ||
| 86 | return withToolkitClipboard(ctx, func() error { | 131 | return withToolkitClipboard(ctx, func() error { | ||
| 87 | if c == nil || c.clipboard == nil { | 132 | if c == nil || c.clipboard == nil { | ||
| @@ -288,10 +333,7 @@ func (a *Adapter) writeImageWithCommand(ctx context.Context, image entity.ImageD | |||||
| 288 | return nil | 333 | return nil | ||
| 289 | } | 334 | } | ||
| 290 | 335 | ||||
| 291 | - | // ReadText reads text from the clipboard. | 336 | + | func (a *Adapter) readTextWithCommand(ctx context.Context, log *zerolog.Logger) (string, error) { |
| 292 | - | func (a *Adapter) ReadText(ctx context.Context) (string, error) { | |||
| 293 | - | log := logging.FromContext(ctx) | |||
| 294 | - | ||||
| 295 | if a.pasteCmd == "" { | 337 | if a.pasteCmd == "" { | ||
| 296 | err := fmt.Errorf("no clipboard tool available (install wl-clipboard or xclip)") | 338 | err := fmt.Errorf("no clipboard tool available (install wl-clipboard or xclip)") | ||
| 297 | log.Error().Err(err).Msg("clipboard read failed") | 339 | log.Error().Err(err).Msg("clipboard read failed") | ||
| @@ -313,7 +355,8 @@ func (a *Adapter) ReadText(ctx context.Context) (string, error) { | |||||
| 313 | 355 | ||||
| 314 | out, err := cmd.Output() | 356 | out, err := cmd.Output() | ||
| 315 | if err != nil { | 357 | if err != nil { | ||
| 316 | - | log.Debug().Err(err).Str("tool", a.pasteCmd).Msg("clipboard read failed (may be empty)") | 358 | + | err = fmt.Errorf("run clipboard tool %s: %w", a.pasteCmd, err) |
| 359 | + | log.Debug().Err(err).Str("tool", a.pasteCmd).Msg("clipboard read failed") | |||
| 317 | return "", err | 360 | return "", err | ||
| 318 | } | 361 | } | ||
| 319 | 362 | ||||
| @@ -321,6 +364,41 @@ func (a *Adapter) ReadText(ctx context.Context) (string, error) { | |||||
| 321 | return string(out), nil | 364 | return string(out), nil | ||
| 322 | } | 365 | } | ||
| 323 | 366 | ||||
| 367 | + | // ReadText reads text from the clipboard. | |||
| 368 | + | func (a *Adapter) ReadText(ctx context.Context) (string, error) { | |||
| 369 | + | log := logging.FromContext(ctx) | |||
| 370 | + | var commandErr error | |||
| 371 | + | ||||
| 372 | + | if a.pasteCmd != "" { | |||
| 373 | + | text, err := a.readTextWithCommand(ctx, log) | |||
| 374 | + | if err == nil { | |||
| 375 | + | return text, nil | |||
| 376 | + | } | |||
| 377 | + | commandErr = err | |||
| 378 | + | log.Debug().Err(err).Msg("system clipboard read failed; falling back") | |||
| 379 | + | } | |||
| 380 | + | ||||
| 381 | + | if toolkit := a.ensureToolkitClipboard(); toolkit != nil { | |||
| 382 | + | text, err := toolkit.ReadText(ctx) | |||
| 383 | + | if err == nil { | |||
| 384 | + | log.Debug().Str("backend", "toolkit").Int("len", len(text)).Msg("clipboard read success") | |||
| 385 | + | return text, nil | |||
| 386 | + | } | |||
| 387 | + | log.Debug().Err(err).Msg("toolkit clipboard read failed") | |||
| 388 | + | if commandErr != nil { | |||
| 389 | + | return "", commandErr | |||
| 390 | + | } | |||
| 391 | + | return "", err | |||
| 392 | + | } | |||
| 393 | + | ||||
| 394 | + | if commandErr != nil { | |||
| 395 | + | return "", commandErr | |||
| 396 | + | } | |||
| 397 | + | err := fmt.Errorf("no clipboard tool available (install wl-clipboard or xclip)") | |||
| 398 | + | log.Error().Err(err).Msg("clipboard read failed") | |||
| 399 | + | return "", err | |||
| 400 | + | } | |||
| 401 | + | ||||
| 324 | // Clear clears the clipboard contents. | 402 | // Clear clears the clipboard contents. | ||
| 325 | func (a *Adapter) Clear(ctx context.Context) error { | 403 | func (a *Adapter) Clear(ctx context.Context) error { | ||
| 326 | return a.WriteText(ctx, "") | 404 | return a.WriteText(ctx, "") | ||
M internal/infrastructure/clipboard/clipboard_test.go +29 -0
| @@ -14,10 +14,12 @@ import ( | |||
| 14 | 14 | ||
| 15 | 15 | type fakeToolkitClipboard struct { | |
| 16 | 16 | writeTextCalls int | |
| 17 | + | readTextCalls int | |
| 17 | 18 | writeImageCalls int | |
| 18 | 19 | text string | |
| 19 | 20 | image entity.ImageData | |
| 20 | 21 | writeTextErr error | |
| 22 | + | readTextErr error | |
| 21 | 23 | writeImageErr error | |
| 22 | 24 | } | |
| 23 | 25 | ||
| @@ -27,12 +29,39 @@ func (f *fakeToolkitClipboard) WriteText(_ context.Context, text string) error { | |||
| 27 | 29 | return f.writeTextErr | |
| 28 | 30 | } | |
| 29 | 31 | ||
| 32 | + | func (f *fakeToolkitClipboard) ReadText(_ context.Context) (string, error) { | |
| 33 | + | f.readTextCalls++ | |
| 34 | + | return f.text, f.readTextErr | |
| 35 | + | } | |
| 36 | + | ||
| 30 | 37 | func (f *fakeToolkitClipboard) WriteImage(_ context.Context, image entity.ImageData) error { | |
| 31 | 38 | f.writeImageCalls++ | |
| 32 | 39 | f.image = image | |
| 33 | 40 | return f.writeImageErr | |
| 34 | 41 | } | |
| 35 | 42 | ||
| 43 | + | func TestAdapter_ReadTextFallsBackToToolkitClipboardWhenNoSystemTool(t *testing.T) { | |
| 44 | + | fake := &fakeToolkitClipboard{text: "toolkit text"} | |
| 45 | + | adapter := &Adapter{toolkit: fake} | |
| 46 | + | ||
| 47 | + | text, err := adapter.ReadText(context.Background()) | |
| 48 | + | ||
| 49 | + | require.NoError(t, err) | |
| 50 | + | require.Equal(t, "toolkit text", text) | |
| 51 | + | require.Equal(t, 1, fake.readTextCalls) | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | func TestAdapter_HasTextReturnsTrueWhenToolkitFallbackHasText(t *testing.T) { | |
| 55 | + | fake := &fakeToolkitClipboard{text: "toolkit text"} | |
| 56 | + | adapter := &Adapter{toolkit: fake} | |
| 57 | + | ||
| 58 | + | hasText, err := adapter.HasText(context.Background()) | |
| 59 | + | ||
| 60 | + | require.NoError(t, err) | |
| 61 | + | require.True(t, hasText) | |
| 62 | + | require.Equal(t, 1, fake.readTextCalls) | |
| 63 | + | } | |
| 64 | + | ||
| 36 | 65 | func TestAdapter_WriteTextReturnsToolkitErrorWhenNoSystemTool(t *testing.T) { | |
| 37 | 66 | sentinel := errors.New("toolkit text failed") | |
| 38 | 67 | fake := &fakeToolkitClipboard{writeTextErr: sentinel} | |
| @@ -14,10 +14,12 @@ import ( | |||||
| 14 | 14 | ||||
| 15 | type fakeToolkitClipboard struct { | 15 | type fakeToolkitClipboard struct { | ||
| 16 | writeTextCalls int | 16 | writeTextCalls int | ||
| 17 | + | readTextCalls int | |||
| 17 | writeImageCalls int | 18 | writeImageCalls int | ||
| 18 | text string | 19 | text string | ||
| 19 | image entity.ImageData | 20 | image entity.ImageData | ||
| 20 | writeTextErr error | 21 | writeTextErr error | ||
| 22 | + | readTextErr error | |||
| 21 | writeImageErr error | 23 | writeImageErr error | ||
| 22 | } | 24 | } | ||
| 23 | 25 | ||||
| @@ -27,12 +29,39 @@ func (f *fakeToolkitClipboard) WriteText(_ context.Context, text string) error { | |||||
| 27 | return f.writeTextErr | 29 | return f.writeTextErr | ||
| 28 | } | 30 | } | ||
| 29 | 31 | ||||
| 32 | + | func (f *fakeToolkitClipboard) ReadText(_ context.Context) (string, error) { | |||
| 33 | + | f.readTextCalls++ | |||
| 34 | + | return f.text, f.readTextErr | |||
| 35 | + | } | |||
| 36 | + | ||||
| 30 | func (f *fakeToolkitClipboard) WriteImage(_ context.Context, image entity.ImageData) error { | 37 | func (f *fakeToolkitClipboard) WriteImage(_ context.Context, image entity.ImageData) error { | ||
| 31 | f.writeImageCalls++ | 38 | f.writeImageCalls++ | ||
| 32 | f.image = image | 39 | f.image = image | ||
| 33 | return f.writeImageErr | 40 | return f.writeImageErr | ||
| 34 | } | 41 | } | ||
| 35 | 42 | ||||
| 43 | + | func TestAdapter_ReadTextFallsBackToToolkitClipboardWhenNoSystemTool(t *testing.T) { | |||
| 44 | + | fake := &fakeToolkitClipboard{text: "toolkit text"} | |||
| 45 | + | adapter := &Adapter{toolkit: fake} | |||
| 46 | + | ||||
| 47 | + | text, err := adapter.ReadText(context.Background()) | |||
| 48 | + | ||||
| 49 | + | require.NoError(t, err) | |||
| 50 | + | require.Equal(t, "toolkit text", text) | |||
| 51 | + | require.Equal(t, 1, fake.readTextCalls) | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | func TestAdapter_HasTextReturnsTrueWhenToolkitFallbackHasText(t *testing.T) { | |||
| 55 | + | fake := &fakeToolkitClipboard{text: "toolkit text"} | |||
| 56 | + | adapter := &Adapter{toolkit: fake} | |||
| 57 | + | ||||
| 58 | + | hasText, err := adapter.HasText(context.Background()) | |||
| 59 | + | ||||
| 60 | + | require.NoError(t, err) | |||
| 61 | + | require.True(t, hasText) | |||
| 62 | + | require.Equal(t, 1, fake.readTextCalls) | |||
| 63 | + | } | |||
| 64 | + | ||||
| 36 | func TestAdapter_WriteTextReturnsToolkitErrorWhenNoSystemTool(t *testing.T) { | 65 | func TestAdapter_WriteTextReturnsToolkitErrorWhenNoSystemTool(t *testing.T) { | ||
| 37 | sentinel := errors.New("toolkit text failed") | 66 | sentinel := errors.New("toolkit text failed") | ||
| 38 | fake := &fakeToolkitClipboard{writeTextErr: sentinel} | 67 | fake := &fakeToolkitClipboard{writeTextErr: sentinel} | ||