fix(review): address PR feedback
- commit
- b944d63bcd29af743977cc491d901e701fc8deba
- author
- brice · · 2026-04-18 11:47 UTC
- parent
- 3192764
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
29 files · +443 -243
M cmd/cef-helper/main.go +3 -2
| @@ -23,8 +23,9 @@ func main() { | |||
| 23 | 23 | ||
| 24 | 24 | // cef_execute_process will handle the subprocess lifecycle and call | |
| 25 | 25 | // os.Exit internally if this is a valid CEF subprocess invocation. | |
| 26 | - | // We pass the lightweight subprocess app so renderer-process handlers | |
| 27 | - | // (native JS bridge, custom schemes, etc.) are available in helpers. | |
| 26 | + | // We pass the lightweight subprocess app, which intentionally returns nil | |
| 27 | + | // from GetRenderProcessHandler(), so renderer-side bridges remain disabled | |
| 28 | + | // in helper subprocesses while the OSR startup regression is under repair. | |
| 28 | 29 | cef.MaybeExitSubprocessWithApp(infracef.NewSubprocessApp()) | |
| 29 | 30 | ||
| 30 | 31 | // If we get here, something went wrong. | |
| @@ -23,8 +23,9 @@ func main() { | |||||
| 23 | 23 | ||||
| 24 | // cef_execute_process will handle the subprocess lifecycle and call | 24 | // cef_execute_process will handle the subprocess lifecycle and call | ||
| 25 | // os.Exit internally if this is a valid CEF subprocess invocation. | 25 | // os.Exit internally if this is a valid CEF subprocess invocation. | ||
| 26 | - | // We pass the lightweight subprocess app so renderer-process handlers | 26 | + | // We pass the lightweight subprocess app, which intentionally returns nil |
| 27 | - | // (native JS bridge, custom schemes, etc.) are available in helpers. | 27 | + | // from GetRenderProcessHandler(), so renderer-side bridges remain disabled |
| 28 | + | // in helper subprocesses while the OSR startup regression is under repair. | |||
| 28 | cef.MaybeExitSubprocessWithApp(infracef.NewSubprocessApp()) | 29 | cef.MaybeExitSubprocessWithApp(infracef.NewSubprocessApp()) | ||
| 29 | 30 | ||||
| 30 | // If we get here, something went wrong. | 31 | // If we get here, something went wrong. | ||
M cmd/dumber/main.go +1 -1
| @@ -774,7 +774,7 @@ func buildUIDependencies( | |||
| 774 | 774 | Theme: themeManager, | |
| 775 | 775 | ColorResolver: colorResolver, | |
| 776 | 776 | AdwaitaDetector: adwaitaDetector, | |
| 777 | - | XDG: xdg.New(), | |
| 777 | + | XDG: xdg.New(runtimeProfile), | |
| 778 | 778 | Engine: engine, | |
| 779 | 779 | FilterManager: filterManager, | |
| 780 | 780 | HistoryRepo: repos.history, | |
| @@ -774,7 +774,7 @@ func buildUIDependencies( | |||||
| 774 | Theme: themeManager, | 774 | Theme: themeManager, | ||
| 775 | ColorResolver: colorResolver, | 775 | ColorResolver: colorResolver, | ||
| 776 | AdwaitaDetector: adwaitaDetector, | 776 | AdwaitaDetector: adwaitaDetector, | ||
| 777 | - | XDG: xdg.New(), | 777 | + | XDG: xdg.New(runtimeProfile), |
| 778 | Engine: engine, | 778 | Engine: engine, | ||
| 779 | FilterManager: filterManager, | 779 | FilterManager: filterManager, | ||
| 780 | HistoryRepo: repos.history, | 780 | HistoryRepo: repos.history, | ||
A internal/application/dto/clipboard.go +27 -0
| @@ -0,0 +1,27 @@ | |||
| 1 | + | package dto | |
| 2 | + | ||
| 3 | + | // ClipboardSource identifies the originating engine for clipboard events. | |
| 4 | + | type ClipboardSource string | |
| 5 | + | ||
| 6 | + | const ( | |
| 7 | + | // ClipboardSourceCEF marks events from the CEF engine. | |
| 8 | + | ClipboardSourceCEF ClipboardSource = "cef" | |
| 9 | + | // ClipboardSourceWebKit marks events from the WebKit engine. | |
| 10 | + | ClipboardSourceWebKit ClipboardSource = "webkit" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // SelectionClipboardInput carries auto-copy selection updates. | |
| 14 | + | type SelectionClipboardInput struct { | |
| 15 | + | Text string | |
| 16 | + | SourceEngine ClipboardSource | |
| 17 | + | ViewID uint64 | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | // ExplicitClipboardInput carries explicit copy requests. | |
| 21 | + | type ExplicitClipboardInput struct { | |
| 22 | + | Text string | |
| 23 | + | SourceEngine ClipboardSource | |
| 24 | + | ViewID uint64 | |
| 25 | + | Action string | |
| 26 | + | NativeHandled bool | |
| 27 | + | } | |
| @@ -0,0 +1,27 @@ | |||||
| 1 | + | package dto | |||
| 2 | + | ||||
| 3 | + | // ClipboardSource identifies the originating engine for clipboard events. | |||
| 4 | + | type ClipboardSource string | |||
| 5 | + | ||||
| 6 | + | const ( | |||
| 7 | + | // ClipboardSourceCEF marks events from the CEF engine. | |||
| 8 | + | ClipboardSourceCEF ClipboardSource = "cef" | |||
| 9 | + | // ClipboardSourceWebKit marks events from the WebKit engine. | |||
| 10 | + | ClipboardSourceWebKit ClipboardSource = "webkit" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // SelectionClipboardInput carries auto-copy selection updates. | |||
| 14 | + | type SelectionClipboardInput struct { | |||
| 15 | + | Text string | |||
| 16 | + | SourceEngine ClipboardSource | |||
| 17 | + | ViewID uint64 | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | // ExplicitClipboardInput carries explicit copy requests. | |||
| 21 | + | type ExplicitClipboardInput struct { | |||
| 22 | + | Text string | |||
| 23 | + | SourceEngine ClipboardSource | |||
| 24 | + | ViewID uint64 | |||
| 25 | + | Action string | |||
| 26 | + | NativeHandled bool | |||
| 27 | + | } | |||
M internal/application/port/clipboard_events.go +5 -27
| @@ -1,35 +1,13 @@ | |||
| 1 | 1 | package port | |
| 2 | 2 | ||
| 3 | - | import "context" | |
| 3 | + | import ( | |
| 4 | + | "context" | |
| 4 | 5 | ||
| 5 | - | // ClipboardSource identifies the originating engine for clipboard events. | |
| 6 | - | type ClipboardSource string | |
| 7 | - | ||
| 8 | - | const ( | |
| 9 | - | // ClipboardSourceCEF marks events from the CEF engine. | |
| 10 | - | ClipboardSourceCEF ClipboardSource = "cef" | |
| 11 | - | // ClipboardSourceWebKit marks events from the WebKit engine. | |
| 12 | - | ClipboardSourceWebKit ClipboardSource = "webkit" | |
| 6 | + | "github.com/bnema/dumber/internal/application/dto" | |
| 13 | 7 | ) | |
| 14 | 8 | ||
| 15 | - | // SelectionClipboardInput carries auto-copy selection updates. | |
| 16 | - | type SelectionClipboardInput struct { | |
| 17 | - | Text string | |
| 18 | - | SourceEngine ClipboardSource | |
| 19 | - | ViewID WebViewID | |
| 20 | - | } | |
| 21 | - | ||
| 22 | - | // ExplicitClipboardInput carries explicit copy requests. | |
| 23 | - | type ExplicitClipboardInput struct { | |
| 24 | - | Text string | |
| 25 | - | SourceEngine ClipboardSource | |
| 26 | - | ViewID WebViewID | |
| 27 | - | Action string | |
| 28 | - | NativeHandled bool | |
| 29 | - | } | |
| 30 | - | ||
| 31 | 9 | // ClipboardTextOrchestrator coordinates shared clipboard behavior. | |
| 32 | 10 | type ClipboardTextOrchestrator interface { | |
| 33 | - | HandleSelectionUpdate(ctx context.Context, input SelectionClipboardInput) error | |
| 34 | - | HandleExplicitCopy(ctx context.Context, input ExplicitClipboardInput) error | |
| 11 | + | HandleSelectionUpdate(ctx context.Context, input dto.SelectionClipboardInput) error | |
| 12 | + | HandleExplicitCopy(ctx context.Context, input dto.ExplicitClipboardInput) error | |
| 35 | 13 | } | |
| @@ -1,35 +1,13 @@ | |||||
| 1 | package port | 1 | package port | ||
| 2 | 2 | ||||
| 3 | - | import "context" | 3 | + | import ( |
| 4 | + | "context" | |||
| 4 | 5 | ||||
| 5 | - | // ClipboardSource identifies the originating engine for clipboard events. | 6 | + | "github.com/bnema/dumber/internal/application/dto" |
| 6 | - | type ClipboardSource string | |||
| 7 | - | ||||
| 8 | - | const ( | |||
| 9 | - | // ClipboardSourceCEF marks events from the CEF engine. | |||
| 10 | - | ClipboardSourceCEF ClipboardSource = "cef" | |||
| 11 | - | // ClipboardSourceWebKit marks events from the WebKit engine. | |||
| 12 | - | ClipboardSourceWebKit ClipboardSource = "webkit" | |||
| 13 | ) | 7 | ) | ||
| 14 | 8 | ||||
| 15 | - | // SelectionClipboardInput carries auto-copy selection updates. | |||
| 16 | - | type SelectionClipboardInput struct { | |||
| 17 | - | Text string | |||
| 18 | - | SourceEngine ClipboardSource | |||
| 19 | - | ViewID WebViewID | |||
| 20 | - | } | |||
| 21 | - | ||||
| 22 | - | // ExplicitClipboardInput carries explicit copy requests. | |||
| 23 | - | type ExplicitClipboardInput struct { | |||
| 24 | - | Text string | |||
| 25 | - | SourceEngine ClipboardSource | |||
| 26 | - | ViewID WebViewID | |||
| 27 | - | Action string | |||
| 28 | - | NativeHandled bool | |||
| 29 | - | } | |||
| 30 | - | ||||
| 31 | // ClipboardTextOrchestrator coordinates shared clipboard behavior. | 9 | // ClipboardTextOrchestrator coordinates shared clipboard behavior. | ||
| 32 | type ClipboardTextOrchestrator interface { | 10 | type ClipboardTextOrchestrator interface { | ||
| 33 | - | HandleSelectionUpdate(ctx context.Context, input SelectionClipboardInput) error | 11 | + | HandleSelectionUpdate(ctx context.Context, input dto.SelectionClipboardInput) error |
| 34 | - | HandleExplicitCopy(ctx context.Context, input ExplicitClipboardInput) error | 12 | + | HandleExplicitCopy(ctx context.Context, input dto.ExplicitClipboardInput) error |
| 35 | } | 13 | } | ||
M internal/application/port/mocks/mocks.go +15 -14
| @@ -11,6 +11,7 @@ import ( | |||
| 11 | 11 | "io" | |
| 12 | 12 | "time" | |
| 13 | 13 | ||
| 14 | + | "github.com/bnema/dumber/internal/application/dto" | |
| 14 | 15 | "github.com/bnema/dumber/internal/application/port" | |
| 15 | 16 | "github.com/bnema/dumber/internal/domain/entity" | |
| 16 | 17 | "github.com/rs/zerolog" | |
| @@ -758,7 +759,7 @@ func (_m *MockClipboardTextOrchestrator) EXPECT() *MockClipboardTextOrchestrator | |||
| 758 | 759 | } | |
| 759 | 760 | ||
| 760 | 761 | // HandleExplicitCopy provides a mock function for the type MockClipboardTextOrchestrator | |
| 761 | - | func (_mock *MockClipboardTextOrchestrator) HandleExplicitCopy(ctx context.Context, input port.ExplicitClipboardInput) error { | |
| 762 | + | func (_mock *MockClipboardTextOrchestrator) HandleExplicitCopy(ctx context.Context, input dto.ExplicitClipboardInput) error { | |
| 762 | 763 | ret := _mock.Called(ctx, input) | |
| 763 | 764 | ||
| 764 | 765 | if len(ret) == 0 { | |
| @@ -766,7 +767,7 @@ func (_mock *MockClipboardTextOrchestrator) HandleExplicitCopy(ctx context.Conte | |||
| 766 | 767 | } | |
| 767 | 768 | ||
| 768 | 769 | var r0 error | |
| 769 | - | if returnFunc, ok := ret.Get(0).(func(context.Context, port.ExplicitClipboardInput) error); ok { | |
| 770 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, dto.ExplicitClipboardInput) error); ok { | |
| 770 | 771 | r0 = returnFunc(ctx, input) | |
| 771 | 772 | } else { | |
| 772 | 773 | r0 = ret.Error(0) | |
| @@ -781,20 +782,20 @@ type MockClipboardTextOrchestrator_HandleExplicitCopy_Call struct { | |||
| 781 | 782 | ||
| 782 | 783 | // HandleExplicitCopy is a helper method to define mock.On call | |
| 783 | 784 | // - ctx context.Context | |
| 784 | - | // - input port.ExplicitClipboardInput | |
| 785 | + | // - input dto.ExplicitClipboardInput | |
| 785 | 786 | func (_e *MockClipboardTextOrchestrator_Expecter) HandleExplicitCopy(ctx interface{}, input interface{}) *MockClipboardTextOrchestrator_HandleExplicitCopy_Call { | |
| 786 | 787 | return &MockClipboardTextOrchestrator_HandleExplicitCopy_Call{Call: _e.mock.On("HandleExplicitCopy", ctx, input)} | |
| 787 | 788 | } | |
| 788 | 789 | ||
| 789 | - | func (_c *MockClipboardTextOrchestrator_HandleExplicitCopy_Call) Run(run func(ctx context.Context, input port.ExplicitClipboardInput)) *MockClipboardTextOrchestrator_HandleExplicitCopy_Call { | |
| 790 | + | func (_c *MockClipboardTextOrchestrator_HandleExplicitCopy_Call) Run(run func(ctx context.Context, input dto.ExplicitClipboardInput)) *MockClipboardTextOrchestrator_HandleExplicitCopy_Call { | |
| 790 | 791 | _c.Call.Run(func(args mock.Arguments) { | |
| 791 | 792 | var arg0 context.Context | |
| 792 | 793 | if args[0] != nil { | |
| 793 | 794 | arg0 = args[0].(context.Context) | |
| 794 | 795 | } | |
| 795 | - | var arg1 port.ExplicitClipboardInput | |
| 796 | + | var arg1 dto.ExplicitClipboardInput | |
| 796 | 797 | if args[1] != nil { | |
| 797 | - | arg1 = args[1].(port.ExplicitClipboardInput) | |
| 798 | + | arg1 = args[1].(dto.ExplicitClipboardInput) | |
| 798 | 799 | } | |
| 799 | 800 | run( | |
| 800 | 801 | arg0, | |
| @@ -809,13 +810,13 @@ func (_c *MockClipboardTextOrchestrator_HandleExplicitCopy_Call) Return(err erro | |||
| 809 | 810 | return _c | |
| 810 | 811 | } | |
| 811 | 812 | ||
| 812 | - | func (_c *MockClipboardTextOrchestrator_HandleExplicitCopy_Call) RunAndReturn(run func(ctx context.Context, input port.ExplicitClipboardInput) error) *MockClipboardTextOrchestrator_HandleExplicitCopy_Call { | |
| 813 | + | func (_c *MockClipboardTextOrchestrator_HandleExplicitCopy_Call) RunAndReturn(run func(ctx context.Context, input dto.ExplicitClipboardInput) error) *MockClipboardTextOrchestrator_HandleExplicitCopy_Call { | |
| 813 | 814 | _c.Call.Return(run) | |
| 814 | 815 | return _c | |
| 815 | 816 | } | |
| 816 | 817 | ||
| 817 | 818 | // HandleSelectionUpdate provides a mock function for the type MockClipboardTextOrchestrator | |
| 818 | - | func (_mock *MockClipboardTextOrchestrator) HandleSelectionUpdate(ctx context.Context, input port.SelectionClipboardInput) error { | |
| 819 | + | func (_mock *MockClipboardTextOrchestrator) HandleSelectionUpdate(ctx context.Context, input dto.SelectionClipboardInput) error { | |
| 819 | 820 | ret := _mock.Called(ctx, input) | |
| 820 | 821 | ||
| 821 | 822 | if len(ret) == 0 { | |
| @@ -823,7 +824,7 @@ func (_mock *MockClipboardTextOrchestrator) HandleSelectionUpdate(ctx context.Co | |||
| 823 | 824 | } | |
| 824 | 825 | ||
| 825 | 826 | var r0 error | |
| 826 | - | if returnFunc, ok := ret.Get(0).(func(context.Context, port.SelectionClipboardInput) error); ok { | |
| 827 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, dto.SelectionClipboardInput) error); ok { | |
| 827 | 828 | r0 = returnFunc(ctx, input) | |
| 828 | 829 | } else { | |
| 829 | 830 | r0 = ret.Error(0) | |
| @@ -838,20 +839,20 @@ type MockClipboardTextOrchestrator_HandleSelectionUpdate_Call struct { | |||
| 838 | 839 | ||
| 839 | 840 | // HandleSelectionUpdate is a helper method to define mock.On call | |
| 840 | 841 | // - ctx context.Context | |
| 841 | - | // - input port.SelectionClipboardInput | |
| 842 | + | // - input dto.SelectionClipboardInput | |
| 842 | 843 | func (_e *MockClipboardTextOrchestrator_Expecter) HandleSelectionUpdate(ctx interface{}, input interface{}) *MockClipboardTextOrchestrator_HandleSelectionUpdate_Call { | |
| 843 | 844 | return &MockClipboardTextOrchestrator_HandleSelectionUpdate_Call{Call: _e.mock.On("HandleSelectionUpdate", ctx, input)} | |
| 844 | 845 | } | |
| 845 | 846 | ||
| 846 | - | func (_c *MockClipboardTextOrchestrator_HandleSelectionUpdate_Call) Run(run func(ctx context.Context, input port.SelectionClipboardInput)) *MockClipboardTextOrchestrator_HandleSelectionUpdate_Call { | |
| 847 | + | func (_c *MockClipboardTextOrchestrator_HandleSelectionUpdate_Call) Run(run func(ctx context.Context, input dto.SelectionClipboardInput)) *MockClipboardTextOrchestrator_HandleSelectionUpdate_Call { | |
| 847 | 848 | _c.Call.Run(func(args mock.Arguments) { | |
| 848 | 849 | var arg0 context.Context | |
| 849 | 850 | if args[0] != nil { | |
| 850 | 851 | arg0 = args[0].(context.Context) | |
| 851 | 852 | } | |
| 852 | - | var arg1 port.SelectionClipboardInput | |
| 853 | + | var arg1 dto.SelectionClipboardInput | |
| 853 | 854 | if args[1] != nil { | |
| 854 | - | arg1 = args[1].(port.SelectionClipboardInput) | |
| 855 | + | arg1 = args[1].(dto.SelectionClipboardInput) | |
| 855 | 856 | } | |
| 856 | 857 | run( | |
| 857 | 858 | arg0, | |
| @@ -866,7 +867,7 @@ func (_c *MockClipboardTextOrchestrator_HandleSelectionUpdate_Call) Return(err e | |||
| 866 | 867 | return _c | |
| 867 | 868 | } | |
| 868 | 869 | ||
| 869 | - | func (_c *MockClipboardTextOrchestrator_HandleSelectionUpdate_Call) RunAndReturn(run func(ctx context.Context, input port.SelectionClipboardInput) error) *MockClipboardTextOrchestrator_HandleSelectionUpdate_Call { | |
| 870 | + | func (_c *MockClipboardTextOrchestrator_HandleSelectionUpdate_Call) RunAndReturn(run func(ctx context.Context, input dto.SelectionClipboardInput) error) *MockClipboardTextOrchestrator_HandleSelectionUpdate_Call { | |
| 870 | 871 | _c.Call.Return(run) | |
| 871 | 872 | return _c | |
| 872 | 873 | } | |
| @@ -11,6 +11,7 @@ import ( | |||||
| 11 | "io" | 11 | "io" | ||
| 12 | "time" | 12 | "time" | ||
| 13 | 13 | ||||
| 14 | + | "github.com/bnema/dumber/internal/application/dto" | |||
| 14 | "github.com/bnema/dumber/internal/application/port" | 15 | "github.com/bnema/dumber/internal/application/port" | ||
| 15 | "github.com/bnema/dumber/internal/domain/entity" | 16 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 16 | "github.com/rs/zerolog" | 17 | "github.com/rs/zerolog" | ||
| @@ -758,7 +759,7 @@ func (_m *MockClipboardTextOrchestrator) EXPECT() *MockClipboardTextOrchestrator | |||||
| 758 | } | 759 | } | ||
| 759 | 760 | ||||
| 760 | // HandleExplicitCopy provides a mock function for the type MockClipboardTextOrchestrator | 761 | // HandleExplicitCopy provides a mock function for the type MockClipboardTextOrchestrator | ||
| 761 | - | func (_mock *MockClipboardTextOrchestrator) HandleExplicitCopy(ctx context.Context, input port.ExplicitClipboardInput) error { | 762 | + | func (_mock *MockClipboardTextOrchestrator) HandleExplicitCopy(ctx context.Context, input dto.ExplicitClipboardInput) error { |
| 762 | ret := _mock.Called(ctx, input) | 763 | ret := _mock.Called(ctx, input) | ||
| 763 | 764 | ||||
| 764 | if len(ret) == 0 { | 765 | if len(ret) == 0 { | ||
| @@ -766,7 +767,7 @@ func (_mock *MockClipboardTextOrchestrator) HandleExplicitCopy(ctx context.Conte | |||||
| 766 | } | 767 | } | ||
| 767 | 768 | ||||
| 768 | var r0 error | 769 | var r0 error | ||
| 769 | - | if returnFunc, ok := ret.Get(0).(func(context.Context, port.ExplicitClipboardInput) error); ok { | 770 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, dto.ExplicitClipboardInput) error); ok { |
| 770 | r0 = returnFunc(ctx, input) | 771 | r0 = returnFunc(ctx, input) | ||
| 771 | } else { | 772 | } else { | ||
| 772 | r0 = ret.Error(0) | 773 | r0 = ret.Error(0) | ||
| @@ -781,20 +782,20 @@ type MockClipboardTextOrchestrator_HandleExplicitCopy_Call struct { | |||||
| 781 | 782 | ||||
| 782 | // HandleExplicitCopy is a helper method to define mock.On call | 783 | // HandleExplicitCopy is a helper method to define mock.On call | ||
| 783 | // - ctx context.Context | 784 | // - ctx context.Context | ||
| 784 | - | // - input port.ExplicitClipboardInput | 785 | + | // - input dto.ExplicitClipboardInput |
| 785 | func (_e *MockClipboardTextOrchestrator_Expecter) HandleExplicitCopy(ctx interface{}, input interface{}) *MockClipboardTextOrchestrator_HandleExplicitCopy_Call { | 786 | func (_e *MockClipboardTextOrchestrator_Expecter) HandleExplicitCopy(ctx interface{}, input interface{}) *MockClipboardTextOrchestrator_HandleExplicitCopy_Call { | ||
| 786 | return &MockClipboardTextOrchestrator_HandleExplicitCopy_Call{Call: _e.mock.On("HandleExplicitCopy", ctx, input)} | 787 | return &MockClipboardTextOrchestrator_HandleExplicitCopy_Call{Call: _e.mock.On("HandleExplicitCopy", ctx, input)} | ||
| 787 | } | 788 | } | ||
| 788 | 789 | ||||
| 789 | - | func (_c *MockClipboardTextOrchestrator_HandleExplicitCopy_Call) Run(run func(ctx context.Context, input port.ExplicitClipboardInput)) *MockClipboardTextOrchestrator_HandleExplicitCopy_Call { | 790 | + | func (_c *MockClipboardTextOrchestrator_HandleExplicitCopy_Call) Run(run func(ctx context.Context, input dto.ExplicitClipboardInput)) *MockClipboardTextOrchestrator_HandleExplicitCopy_Call { |
| 790 | _c.Call.Run(func(args mock.Arguments) { | 791 | _c.Call.Run(func(args mock.Arguments) { | ||
| 791 | var arg0 context.Context | 792 | var arg0 context.Context | ||
| 792 | if args[0] != nil { | 793 | if args[0] != nil { | ||
| 793 | arg0 = args[0].(context.Context) | 794 | arg0 = args[0].(context.Context) | ||
| 794 | } | 795 | } | ||
| 795 | - | var arg1 port.ExplicitClipboardInput | 796 | + | var arg1 dto.ExplicitClipboardInput |
| 796 | if args[1] != nil { | 797 | if args[1] != nil { | ||
| 797 | - | arg1 = args[1].(port.ExplicitClipboardInput) | 798 | + | arg1 = args[1].(dto.ExplicitClipboardInput) |
| 798 | } | 799 | } | ||
| 799 | run( | 800 | run( | ||
| 800 | arg0, | 801 | arg0, | ||
| @@ -809,13 +810,13 @@ func (_c *MockClipboardTextOrchestrator_HandleExplicitCopy_Call) Return(err erro | |||||
| 809 | return _c | 810 | return _c | ||
| 810 | } | 811 | } | ||
| 811 | 812 | ||||
| 812 | - | func (_c *MockClipboardTextOrchestrator_HandleExplicitCopy_Call) RunAndReturn(run func(ctx context.Context, input port.ExplicitClipboardInput) error) *MockClipboardTextOrchestrator_HandleExplicitCopy_Call { | 813 | + | func (_c *MockClipboardTextOrchestrator_HandleExplicitCopy_Call) RunAndReturn(run func(ctx context.Context, input dto.ExplicitClipboardInput) error) *MockClipboardTextOrchestrator_HandleExplicitCopy_Call { |
| 813 | _c.Call.Return(run) | 814 | _c.Call.Return(run) | ||
| 814 | return _c | 815 | return _c | ||
| 815 | } | 816 | } | ||
| 816 | 817 | ||||
| 817 | // HandleSelectionUpdate provides a mock function for the type MockClipboardTextOrchestrator | 818 | // HandleSelectionUpdate provides a mock function for the type MockClipboardTextOrchestrator | ||
| 818 | - | func (_mock *MockClipboardTextOrchestrator) HandleSelectionUpdate(ctx context.Context, input port.SelectionClipboardInput) error { | 819 | + | func (_mock *MockClipboardTextOrchestrator) HandleSelectionUpdate(ctx context.Context, input dto.SelectionClipboardInput) error { |
| 819 | ret := _mock.Called(ctx, input) | 820 | ret := _mock.Called(ctx, input) | ||
| 820 | 821 | ||||
| 821 | if len(ret) == 0 { | 822 | if len(ret) == 0 { | ||
| @@ -823,7 +824,7 @@ func (_mock *MockClipboardTextOrchestrator) HandleSelectionUpdate(ctx context.Co | |||||
| 823 | } | 824 | } | ||
| 824 | 825 | ||||
| 825 | var r0 error | 826 | var r0 error | ||
| 826 | - | if returnFunc, ok := ret.Get(0).(func(context.Context, port.SelectionClipboardInput) error); ok { | 827 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, dto.SelectionClipboardInput) error); ok { |
| 827 | r0 = returnFunc(ctx, input) | 828 | r0 = returnFunc(ctx, input) | ||
| 828 | } else { | 829 | } else { | ||
| 829 | r0 = ret.Error(0) | 830 | r0 = ret.Error(0) | ||
| @@ -838,20 +839,20 @@ type MockClipboardTextOrchestrator_HandleSelectionUpdate_Call struct { | |||||
| 838 | 839 | ||||
| 839 | // HandleSelectionUpdate is a helper method to define mock.On call | 840 | // HandleSelectionUpdate is a helper method to define mock.On call | ||
| 840 | // - ctx context.Context | 841 | // - ctx context.Context | ||
| 841 | - | // - input port.SelectionClipboardInput | 842 | + | // - input dto.SelectionClipboardInput |
| 842 | func (_e *MockClipboardTextOrchestrator_Expecter) HandleSelectionUpdate(ctx interface{}, input interface{}) *MockClipboardTextOrchestrator_HandleSelectionUpdate_Call { | 843 | func (_e *MockClipboardTextOrchestrator_Expecter) HandleSelectionUpdate(ctx interface{}, input interface{}) *MockClipboardTextOrchestrator_HandleSelectionUpdate_Call { | ||
| 843 | return &MockClipboardTextOrchestrator_HandleSelectionUpdate_Call{Call: _e.mock.On("HandleSelectionUpdate", ctx, input)} | 844 | return &MockClipboardTextOrchestrator_HandleSelectionUpdate_Call{Call: _e.mock.On("HandleSelectionUpdate", ctx, input)} | ||
| 844 | } | 845 | } | ||
| 845 | 846 | ||||
| 846 | - | func (_c *MockClipboardTextOrchestrator_HandleSelectionUpdate_Call) Run(run func(ctx context.Context, input port.SelectionClipboardInput)) *MockClipboardTextOrchestrator_HandleSelectionUpdate_Call { | 847 | + | func (_c *MockClipboardTextOrchestrator_HandleSelectionUpdate_Call) Run(run func(ctx context.Context, input dto.SelectionClipboardInput)) *MockClipboardTextOrchestrator_HandleSelectionUpdate_Call { |
| 847 | _c.Call.Run(func(args mock.Arguments) { | 848 | _c.Call.Run(func(args mock.Arguments) { | ||
| 848 | var arg0 context.Context | 849 | var arg0 context.Context | ||
| 849 | if args[0] != nil { | 850 | if args[0] != nil { | ||
| 850 | arg0 = args[0].(context.Context) | 851 | arg0 = args[0].(context.Context) | ||
| 851 | } | 852 | } | ||
| 852 | - | var arg1 port.SelectionClipboardInput | 853 | + | var arg1 dto.SelectionClipboardInput |
| 853 | if args[1] != nil { | 854 | if args[1] != nil { | ||
| 854 | - | arg1 = args[1].(port.SelectionClipboardInput) | 855 | + | arg1 = args[1].(dto.SelectionClipboardInput) |
| 855 | } | 856 | } | ||
| 856 | run( | 857 | run( | ||
| 857 | arg0, | 858 | arg0, | ||
| @@ -866,7 +867,7 @@ func (_c *MockClipboardTextOrchestrator_HandleSelectionUpdate_Call) Return(err e | |||||
| 866 | return _c | 867 | return _c | ||
| 867 | } | 868 | } | ||
| 868 | 869 | ||||
| 869 | - | func (_c *MockClipboardTextOrchestrator_HandleSelectionUpdate_Call) RunAndReturn(run func(ctx context.Context, input port.SelectionClipboardInput) error) *MockClipboardTextOrchestrator_HandleSelectionUpdate_Call { | 870 | + | func (_c *MockClipboardTextOrchestrator_HandleSelectionUpdate_Call) RunAndReturn(run func(ctx context.Context, input dto.SelectionClipboardInput) error) *MockClipboardTextOrchestrator_HandleSelectionUpdate_Call { |
| 870 | _c.Call.Return(run) | 871 | _c.Call.Return(run) | ||
| 871 | return _c | 872 | return _c | ||
| 872 | } | 873 | } | ||
M internal/application/usecase/clipboard_text_orchestrator.go +37 -22
| @@ -8,6 +8,7 @@ import ( | |||
| 8 | 8 | "time" | |
| 9 | 9 | "unicode/utf8" | |
| 10 | 10 | ||
| 11 | + | "github.com/bnema/dumber/internal/application/dto" | |
| 11 | 12 | "github.com/bnema/dumber/internal/application/port" | |
| 12 | 13 | "github.com/bnema/dumber/internal/logging" | |
| 13 | 14 | ) | |
| @@ -26,13 +27,14 @@ type ClipboardTextOrchestratorUseCase struct { | |||
| 26 | 27 | } | |
| 27 | 28 | ||
| 28 | 29 | type clipboardScope struct { | |
| 29 | - | source port.ClipboardSource | |
| 30 | - | viewID port.WebViewID | |
| 30 | + | source dto.ClipboardSource | |
| 31 | + | viewID uint64 | |
| 31 | 32 | } | |
| 32 | 33 | ||
| 33 | 34 | type explicitCopyState struct { | |
| 34 | - | text string | |
| 35 | - | at time.Time | |
| 35 | + | text string | |
| 36 | + | action string | |
| 37 | + | at time.Time | |
| 36 | 38 | } | |
| 37 | 39 | ||
| 38 | 40 | var _ port.ClipboardTextOrchestrator = (*ClipboardTextOrchestratorUseCase)(nil) | |
| @@ -56,7 +58,7 @@ func NewClipboardTextOrchestrator( | |||
| 56 | 58 | // HandleSelectionUpdate applies auto-copy selection rules. | |
| 57 | 59 | func (uc *ClipboardTextOrchestratorUseCase) HandleSelectionUpdate( | |
| 58 | 60 | ctx context.Context, | |
| 59 | - | input port.SelectionClipboardInput, | |
| 61 | + | input dto.SelectionClipboardInput, | |
| 60 | 62 | ) error { | |
| 61 | 63 | if uc == nil || uc.autoCopyConfig == nil || !uc.autoCopyConfig.IsAutoCopyEnabled() { | |
| 62 | 64 | if uc != nil { | |
| @@ -94,19 +96,25 @@ func (uc *ClipboardTextOrchestratorUseCase) HandleSelectionUpdate( | |||
| 94 | 96 | uc.mu.Unlock() | |
| 95 | 97 | return fmt.Errorf("clipboard not available") | |
| 96 | 98 | } | |
| 99 | + | selectionState := input.Text | |
| 100 | + | explicitInput := dto.ExplicitClipboardInput{SourceEngine: input.SourceEngine, ViewID: input.ViewID, Action: "copy"} | |
| 101 | + | explicitState := explicitCopyState{text: input.Text, action: explicitInput.Action, at: currentTime} | |
| 102 | + | uc.lastSelection[scope] = selectionState | |
| 103 | + | uc.lastExplicit[explicitScope(explicitInput)] = explicitState | |
| 104 | + | uc.mu.Unlock() | |
| 105 | + | ||
| 97 | 106 | if err := uc.clipboard.WriteText(ctx, input.Text); err != nil { | |
| 107 | + | uc.mu.Lock() | |
| 108 | + | if uc.lastSelection[scope] == selectionState { | |
| 109 | + | delete(uc.lastSelection, scope) | |
| 110 | + | } | |
| 111 | + | if state, ok := uc.lastExplicit[explicitScope(explicitInput)]; ok && state == explicitState { | |
| 112 | + | delete(uc.lastExplicit, explicitScope(explicitInput)) | |
| 113 | + | } | |
| 98 | 114 | uc.mu.Unlock() | |
| 99 | 115 | return fmt.Errorf("clipboard write failed: %w", err) | |
| 100 | 116 | } | |
| 101 | 117 | ||
| 102 | - | uc.lastSelection[scope] = input.Text | |
| 103 | - | explicitInput := port.ExplicitClipboardInput{SourceEngine: input.SourceEngine, ViewID: input.ViewID} | |
| 104 | - | uc.lastExplicit[explicitScope(explicitInput)] = explicitCopyState{ | |
| 105 | - | text: input.Text, | |
| 106 | - | at: currentTime, | |
| 107 | - | } | |
| 108 | - | uc.mu.Unlock() | |
| 109 | - | ||
| 110 | 118 | if uc.toast != nil { | |
| 111 | 119 | uc.toast(textLen) | |
| 112 | 120 | } | |
| @@ -120,7 +128,7 @@ func (uc *ClipboardTextOrchestratorUseCase) HandleSelectionUpdate( | |||
| 120 | 128 | // HandleExplicitCopy applies explicit copy rules. | |
| 121 | 129 | func (uc *ClipboardTextOrchestratorUseCase) HandleExplicitCopy( | |
| 122 | 130 | ctx context.Context, | |
| 123 | - | input port.ExplicitClipboardInput, | |
| 131 | + | input dto.ExplicitClipboardInput, | |
| 124 | 132 | ) error { | |
| 125 | 133 | if uc == nil || input.Text == "" { | |
| 126 | 134 | return nil | |
| @@ -134,10 +142,13 @@ func (uc *ClipboardTextOrchestratorUseCase) HandleExplicitCopy( | |||
| 134 | 142 | textLen := utf8.RuneCountInString(input.Text) | |
| 135 | 143 | scope := explicitScope(input) | |
| 136 | 144 | ||
| 145 | + | state := explicitCopyState{text: input.Text, action: input.Action, at: currentTime} | |
| 146 | + | ||
| 137 | 147 | uc.mu.Lock() | |
| 138 | - | if state, ok := uc.lastExplicit[scope]; ok && | |
| 139 | - | input.Text == state.text && | |
| 140 | - | currentTime.Sub(state.at) < explicitClipboardDedupWindow { | |
| 148 | + | if previous, ok := uc.lastExplicit[scope]; ok && | |
| 149 | + | input.Text == previous.text && | |
| 150 | + | input.Action == previous.action && | |
| 151 | + | currentTime.Sub(previous.at) < explicitClipboardDedupWindow { | |
| 141 | 152 | uc.mu.Unlock() | |
| 142 | 153 | return nil | |
| 143 | 154 | } | |
| @@ -145,16 +156,20 @@ func (uc *ClipboardTextOrchestratorUseCase) HandleExplicitCopy( | |||
| 145 | 156 | uc.mu.Unlock() | |
| 146 | 157 | return fmt.Errorf("clipboard not available") | |
| 147 | 158 | } | |
| 159 | + | uc.lastExplicit[scope] = state | |
| 160 | + | uc.mu.Unlock() | |
| 161 | + | ||
| 148 | 162 | if !input.NativeHandled { | |
| 149 | 163 | if err := uc.clipboard.WriteText(ctx, input.Text); err != nil { | |
| 164 | + | uc.mu.Lock() | |
| 165 | + | if current, ok := uc.lastExplicit[scope]; ok && current == state { | |
| 166 | + | delete(uc.lastExplicit, scope) | |
| 167 | + | } | |
| 150 | 168 | uc.mu.Unlock() | |
| 151 | 169 | return fmt.Errorf("clipboard write failed: %w", err) | |
| 152 | 170 | } | |
| 153 | 171 | } | |
| 154 | 172 | ||
| 155 | - | uc.lastExplicit[scope] = explicitCopyState{text: input.Text, at: currentTime} | |
| 156 | - | uc.mu.Unlock() | |
| 157 | - | ||
| 158 | 173 | if uc.toast != nil { | |
| 159 | 174 | uc.toast(textLen) | |
| 160 | 175 | } | |
| @@ -167,10 +182,10 @@ func (uc *ClipboardTextOrchestratorUseCase) HandleExplicitCopy( | |||
| 167 | 182 | return nil | |
| 168 | 183 | } | |
| 169 | 184 | ||
| 170 | - | func selectionScope(input port.SelectionClipboardInput) clipboardScope { | |
| 185 | + | func selectionScope(input dto.SelectionClipboardInput) clipboardScope { | |
| 171 | 186 | return clipboardScope{source: input.SourceEngine, viewID: input.ViewID} | |
| 172 | 187 | } | |
| 173 | 188 | ||
| 174 | - | func explicitScope(input port.ExplicitClipboardInput) clipboardScope { | |
| 189 | + | func explicitScope(input dto.ExplicitClipboardInput) clipboardScope { | |
| 175 | 190 | return clipboardScope{source: input.SourceEngine, viewID: input.ViewID} | |
| 176 | 191 | } | |
| @@ -8,6 +8,7 @@ import ( | |||||
| 8 | "time" | 8 | "time" | ||
| 9 | "unicode/utf8" | 9 | "unicode/utf8" | ||
| 10 | 10 | ||||
| 11 | + | "github.com/bnema/dumber/internal/application/dto" | |||
| 11 | "github.com/bnema/dumber/internal/application/port" | 12 | "github.com/bnema/dumber/internal/application/port" | ||
| 12 | "github.com/bnema/dumber/internal/logging" | 13 | "github.com/bnema/dumber/internal/logging" | ||
| 13 | ) | 14 | ) | ||
| @@ -26,13 +27,14 @@ type ClipboardTextOrchestratorUseCase struct { | |||||
| 26 | } | 27 | } | ||
| 27 | 28 | ||||
| 28 | type clipboardScope struct { | 29 | type clipboardScope struct { | ||
| 29 | - | source port.ClipboardSource | 30 | + | source dto.ClipboardSource |
| 30 | - | viewID port.WebViewID | 31 | + | viewID uint64 |
| 31 | } | 32 | } | ||
| 32 | 33 | ||||
| 33 | type explicitCopyState struct { | 34 | type explicitCopyState struct { | ||
| 34 | - | text string | 35 | + | text string |
| 35 | - | at time.Time | 36 | + | action string |
| 37 | + | at time.Time | |||
| 36 | } | 38 | } | ||
| 37 | 39 | ||||
| 38 | var _ port.ClipboardTextOrchestrator = (*ClipboardTextOrchestratorUseCase)(nil) | 40 | var _ port.ClipboardTextOrchestrator = (*ClipboardTextOrchestratorUseCase)(nil) | ||
| @@ -56,7 +58,7 @@ func NewClipboardTextOrchestrator( | |||||
| 56 | // HandleSelectionUpdate applies auto-copy selection rules. | 58 | // HandleSelectionUpdate applies auto-copy selection rules. | ||
| 57 | func (uc *ClipboardTextOrchestratorUseCase) HandleSelectionUpdate( | 59 | func (uc *ClipboardTextOrchestratorUseCase) HandleSelectionUpdate( | ||
| 58 | ctx context.Context, | 60 | ctx context.Context, | ||
| 59 | - | input port.SelectionClipboardInput, | 61 | + | input dto.SelectionClipboardInput, |
| 60 | ) error { | 62 | ) error { | ||
| 61 | if uc == nil || uc.autoCopyConfig == nil || !uc.autoCopyConfig.IsAutoCopyEnabled() { | 63 | if uc == nil || uc.autoCopyConfig == nil || !uc.autoCopyConfig.IsAutoCopyEnabled() { | ||
| 62 | if uc != nil { | 64 | if uc != nil { | ||
| @@ -94,19 +96,25 @@ func (uc *ClipboardTextOrchestratorUseCase) HandleSelectionUpdate( | |||||
| 94 | uc.mu.Unlock() | 96 | uc.mu.Unlock() | ||
| 95 | return fmt.Errorf("clipboard not available") | 97 | return fmt.Errorf("clipboard not available") | ||
| 96 | } | 98 | } | ||
| 99 | + | selectionState := input.Text | |||
| 100 | + | explicitInput := dto.ExplicitClipboardInput{SourceEngine: input.SourceEngine, ViewID: input.ViewID, Action: "copy"} | |||
| 101 | + | explicitState := explicitCopyState{text: input.Text, action: explicitInput.Action, at: currentTime} | |||
| 102 | + | uc.lastSelection[scope] = selectionState | |||
| 103 | + | uc.lastExplicit[explicitScope(explicitInput)] = explicitState | |||
| 104 | + | uc.mu.Unlock() | |||
| 105 | + | ||||
| 97 | if err := uc.clipboard.WriteText(ctx, input.Text); err != nil { | 106 | if err := uc.clipboard.WriteText(ctx, input.Text); err != nil { | ||
| 107 | + | uc.mu.Lock() | |||
| 108 | + | if uc.lastSelection[scope] == selectionState { | |||
| 109 | + | delete(uc.lastSelection, scope) | |||
| 110 | + | } | |||
| 111 | + | if state, ok := uc.lastExplicit[explicitScope(explicitInput)]; ok && state == explicitState { | |||
| 112 | + | delete(uc.lastExplicit, explicitScope(explicitInput)) | |||
| 113 | + | } | |||
| 98 | uc.mu.Unlock() | 114 | uc.mu.Unlock() | ||
| 99 | return fmt.Errorf("clipboard write failed: %w", err) | 115 | return fmt.Errorf("clipboard write failed: %w", err) | ||
| 100 | } | 116 | } | ||
| 101 | 117 | ||||
| 102 | - | uc.lastSelection[scope] = input.Text | |||
| 103 | - | explicitInput := port.ExplicitClipboardInput{SourceEngine: input.SourceEngine, ViewID: input.ViewID} | |||
| 104 | - | uc.lastExplicit[explicitScope(explicitInput)] = explicitCopyState{ | |||
| 105 | - | text: input.Text, | |||
| 106 | - | at: currentTime, | |||
| 107 | - | } | |||
| 108 | - | uc.mu.Unlock() | |||
| 109 | - | ||||
| 110 | if uc.toast != nil { | 118 | if uc.toast != nil { | ||
| 111 | uc.toast(textLen) | 119 | uc.toast(textLen) | ||
| 112 | } | 120 | } | ||
| @@ -120,7 +128,7 @@ func (uc *ClipboardTextOrchestratorUseCase) HandleSelectionUpdate( | |||||
| 120 | // HandleExplicitCopy applies explicit copy rules. | 128 | // HandleExplicitCopy applies explicit copy rules. | ||
| 121 | func (uc *ClipboardTextOrchestratorUseCase) HandleExplicitCopy( | 129 | func (uc *ClipboardTextOrchestratorUseCase) HandleExplicitCopy( | ||
| 122 | ctx context.Context, | 130 | ctx context.Context, | ||
| 123 | - | input port.ExplicitClipboardInput, | 131 | + | input dto.ExplicitClipboardInput, |
| 124 | ) error { | 132 | ) error { | ||
| 125 | if uc == nil || input.Text == "" { | 133 | if uc == nil || input.Text == "" { | ||
| 126 | return nil | 134 | return nil | ||
| @@ -134,10 +142,13 @@ func (uc *ClipboardTextOrchestratorUseCase) HandleExplicitCopy( | |||||
| 134 | textLen := utf8.RuneCountInString(input.Text) | 142 | textLen := utf8.RuneCountInString(input.Text) | ||
| 135 | scope := explicitScope(input) | 143 | scope := explicitScope(input) | ||
| 136 | 144 | ||||
| 145 | + | state := explicitCopyState{text: input.Text, action: input.Action, at: currentTime} | |||
| 146 | + | ||||
| 137 | uc.mu.Lock() | 147 | uc.mu.Lock() | ||
| 138 | - | if state, ok := uc.lastExplicit[scope]; ok && | 148 | + | if previous, ok := uc.lastExplicit[scope]; ok && |
| 139 | - | input.Text == state.text && | 149 | + | input.Text == previous.text && |
| 140 | - | currentTime.Sub(state.at) < explicitClipboardDedupWindow { | 150 | + | input.Action == previous.action && |
| 151 | + | currentTime.Sub(previous.at) < explicitClipboardDedupWindow { | |||
| 141 | uc.mu.Unlock() | 152 | uc.mu.Unlock() | ||
| 142 | return nil | 153 | return nil | ||
| 143 | } | 154 | } | ||
| @@ -145,16 +156,20 @@ func (uc *ClipboardTextOrchestratorUseCase) HandleExplicitCopy( | |||||
| 145 | uc.mu.Unlock() | 156 | uc.mu.Unlock() | ||
| 146 | return fmt.Errorf("clipboard not available") | 157 | return fmt.Errorf("clipboard not available") | ||
| 147 | } | 158 | } | ||
| 159 | + | uc.lastExplicit[scope] = state | |||
| 160 | + | uc.mu.Unlock() | |||
| 161 | + | ||||
| 148 | if !input.NativeHandled { | 162 | if !input.NativeHandled { | ||
| 149 | if err := uc.clipboard.WriteText(ctx, input.Text); err != nil { | 163 | if err := uc.clipboard.WriteText(ctx, input.Text); err != nil { | ||
| 164 | + | uc.mu.Lock() | |||
| 165 | + | if current, ok := uc.lastExplicit[scope]; ok && current == state { | |||
| 166 | + | delete(uc.lastExplicit, scope) | |||
| 167 | + | } | |||
| 150 | uc.mu.Unlock() | 168 | uc.mu.Unlock() | ||
| 151 | return fmt.Errorf("clipboard write failed: %w", err) | 169 | return fmt.Errorf("clipboard write failed: %w", err) | ||
| 152 | } | 170 | } | ||
| 153 | } | 171 | } | ||
| 154 | 172 | ||||
| 155 | - | uc.lastExplicit[scope] = explicitCopyState{text: input.Text, at: currentTime} | |||
| 156 | - | uc.mu.Unlock() | |||
| 157 | - | ||||
| 158 | if uc.toast != nil { | 173 | if uc.toast != nil { | ||
| 159 | uc.toast(textLen) | 174 | uc.toast(textLen) | ||
| 160 | } | 175 | } | ||
| @@ -167,10 +182,10 @@ func (uc *ClipboardTextOrchestratorUseCase) HandleExplicitCopy( | |||||
| 167 | return nil | 182 | return nil | ||
| 168 | } | 183 | } | ||
| 169 | 184 | ||||
| 170 | - | func selectionScope(input port.SelectionClipboardInput) clipboardScope { | 185 | + | func selectionScope(input dto.SelectionClipboardInput) clipboardScope { |
| 171 | return clipboardScope{source: input.SourceEngine, viewID: input.ViewID} | 186 | return clipboardScope{source: input.SourceEngine, viewID: input.ViewID} | ||
| 172 | } | 187 | } | ||
| 173 | 188 | ||||
| 174 | - | func explicitScope(input port.ExplicitClipboardInput) clipboardScope { | 189 | + | func explicitScope(input dto.ExplicitClipboardInput) clipboardScope { |
| 175 | return clipboardScope{source: input.SourceEngine, viewID: input.ViewID} | 190 | return clipboardScope{source: input.SourceEngine, viewID: input.ViewID} | ||
| 176 | } | 191 | } | ||
M internal/application/usecase/clipboard_text_orchestrator_test.go +50 -30
| @@ -7,7 +7,7 @@ import ( | |||
| 7 | 7 | "testing" | |
| 8 | 8 | "time" | |
| 9 | 9 | ||
| 10 | - | "github.com/bnema/dumber/internal/application/port" | |
| 10 | + | "github.com/bnema/dumber/internal/application/dto" | |
| 11 | 11 | portmocks "github.com/bnema/dumber/internal/application/port/mocks" | |
| 12 | 12 | "github.com/bnema/dumber/internal/domain/entity" | |
| 13 | 13 | ) | |
| @@ -85,7 +85,7 @@ func TestClipboardTextOrchestrator_HandleSelectionUpdate_AutoCopyDisabledDoesNot | |||
| 85 | 85 | toastCalls = append(toastCalls, textLen) | |
| 86 | 86 | }) | |
| 87 | 87 | ||
| 88 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "hello", SourceEngine: port.ClipboardSourceCEF}); err != nil { | |
| 88 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "hello", SourceEngine: dto.ClipboardSourceCEF}); err != nil { | |
| 89 | 89 | t.Fatalf("HandleSelectionUpdate() error = %v, want nil", err) | |
| 90 | 90 | } | |
| 91 | 91 | ||
| @@ -106,19 +106,19 @@ func TestClipboardTextOrchestrator_HandleSelectionUpdate_WritesOncePerUniqueSele | |||
| 106 | 106 | toastCalls = append(toastCalls, textLen) | |
| 107 | 107 | }) | |
| 108 | 108 | ||
| 109 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "é", SourceEngine: port.ClipboardSourceCEF}); err != nil { | |
| 109 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "é", SourceEngine: dto.ClipboardSourceCEF}); err != nil { | |
| 110 | 110 | t.Fatalf("HandleSelectionUpdate(short) error = %v, want nil", err) | |
| 111 | 111 | } | |
| 112 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "éé", SourceEngine: port.ClipboardSourceCEF}); err != nil { | |
| 112 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceCEF}); err != nil { | |
| 113 | 113 | t.Fatalf("HandleSelectionUpdate(first) error = %v, want nil", err) | |
| 114 | 114 | } | |
| 115 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "éé", SourceEngine: port.ClipboardSourceCEF}); err != nil { | |
| 115 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceCEF}); err != nil { | |
| 116 | 116 | t.Fatalf("HandleSelectionUpdate(duplicate) error = %v, want nil", err) | |
| 117 | 117 | } | |
| 118 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "", SourceEngine: port.ClipboardSourceCEF}); err != nil { | |
| 118 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "", SourceEngine: dto.ClipboardSourceCEF}); err != nil { | |
| 119 | 119 | t.Fatalf("HandleSelectionUpdate(reset) error = %v, want nil", err) | |
| 120 | 120 | } | |
| 121 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "éé", SourceEngine: port.ClipboardSourceCEF}); err != nil { | |
| 121 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceCEF}); err != nil { | |
| 122 | 122 | t.Fatalf("HandleSelectionUpdate(after reset) error = %v, want nil", err) | |
| 123 | 123 | } | |
| 124 | 124 | ||
| @@ -136,10 +136,10 @@ func TestClipboardTextOrchestrator_HandleSelectionUpdate_DoesNotDedupAcrossEngin | |||
| 136 | 136 | ||
| 137 | 137 | uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, nil) | |
| 138 | 138 | ||
| 139 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "shared", SourceEngine: port.ClipboardSourceWebKit}); err != nil { | |
| 139 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceWebKit}); err != nil { | |
| 140 | 140 | t.Fatalf("HandleSelectionUpdate(webkit) error = %v, want nil", err) | |
| 141 | 141 | } | |
| 142 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "shared", SourceEngine: port.ClipboardSourceCEF}); err != nil { | |
| 142 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF}); err != nil { | |
| 143 | 143 | t.Fatalf("HandleSelectionUpdate(cef) error = %v, want nil", err) | |
| 144 | 144 | } | |
| 145 | 145 | } | |
| @@ -153,10 +153,10 @@ func TestClipboardTextOrchestrator_HandleSelectionUpdate_DoesNotDedupAcrossViews | |||
| 153 | 153 | ||
| 154 | 154 | uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, nil) | |
| 155 | 155 | ||
| 156 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "shared", SourceEngine: port.ClipboardSourceCEF, ViewID: 1}); err != nil { | |
| 156 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1}); err != nil { | |
| 157 | 157 | t.Fatalf("HandleSelectionUpdate(view 1) error = %v, want nil", err) | |
| 158 | 158 | } | |
| 159 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "shared", SourceEngine: port.ClipboardSourceCEF, ViewID: 2}); err != nil { | |
| 159 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, ViewID: 2}); err != nil { | |
| 160 | 160 | t.Fatalf("HandleSelectionUpdate(view 2) error = %v, want nil", err) | |
| 161 | 161 | } | |
| 162 | 162 | } | |
| @@ -176,15 +176,15 @@ func TestClipboardTextOrchestrator_HandleExplicitCopy_DeduplicatesIdenticalPaylo | |||
| 176 | 176 | now := time.Unix(0, 0) | |
| 177 | 177 | uc.now = func() time.Time { return now } | |
| 178 | 178 | ||
| 179 | - | if err := uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: "éé", SourceEngine: port.ClipboardSourceWebKit, Action: "copy"}); err != nil { | |
| 179 | + | if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceWebKit, Action: "copy"}); err != nil { | |
| 180 | 180 | t.Fatalf("HandleExplicitCopy(first) error = %v, want nil", err) | |
| 181 | 181 | } | |
| 182 | 182 | now = now.Add(100 * time.Millisecond) | |
| 183 | - | if err := uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: "éé", SourceEngine: port.ClipboardSourceWebKit, Action: "copy"}); err != nil { | |
| 183 | + | if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceWebKit, Action: "copy"}); err != nil { | |
| 184 | 184 | t.Fatalf("HandleExplicitCopy(duplicate) error = %v, want nil", err) | |
| 185 | 185 | } | |
| 186 | 186 | now = now.Add(151 * time.Millisecond) | |
| 187 | - | if err := uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: "éé", SourceEngine: port.ClipboardSourceWebKit, Action: "copy"}); err != nil { | |
| 187 | + | if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceWebKit, Action: "copy"}); err != nil { | |
| 188 | 188 | t.Fatalf("HandleExplicitCopy(after window) error = %v, want nil", err) | |
| 189 | 189 | } | |
| 190 | 190 | ||
| @@ -203,7 +203,7 @@ func TestClipboardTextOrchestrator_HandleExplicitCopy_NativeHandledSkipsBackendW | |||
| 203 | 203 | toastCalls = append(toastCalls, textLen) | |
| 204 | 204 | }) | |
| 205 | 205 | ||
| 206 | - | if err := uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: "shared", SourceEngine: port.ClipboardSourceWebKit, Action: "copy", NativeHandled: true}); err != nil { | |
| 206 | + | if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceWebKit, Action: "copy", NativeHandled: true}); err != nil { | |
| 207 | 207 | t.Fatalf("HandleExplicitCopy() error = %v, want nil", err) | |
| 208 | 208 | } | |
| 209 | 209 | ||
| @@ -218,6 +218,26 @@ func TestClipboardTextOrchestrator_HandleExplicitCopy_NativeHandledSkipsBackendW | |||
| 218 | 218 | } | |
| 219 | 219 | } | |
| 220 | 220 | ||
| 221 | + | func TestClipboardTextOrchestrator_HandleExplicitCopy_DoesNotDedupDifferentActionsWithinWindow(t *testing.T) { | |
| 222 | + | ctx := context.Background() | |
| 223 | + | clipboard := portmocks.NewMockClipboard(t) | |
| 224 | + | autoCopyConfig := &staticAutoCopyConfig{enabled: true} | |
| 225 | + | ||
| 226 | + | clipboard.EXPECT().WriteText(ctx, "shared").Return(nil).Times(2) | |
| 227 | + | ||
| 228 | + | uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, nil) | |
| 229 | + | now := time.Unix(0, 0) | |
| 230 | + | uc.now = func() time.Time { return now } | |
| 231 | + | ||
| 232 | + | if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1, Action: "copy"}); err != nil { | |
| 233 | + | t.Fatalf("HandleExplicitCopy(copy) error = %v, want nil", err) | |
| 234 | + | } | |
| 235 | + | now = now.Add(100 * time.Millisecond) | |
| 236 | + | if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1, Action: "cut"}); err != nil { | |
| 237 | + | t.Fatalf("HandleExplicitCopy(cut) error = %v, want nil", err) | |
| 238 | + | } | |
| 239 | + | } | |
| 240 | + | ||
| 221 | 241 | func TestClipboardTextOrchestrator_HandleExplicitCopy_DeduplicatesRecentAutoCopySelection(t *testing.T) { | |
| 222 | 242 | ctx := context.Background() | |
| 223 | 243 | clipboard := portmocks.NewMockClipboard(t) | |
| @@ -233,11 +253,11 @@ func TestClipboardTextOrchestrator_HandleExplicitCopy_DeduplicatesRecentAutoCopy | |||
| 233 | 253 | now := time.Unix(0, 0) | |
| 234 | 254 | uc.now = func() time.Time { return now } | |
| 235 | 255 | ||
| 236 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "shared", SourceEngine: port.ClipboardSourceCEF, ViewID: 1}); err != nil { | |
| 256 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1}); err != nil { | |
| 237 | 257 | t.Fatalf("HandleSelectionUpdate() error = %v, want nil", err) | |
| 238 | 258 | } | |
| 239 | 259 | now = now.Add(100 * time.Millisecond) | |
| 240 | - | if err := uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: "shared", SourceEngine: port.ClipboardSourceCEF, ViewID: 1, Action: "cut"}); err != nil { | |
| 260 | + | if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1, Action: "copy"}); err != nil { | |
| 241 | 261 | t.Fatalf("HandleExplicitCopy() error = %v, want nil", err) | |
| 242 | 262 | } | |
| 243 | 263 | ||
| @@ -259,10 +279,10 @@ func TestClipboardTextOrchestrator_HandleExplicitCopy_DoesNotDedupAcrossEngines( | |||
| 259 | 279 | uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, nil) | |
| 260 | 280 | uc.now = func() time.Time { return time.Unix(0, 0) } | |
| 261 | 281 | ||
| 262 | - | if err := uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: "shared", SourceEngine: port.ClipboardSourceWebKit, Action: "copy"}); err != nil { | |
| 282 | + | if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceWebKit, Action: "copy"}); err != nil { | |
| 263 | 283 | t.Fatalf("HandleExplicitCopy(webkit) error = %v, want nil", err) | |
| 264 | 284 | } | |
| 265 | - | if err := uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: "shared", SourceEngine: port.ClipboardSourceCEF, Action: "copy"}); err != nil { | |
| 285 | + | if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, Action: "copy"}); err != nil { | |
| 266 | 286 | t.Fatalf("HandleExplicitCopy(cef) error = %v, want nil", err) | |
| 267 | 287 | } | |
| 268 | 288 | } | |
| @@ -277,10 +297,10 @@ func TestClipboardTextOrchestrator_HandleExplicitCopy_DoesNotDedupAcrossViews(t | |||
| 277 | 297 | uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, nil) | |
| 278 | 298 | uc.now = func() time.Time { return time.Unix(0, 0) } | |
| 279 | 299 | ||
| 280 | - | if err := uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: "shared", SourceEngine: port.ClipboardSourceCEF, ViewID: 1, Action: "copy"}); err != nil { | |
| 300 | + | if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1, Action: "copy"}); err != nil { | |
| 281 | 301 | t.Fatalf("HandleExplicitCopy(view 1) error = %v, want nil", err) | |
| 282 | 302 | } | |
| 283 | - | if err := uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: "shared", SourceEngine: port.ClipboardSourceCEF, ViewID: 2, Action: "copy"}); err != nil { | |
| 303 | + | if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, ViewID: 2, Action: "copy"}); err != nil { | |
| 284 | 304 | t.Fatalf("HandleExplicitCopy(view 2) error = %v, want nil", err) | |
| 285 | 305 | } | |
| 286 | 306 | } | |
| @@ -299,7 +319,7 @@ func TestClipboardTextOrchestrator_HandleExplicitCopy_KeepsBoundedStatePerScope( | |||
| 299 | 319 | for i, text := range []string{"one", "two", "three"} { | |
| 300 | 320 | advancedTime := time.Unix(0, 0).Add(time.Duration(i) * time.Second) | |
| 301 | 321 | uc.now = func() time.Time { return advancedTime } | |
| 302 | - | if err := uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: text, SourceEngine: port.ClipboardSourceCEF, ViewID: 7, Action: "copy"}); err != nil { | |
| 322 | + | if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: text, SourceEngine: dto.ClipboardSourceCEF, ViewID: 7, Action: "copy"}); err != nil { | |
| 303 | 323 | t.Fatalf("HandleExplicitCopy(%q) error = %v, want nil", text, err) | |
| 304 | 324 | } | |
| 305 | 325 | } | |
| @@ -318,15 +338,15 @@ func TestClipboardTextOrchestrator_HandleSelectionUpdate_ReenableClearsDedupStat | |||
| 318 | 338 | ||
| 319 | 339 | uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, nil) | |
| 320 | 340 | ||
| 321 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "toggle", SourceEngine: port.ClipboardSourceCEF, ViewID: 1}); err != nil { | |
| 341 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "toggle", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1}); err != nil { | |
| 322 | 342 | t.Fatalf("HandleSelectionUpdate(enabled) error = %v, want nil", err) | |
| 323 | 343 | } | |
| 324 | 344 | autoCopyConfig.enabled = false | |
| 325 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "toggle", SourceEngine: port.ClipboardSourceCEF, ViewID: 1}); err != nil { | |
| 345 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "toggle", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1}); err != nil { | |
| 326 | 346 | t.Fatalf("HandleSelectionUpdate(disabled) error = %v, want nil", err) | |
| 327 | 347 | } | |
| 328 | 348 | autoCopyConfig.enabled = true | |
| 329 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "toggle", SourceEngine: port.ClipboardSourceCEF, ViewID: 1}); err != nil { | |
| 349 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "toggle", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1}); err != nil { | |
| 330 | 350 | t.Fatalf("HandleSelectionUpdate(reenabled) error = %v, want nil", err) | |
| 331 | 351 | } | |
| 332 | 352 | } | |
| @@ -342,7 +362,7 @@ func TestClipboardTextOrchestrator_HandleExplicitCopy_ClipboardFailureDoesNotToa | |||
| 342 | 362 | toastCalls = append(toastCalls, textLen) | |
| 343 | 363 | }) | |
| 344 | 364 | ||
| 345 | - | err := uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: "éé", SourceEngine: port.ClipboardSourceCEF, Action: "copy"}) | |
| 365 | + | err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceCEF, Action: "copy"}) | |
| 346 | 366 | if err == nil { | |
| 347 | 367 | t.Fatal("HandleExplicitCopy() error = nil, want non-nil") | |
| 348 | 368 | } | |
| @@ -362,13 +382,13 @@ func TestClipboardTextOrchestrator_HandleSelectionUpdate_DedupIsAtomic(t *testin | |||
| 362 | 382 | ||
| 363 | 383 | done1 := make(chan error, 1) | |
| 364 | 384 | go func() { | |
| 365 | - | done1 <- uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "éé", SourceEngine: port.ClipboardSourceCEF}) | |
| 385 | + | done1 <- uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceCEF}) | |
| 366 | 386 | }() | |
| 367 | 387 | <-clipboard.started | |
| 368 | 388 | ||
| 369 | 389 | done2 := make(chan error, 1) | |
| 370 | 390 | go func() { | |
| 371 | - | done2 <- uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "éé", SourceEngine: port.ClipboardSourceCEF}) | |
| 391 | + | done2 <- uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceCEF}) | |
| 372 | 392 | }() | |
| 373 | 393 | ||
| 374 | 394 | select { | |
| @@ -406,13 +426,13 @@ func TestClipboardTextOrchestrator_HandleExplicitCopy_DedupIsAtomic(t *testing.T | |||
| 406 | 426 | ||
| 407 | 427 | done1 := make(chan error, 1) | |
| 408 | 428 | go func() { | |
| 409 | - | done1 <- uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: "éé", SourceEngine: port.ClipboardSourceWebKit, Action: "copy"}) | |
| 429 | + | done1 <- uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceWebKit, Action: "copy"}) | |
| 410 | 430 | }() | |
| 411 | 431 | <-clipboard.started | |
| 412 | 432 | ||
| 413 | 433 | done2 := make(chan error, 1) | |
| 414 | 434 | go func() { | |
| 415 | - | done2 <- uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: "éé", SourceEngine: port.ClipboardSourceWebKit, Action: "copy"}) | |
| 435 | + | done2 <- uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceWebKit, Action: "copy"}) | |
| 416 | 436 | }() | |
| 417 | 437 | ||
| 418 | 438 | select { | |
| @@ -7,7 +7,7 @@ import ( | |||||
| 7 | "testing" | 7 | "testing" | ||
| 8 | "time" | 8 | "time" | ||
| 9 | 9 | ||||
| 10 | - | "github.com/bnema/dumber/internal/application/port" | 10 | + | "github.com/bnema/dumber/internal/application/dto" |
| 11 | portmocks "github.com/bnema/dumber/internal/application/port/mocks" | 11 | portmocks "github.com/bnema/dumber/internal/application/port/mocks" | ||
| 12 | "github.com/bnema/dumber/internal/domain/entity" | 12 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 13 | ) | 13 | ) | ||
| @@ -85,7 +85,7 @@ func TestClipboardTextOrchestrator_HandleSelectionUpdate_AutoCopyDisabledDoesNot | |||||
| 85 | toastCalls = append(toastCalls, textLen) | 85 | toastCalls = append(toastCalls, textLen) | ||
| 86 | }) | 86 | }) | ||
| 87 | 87 | ||||
| 88 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "hello", SourceEngine: port.ClipboardSourceCEF}); err != nil { | 88 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "hello", SourceEngine: dto.ClipboardSourceCEF}); err != nil { |
| 89 | t.Fatalf("HandleSelectionUpdate() error = %v, want nil", err) | 89 | t.Fatalf("HandleSelectionUpdate() error = %v, want nil", err) | ||
| 90 | } | 90 | } | ||
| 91 | 91 | ||||
| @@ -106,19 +106,19 @@ func TestClipboardTextOrchestrator_HandleSelectionUpdate_WritesOncePerUniqueSele | |||||
| 106 | toastCalls = append(toastCalls, textLen) | 106 | toastCalls = append(toastCalls, textLen) | ||
| 107 | }) | 107 | }) | ||
| 108 | 108 | ||||
| 109 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "é", SourceEngine: port.ClipboardSourceCEF}); err != nil { | 109 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "é", SourceEngine: dto.ClipboardSourceCEF}); err != nil { |
| 110 | t.Fatalf("HandleSelectionUpdate(short) error = %v, want nil", err) | 110 | t.Fatalf("HandleSelectionUpdate(short) error = %v, want nil", err) | ||
| 111 | } | 111 | } | ||
| 112 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "éé", SourceEngine: port.ClipboardSourceCEF}); err != nil { | 112 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceCEF}); err != nil { |
| 113 | t.Fatalf("HandleSelectionUpdate(first) error = %v, want nil", err) | 113 | t.Fatalf("HandleSelectionUpdate(first) error = %v, want nil", err) | ||
| 114 | } | 114 | } | ||
| 115 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "éé", SourceEngine: port.ClipboardSourceCEF}); err != nil { | 115 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceCEF}); err != nil { |
| 116 | t.Fatalf("HandleSelectionUpdate(duplicate) error = %v, want nil", err) | 116 | t.Fatalf("HandleSelectionUpdate(duplicate) error = %v, want nil", err) | ||
| 117 | } | 117 | } | ||
| 118 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "", SourceEngine: port.ClipboardSourceCEF}); err != nil { | 118 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "", SourceEngine: dto.ClipboardSourceCEF}); err != nil { |
| 119 | t.Fatalf("HandleSelectionUpdate(reset) error = %v, want nil", err) | 119 | t.Fatalf("HandleSelectionUpdate(reset) error = %v, want nil", err) | ||
| 120 | } | 120 | } | ||
| 121 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "éé", SourceEngine: port.ClipboardSourceCEF}); err != nil { | 121 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceCEF}); err != nil { |
| 122 | t.Fatalf("HandleSelectionUpdate(after reset) error = %v, want nil", err) | 122 | t.Fatalf("HandleSelectionUpdate(after reset) error = %v, want nil", err) | ||
| 123 | } | 123 | } | ||
| 124 | 124 | ||||
| @@ -136,10 +136,10 @@ func TestClipboardTextOrchestrator_HandleSelectionUpdate_DoesNotDedupAcrossEngin | |||||
| 136 | 136 | ||||
| 137 | uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, nil) | 137 | uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, nil) | ||
| 138 | 138 | ||||
| 139 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "shared", SourceEngine: port.ClipboardSourceWebKit}); err != nil { | 139 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceWebKit}); err != nil { |
| 140 | t.Fatalf("HandleSelectionUpdate(webkit) error = %v, want nil", err) | 140 | t.Fatalf("HandleSelectionUpdate(webkit) error = %v, want nil", err) | ||
| 141 | } | 141 | } | ||
| 142 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "shared", SourceEngine: port.ClipboardSourceCEF}); err != nil { | 142 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF}); err != nil { |
| 143 | t.Fatalf("HandleSelectionUpdate(cef) error = %v, want nil", err) | 143 | t.Fatalf("HandleSelectionUpdate(cef) error = %v, want nil", err) | ||
| 144 | } | 144 | } | ||
| 145 | } | 145 | } | ||
| @@ -153,10 +153,10 @@ func TestClipboardTextOrchestrator_HandleSelectionUpdate_DoesNotDedupAcrossViews | |||||
| 153 | 153 | ||||
| 154 | uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, nil) | 154 | uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, nil) | ||
| 155 | 155 | ||||
| 156 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "shared", SourceEngine: port.ClipboardSourceCEF, ViewID: 1}); err != nil { | 156 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1}); err != nil { |
| 157 | t.Fatalf("HandleSelectionUpdate(view 1) error = %v, want nil", err) | 157 | t.Fatalf("HandleSelectionUpdate(view 1) error = %v, want nil", err) | ||
| 158 | } | 158 | } | ||
| 159 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "shared", SourceEngine: port.ClipboardSourceCEF, ViewID: 2}); err != nil { | 159 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, ViewID: 2}); err != nil { |
| 160 | t.Fatalf("HandleSelectionUpdate(view 2) error = %v, want nil", err) | 160 | t.Fatalf("HandleSelectionUpdate(view 2) error = %v, want nil", err) | ||
| 161 | } | 161 | } | ||
| 162 | } | 162 | } | ||
| @@ -176,15 +176,15 @@ func TestClipboardTextOrchestrator_HandleExplicitCopy_DeduplicatesIdenticalPaylo | |||||
| 176 | now := time.Unix(0, 0) | 176 | now := time.Unix(0, 0) | ||
| 177 | uc.now = func() time.Time { return now } | 177 | uc.now = func() time.Time { return now } | ||
| 178 | 178 | ||||
| 179 | - | if err := uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: "éé", SourceEngine: port.ClipboardSourceWebKit, Action: "copy"}); err != nil { | 179 | + | if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceWebKit, Action: "copy"}); err != nil { |
| 180 | t.Fatalf("HandleExplicitCopy(first) error = %v, want nil", err) | 180 | t.Fatalf("HandleExplicitCopy(first) error = %v, want nil", err) | ||
| 181 | } | 181 | } | ||
| 182 | now = now.Add(100 * time.Millisecond) | 182 | now = now.Add(100 * time.Millisecond) | ||
| 183 | - | if err := uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: "éé", SourceEngine: port.ClipboardSourceWebKit, Action: "copy"}); err != nil { | 183 | + | if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceWebKit, Action: "copy"}); err != nil { |
| 184 | t.Fatalf("HandleExplicitCopy(duplicate) error = %v, want nil", err) | 184 | t.Fatalf("HandleExplicitCopy(duplicate) error = %v, want nil", err) | ||
| 185 | } | 185 | } | ||
| 186 | now = now.Add(151 * time.Millisecond) | 186 | now = now.Add(151 * time.Millisecond) | ||
| 187 | - | if err := uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: "éé", SourceEngine: port.ClipboardSourceWebKit, Action: "copy"}); err != nil { | 187 | + | if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceWebKit, Action: "copy"}); err != nil { |
| 188 | t.Fatalf("HandleExplicitCopy(after window) error = %v, want nil", err) | 188 | t.Fatalf("HandleExplicitCopy(after window) error = %v, want nil", err) | ||
| 189 | } | 189 | } | ||
| 190 | 190 | ||||
| @@ -203,7 +203,7 @@ func TestClipboardTextOrchestrator_HandleExplicitCopy_NativeHandledSkipsBackendW | |||||
| 203 | toastCalls = append(toastCalls, textLen) | 203 | toastCalls = append(toastCalls, textLen) | ||
| 204 | }) | 204 | }) | ||
| 205 | 205 | ||||
| 206 | - | if err := uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: "shared", SourceEngine: port.ClipboardSourceWebKit, Action: "copy", NativeHandled: true}); err != nil { | 206 | + | if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceWebKit, Action: "copy", NativeHandled: true}); err != nil { |
| 207 | t.Fatalf("HandleExplicitCopy() error = %v, want nil", err) | 207 | t.Fatalf("HandleExplicitCopy() error = %v, want nil", err) | ||
| 208 | } | 208 | } | ||
| 209 | 209 | ||||
| @@ -218,6 +218,26 @@ func TestClipboardTextOrchestrator_HandleExplicitCopy_NativeHandledSkipsBackendW | |||||
| 218 | } | 218 | } | ||
| 219 | } | 219 | } | ||
| 220 | 220 | ||||
| 221 | + | func TestClipboardTextOrchestrator_HandleExplicitCopy_DoesNotDedupDifferentActionsWithinWindow(t *testing.T) { | |||
| 222 | + | ctx := context.Background() | |||
| 223 | + | clipboard := portmocks.NewMockClipboard(t) | |||
| 224 | + | autoCopyConfig := &staticAutoCopyConfig{enabled: true} | |||
| 225 | + | ||||
| 226 | + | clipboard.EXPECT().WriteText(ctx, "shared").Return(nil).Times(2) | |||
| 227 | + | ||||
| 228 | + | uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, nil) | |||
| 229 | + | now := time.Unix(0, 0) | |||
| 230 | + | uc.now = func() time.Time { return now } | |||
| 231 | + | ||||
| 232 | + | if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1, Action: "copy"}); err != nil { | |||
| 233 | + | t.Fatalf("HandleExplicitCopy(copy) error = %v, want nil", err) | |||
| 234 | + | } | |||
| 235 | + | now = now.Add(100 * time.Millisecond) | |||
| 236 | + | if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1, Action: "cut"}); err != nil { | |||
| 237 | + | t.Fatalf("HandleExplicitCopy(cut) error = %v, want nil", err) | |||
| 238 | + | } | |||
| 239 | + | } | |||
| 240 | + | ||||
| 221 | func TestClipboardTextOrchestrator_HandleExplicitCopy_DeduplicatesRecentAutoCopySelection(t *testing.T) { | 241 | func TestClipboardTextOrchestrator_HandleExplicitCopy_DeduplicatesRecentAutoCopySelection(t *testing.T) { | ||
| 222 | ctx := context.Background() | 242 | ctx := context.Background() | ||
| 223 | clipboard := portmocks.NewMockClipboard(t) | 243 | clipboard := portmocks.NewMockClipboard(t) | ||
| @@ -233,11 +253,11 @@ func TestClipboardTextOrchestrator_HandleExplicitCopy_DeduplicatesRecentAutoCopy | |||||
| 233 | now := time.Unix(0, 0) | 253 | now := time.Unix(0, 0) | ||
| 234 | uc.now = func() time.Time { return now } | 254 | uc.now = func() time.Time { return now } | ||
| 235 | 255 | ||||
| 236 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "shared", SourceEngine: port.ClipboardSourceCEF, ViewID: 1}); err != nil { | 256 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1}); err != nil { |
| 237 | t.Fatalf("HandleSelectionUpdate() error = %v, want nil", err) | 257 | t.Fatalf("HandleSelectionUpdate() error = %v, want nil", err) | ||
| 238 | } | 258 | } | ||
| 239 | now = now.Add(100 * time.Millisecond) | 259 | now = now.Add(100 * time.Millisecond) | ||
| 240 | - | if err := uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: "shared", SourceEngine: port.ClipboardSourceCEF, ViewID: 1, Action: "cut"}); err != nil { | 260 | + | if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1, Action: "copy"}); err != nil { |
| 241 | t.Fatalf("HandleExplicitCopy() error = %v, want nil", err) | 261 | t.Fatalf("HandleExplicitCopy() error = %v, want nil", err) | ||
| 242 | } | 262 | } | ||
| 243 | 263 | ||||
| @@ -259,10 +279,10 @@ func TestClipboardTextOrchestrator_HandleExplicitCopy_DoesNotDedupAcrossEngines( | |||||
| 259 | uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, nil) | 279 | uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, nil) | ||
| 260 | uc.now = func() time.Time { return time.Unix(0, 0) } | 280 | uc.now = func() time.Time { return time.Unix(0, 0) } | ||
| 261 | 281 | ||||
| 262 | - | if err := uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: "shared", SourceEngine: port.ClipboardSourceWebKit, Action: "copy"}); err != nil { | 282 | + | if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceWebKit, Action: "copy"}); err != nil { |
| 263 | t.Fatalf("HandleExplicitCopy(webkit) error = %v, want nil", err) | 283 | t.Fatalf("HandleExplicitCopy(webkit) error = %v, want nil", err) | ||
| 264 | } | 284 | } | ||
| 265 | - | if err := uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: "shared", SourceEngine: port.ClipboardSourceCEF, Action: "copy"}); err != nil { | 285 | + | if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, Action: "copy"}); err != nil { |
| 266 | t.Fatalf("HandleExplicitCopy(cef) error = %v, want nil", err) | 286 | t.Fatalf("HandleExplicitCopy(cef) error = %v, want nil", err) | ||
| 267 | } | 287 | } | ||
| 268 | } | 288 | } | ||
| @@ -277,10 +297,10 @@ func TestClipboardTextOrchestrator_HandleExplicitCopy_DoesNotDedupAcrossViews(t | |||||
| 277 | uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, nil) | 297 | uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, nil) | ||
| 278 | uc.now = func() time.Time { return time.Unix(0, 0) } | 298 | uc.now = func() time.Time { return time.Unix(0, 0) } | ||
| 279 | 299 | ||||
| 280 | - | if err := uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: "shared", SourceEngine: port.ClipboardSourceCEF, ViewID: 1, Action: "copy"}); err != nil { | 300 | + | if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1, Action: "copy"}); err != nil { |
| 281 | t.Fatalf("HandleExplicitCopy(view 1) error = %v, want nil", err) | 301 | t.Fatalf("HandleExplicitCopy(view 1) error = %v, want nil", err) | ||
| 282 | } | 302 | } | ||
| 283 | - | if err := uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: "shared", SourceEngine: port.ClipboardSourceCEF, ViewID: 2, Action: "copy"}); err != nil { | 303 | + | if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, ViewID: 2, Action: "copy"}); err != nil { |
| 284 | t.Fatalf("HandleExplicitCopy(view 2) error = %v, want nil", err) | 304 | t.Fatalf("HandleExplicitCopy(view 2) error = %v, want nil", err) | ||
| 285 | } | 305 | } | ||
| 286 | } | 306 | } | ||
| @@ -299,7 +319,7 @@ func TestClipboardTextOrchestrator_HandleExplicitCopy_KeepsBoundedStatePerScope( | |||||
| 299 | for i, text := range []string{"one", "two", "three"} { | 319 | for i, text := range []string{"one", "two", "three"} { | ||
| 300 | advancedTime := time.Unix(0, 0).Add(time.Duration(i) * time.Second) | 320 | advancedTime := time.Unix(0, 0).Add(time.Duration(i) * time.Second) | ||
| 301 | uc.now = func() time.Time { return advancedTime } | 321 | uc.now = func() time.Time { return advancedTime } | ||
| 302 | - | if err := uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: text, SourceEngine: port.ClipboardSourceCEF, ViewID: 7, Action: "copy"}); err != nil { | 322 | + | if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: text, SourceEngine: dto.ClipboardSourceCEF, ViewID: 7, Action: "copy"}); err != nil { |
| 303 | t.Fatalf("HandleExplicitCopy(%q) error = %v, want nil", text, err) | 323 | t.Fatalf("HandleExplicitCopy(%q) error = %v, want nil", text, err) | ||
| 304 | } | 324 | } | ||
| 305 | } | 325 | } | ||
| @@ -318,15 +338,15 @@ func TestClipboardTextOrchestrator_HandleSelectionUpdate_ReenableClearsDedupStat | |||||
| 318 | 338 | ||||
| 319 | uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, nil) | 339 | uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, nil) | ||
| 320 | 340 | ||||
| 321 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "toggle", SourceEngine: port.ClipboardSourceCEF, ViewID: 1}); err != nil { | 341 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "toggle", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1}); err != nil { |
| 322 | t.Fatalf("HandleSelectionUpdate(enabled) error = %v, want nil", err) | 342 | t.Fatalf("HandleSelectionUpdate(enabled) error = %v, want nil", err) | ||
| 323 | } | 343 | } | ||
| 324 | autoCopyConfig.enabled = false | 344 | autoCopyConfig.enabled = false | ||
| 325 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "toggle", SourceEngine: port.ClipboardSourceCEF, ViewID: 1}); err != nil { | 345 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "toggle", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1}); err != nil { |
| 326 | t.Fatalf("HandleSelectionUpdate(disabled) error = %v, want nil", err) | 346 | t.Fatalf("HandleSelectionUpdate(disabled) error = %v, want nil", err) | ||
| 327 | } | 347 | } | ||
| 328 | autoCopyConfig.enabled = true | 348 | autoCopyConfig.enabled = true | ||
| 329 | - | if err := uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "toggle", SourceEngine: port.ClipboardSourceCEF, ViewID: 1}); err != nil { | 349 | + | if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "toggle", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1}); err != nil { |
| 330 | t.Fatalf("HandleSelectionUpdate(reenabled) error = %v, want nil", err) | 350 | t.Fatalf("HandleSelectionUpdate(reenabled) error = %v, want nil", err) | ||
| 331 | } | 351 | } | ||
| 332 | } | 352 | } | ||
| @@ -342,7 +362,7 @@ func TestClipboardTextOrchestrator_HandleExplicitCopy_ClipboardFailureDoesNotToa | |||||
| 342 | toastCalls = append(toastCalls, textLen) | 362 | toastCalls = append(toastCalls, textLen) | ||
| 343 | }) | 363 | }) | ||
| 344 | 364 | ||||
| 345 | - | err := uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: "éé", SourceEngine: port.ClipboardSourceCEF, Action: "copy"}) | 365 | + | err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceCEF, Action: "copy"}) |
| 346 | if err == nil { | 366 | if err == nil { | ||
| 347 | t.Fatal("HandleExplicitCopy() error = nil, want non-nil") | 367 | t.Fatal("HandleExplicitCopy() error = nil, want non-nil") | ||
| 348 | } | 368 | } | ||
| @@ -362,13 +382,13 @@ func TestClipboardTextOrchestrator_HandleSelectionUpdate_DedupIsAtomic(t *testin | |||||
| 362 | 382 | ||||
| 363 | done1 := make(chan error, 1) | 383 | done1 := make(chan error, 1) | ||
| 364 | go func() { | 384 | go func() { | ||
| 365 | - | done1 <- uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "éé", SourceEngine: port.ClipboardSourceCEF}) | 385 | + | done1 <- uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceCEF}) |
| 366 | }() | 386 | }() | ||
| 367 | <-clipboard.started | 387 | <-clipboard.started | ||
| 368 | 388 | ||||
| 369 | done2 := make(chan error, 1) | 389 | done2 := make(chan error, 1) | ||
| 370 | go func() { | 390 | go func() { | ||
| 371 | - | done2 <- uc.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{Text: "éé", SourceEngine: port.ClipboardSourceCEF}) | 391 | + | done2 <- uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceCEF}) |
| 372 | }() | 392 | }() | ||
| 373 | 393 | ||||
| 374 | select { | 394 | select { | ||
| @@ -406,13 +426,13 @@ func TestClipboardTextOrchestrator_HandleExplicitCopy_DedupIsAtomic(t *testing.T | |||||
| 406 | 426 | ||||
| 407 | done1 := make(chan error, 1) | 427 | done1 := make(chan error, 1) | ||
| 408 | go func() { | 428 | go func() { | ||
| 409 | - | done1 <- uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: "éé", SourceEngine: port.ClipboardSourceWebKit, Action: "copy"}) | 429 | + | done1 <- uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceWebKit, Action: "copy"}) |
| 410 | }() | 430 | }() | ||
| 411 | <-clipboard.started | 431 | <-clipboard.started | ||
| 412 | 432 | ||||
| 413 | done2 := make(chan error, 1) | 433 | done2 := make(chan error, 1) | ||
| 414 | go func() { | 434 | go func() { | ||
| 415 | - | done2 <- uc.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{Text: "éé", SourceEngine: port.ClipboardSourceWebKit, Action: "copy"}) | 435 | + | done2 <- uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceWebKit, Action: "copy"}) |
| 416 | }() | 436 | }() | ||
| 417 | 437 | ||||
| 418 | select { | 438 | select { | ||
M internal/application/usecase/execute_context_menu_action.go +1 -4
| @@ -106,10 +106,7 @@ func (uc *ExecuteContextMenuActionUseCase) executeMenuAction( | |||
| 106 | 106 | } | |
| 107 | 107 | return nil | |
| 108 | 108 | case port.MenuActionCopySelection: | |
| 109 | - | if menuContext.SelectionText != "" { | |
| 110 | - | if uc.clipboard == nil { | |
| 111 | - | return fmt.Errorf("copy selection: clipboard not available") | |
| 112 | - | } | |
| 109 | + | if menuContext.SelectionText != "" && uc.clipboard != nil { | |
| 113 | 110 | if err := uc.clipboard.WriteText(ctx, menuContext.SelectionText); err != nil { | |
| 114 | 111 | return fmt.Errorf("copy selection: %w", err) | |
| 115 | 112 | } | |
| @@ -106,10 +106,7 @@ func (uc *ExecuteContextMenuActionUseCase) executeMenuAction( | |||||
| 106 | } | 106 | } | ||
| 107 | return nil | 107 | return nil | ||
| 108 | case port.MenuActionCopySelection: | 108 | case port.MenuActionCopySelection: | ||
| 109 | - | if menuContext.SelectionText != "" { | 109 | + | if menuContext.SelectionText != "" && uc.clipboard != nil { |
| 110 | - | if uc.clipboard == nil { | |||
| 111 | - | return fmt.Errorf("copy selection: clipboard not available") | |||
| 112 | - | } | |||
| 113 | if err := uc.clipboard.WriteText(ctx, menuContext.SelectionText); err != nil { | 110 | if err := uc.clipboard.WriteText(ctx, menuContext.SelectionText); err != nil { | ||
| 114 | return fmt.Errorf("copy selection: %w", err) | 111 | return fmt.Errorf("copy selection: %w", err) | ||
| 115 | } | 112 | } | ||
M internal/application/usecase/execute_context_menu_action_test.go +19 -0
| @@ -238,6 +238,25 @@ func TestExecuteContextMenuActionUseCase_CopySelectionWritesSelectedText(t *test | |||
| 238 | 238 | require.Zero(t, delegator.delegateCalls) | |
| 239 | 239 | } | |
| 240 | 240 | ||
| 241 | + | func TestExecuteContextMenuActionUseCase_CopySelectionFallsBackToDelegatorWhenClipboardMissing(t *testing.T) { | |
| 242 | + | resolver := &fakeImageResolver{} | |
| 243 | + | saver := &fakeResolvedImageSaver{} | |
| 244 | + | delegator := &fakeMenuActionDelegator{} | |
| 245 | + | uc := NewExecuteContextMenuActionUseCase(nil, resolver, saver, delegator) | |
| 246 | + | ||
| 247 | + | err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{ | |
| 248 | + | Action: port.MenuActionCopySelection, | |
| 249 | + | Context: port.MenuContext{ | |
| 250 | + | HasSelection: true, | |
| 251 | + | SelectionText: "selected text", | |
| 252 | + | }, | |
| 253 | + | }) | |
| 254 | + | ||
| 255 | + | require.NoError(t, err) | |
| 256 | + | require.Equal(t, 1, delegator.delegateCalls) | |
| 257 | + | require.Equal(t, port.MenuActionCopySelection, delegator.action) | |
| 258 | + | } | |
| 259 | + | ||
| 241 | 260 | func TestExecuteContextMenuActionUseCase_CopyLinkWritesText(t *testing.T) { | |
| 242 | 261 | clipboard := &fakeClipboard{} | |
| 243 | 262 | resolver := &fakeImageResolver{} | |
| @@ -238,6 +238,25 @@ func TestExecuteContextMenuActionUseCase_CopySelectionWritesSelectedText(t *test | |||||
| 238 | require.Zero(t, delegator.delegateCalls) | 238 | require.Zero(t, delegator.delegateCalls) | ||
| 239 | } | 239 | } | ||
| 240 | 240 | ||||
| 241 | + | func TestExecuteContextMenuActionUseCase_CopySelectionFallsBackToDelegatorWhenClipboardMissing(t *testing.T) { | |||
| 242 | + | resolver := &fakeImageResolver{} | |||
| 243 | + | saver := &fakeResolvedImageSaver{} | |||
| 244 | + | delegator := &fakeMenuActionDelegator{} | |||
| 245 | + | uc := NewExecuteContextMenuActionUseCase(nil, resolver, saver, delegator) | |||
| 246 | + | ||||
| 247 | + | err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{ | |||
| 248 | + | Action: port.MenuActionCopySelection, | |||
| 249 | + | Context: port.MenuContext{ | |||
| 250 | + | HasSelection: true, | |||
| 251 | + | SelectionText: "selected text", | |||
| 252 | + | }, | |||
| 253 | + | }) | |||
| 254 | + | ||||
| 255 | + | require.NoError(t, err) | |||
| 256 | + | require.Equal(t, 1, delegator.delegateCalls) | |||
| 257 | + | require.Equal(t, port.MenuActionCopySelection, delegator.action) | |||
| 258 | + | } | |||
| 259 | + | ||||
| 241 | func TestExecuteContextMenuActionUseCase_CopyLinkWritesText(t *testing.T) { | 260 | func TestExecuteContextMenuActionUseCase_CopyLinkWritesText(t *testing.T) { | ||
| 242 | clipboard := &fakeClipboard{} | 261 | clipboard := &fakeClipboard{} | ||
| 243 | resolver := &fakeImageResolver{} | 262 | resolver := &fakeImageResolver{} | ||
M internal/bootstrap/session_spawner.go +1 -1
| @@ -15,7 +15,7 @@ import ( | |||
| 15 | 15 | func NewSessionSpawner(ctx context.Context, profile runtimeprofile.Profile) port.SessionSpawner { | |
| 16 | 16 | var spawnEnv port.SessionSpawnEnvironment | |
| 17 | 17 | if profile.Engine == config.EngineTypeCEF { | |
| 18 | - | spawnEnv = cef.NewSessionSpawnEnvironment(profile) | |
| 18 | + | spawnEnv = cef.NewSessionSpawnEnvironment(profile.CEFUserDataDir()) | |
| 19 | 19 | } | |
| 20 | 20 | return desktop.NewSessionSpawner(ctx, spawnEnv) | |
| 21 | 21 | } | |
| @@ -15,7 +15,7 @@ import ( | |||||
| 15 | func NewSessionSpawner(ctx context.Context, profile runtimeprofile.Profile) port.SessionSpawner { | 15 | func NewSessionSpawner(ctx context.Context, profile runtimeprofile.Profile) port.SessionSpawner { | ||
| 16 | var spawnEnv port.SessionSpawnEnvironment | 16 | var spawnEnv port.SessionSpawnEnvironment | ||
| 17 | if profile.Engine == config.EngineTypeCEF { | 17 | if profile.Engine == config.EngineTypeCEF { | ||
| 18 | - | spawnEnv = cef.NewSessionSpawnEnvironment(profile) | 18 | + | spawnEnv = cef.NewSessionSpawnEnvironment(profile.CEFUserDataDir()) |
| 19 | } | 19 | } | ||
| 20 | return desktop.NewSessionSpawner(ctx, spawnEnv) | 20 | return desktop.NewSessionSpawner(ctx, spawnEnv) | ||
| 21 | } | 21 | } | ||
M internal/cli/cmd/gen_docs.go +2 -1
| @@ -9,6 +9,7 @@ import ( | |||
| 9 | 9 | "github.com/spf13/cobra" | |
| 10 | 10 | "github.com/spf13/cobra/doc" | |
| 11 | 11 | ||
| 12 | + | "github.com/bnema/dumber/internal/infrastructure/runtimeprofile" | |
| 12 | 13 | xdgadapter "github.com/bnema/dumber/internal/infrastructure/xdg" | |
| 13 | 14 | ) | |
| 14 | 15 | ||
| @@ -57,7 +58,7 @@ func runGenDocs(_ *cobra.Command, _ []string) error { | |||
| 57 | 58 | if outputDir == "" { | |
| 58 | 59 | switch genDocsFormat { | |
| 59 | 60 | case "man": | |
| 60 | - | xdg := xdgadapter.New() | |
| 61 | + | xdg := xdgadapter.New(runtimeprofile.Profile{}) | |
| 61 | 62 | manDir, err := xdg.ManDir() | |
| 62 | 63 | if err != nil { | |
| 63 | 64 | return fmt.Errorf("resolve man directory: %w", err) | |
| @@ -9,6 +9,7 @@ import ( | |||||
| 9 | "github.com/spf13/cobra" | 9 | "github.com/spf13/cobra" | ||
| 10 | "github.com/spf13/cobra/doc" | 10 | "github.com/spf13/cobra/doc" | ||
| 11 | 11 | ||||
| 12 | + | "github.com/bnema/dumber/internal/infrastructure/runtimeprofile" | |||
| 12 | xdgadapter "github.com/bnema/dumber/internal/infrastructure/xdg" | 13 | xdgadapter "github.com/bnema/dumber/internal/infrastructure/xdg" | ||
| 13 | ) | 14 | ) | ||
| 14 | 15 | ||||
| @@ -57,7 +58,7 @@ func runGenDocs(_ *cobra.Command, _ []string) error { | |||||
| 57 | if outputDir == "" { | 58 | if outputDir == "" { | ||
| 58 | switch genDocsFormat { | 59 | switch genDocsFormat { | ||
| 59 | case "man": | 60 | case "man": | ||
| 60 | - | xdg := xdgadapter.New() | 61 | + | xdg := xdgadapter.New(runtimeprofile.Profile{}) |
| 61 | manDir, err := xdg.ManDir() | 62 | manDir, err := xdg.ManDir() | ||
| 62 | if err != nil { | 63 | if err != nil { | ||
| 63 | return fmt.Errorf("resolve man directory: %w", err) | 64 | return fmt.Errorf("resolve man directory: %w", err) | ||
M internal/cli/cmd/purge.go +7 -2
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | "github.com/spf13/cobra" | |
| 8 | 8 | ||
| 9 | 9 | "github.com/bnema/dumber/internal/application/usecase" | |
| 10 | + | "github.com/bnema/dumber/internal/bootstrap" | |
| 10 | 11 | "github.com/bnema/dumber/internal/cli" | |
| 11 | 12 | "github.com/bnema/dumber/internal/cli/model" | |
| 12 | 13 | "github.com/bnema/dumber/internal/cli/styles" | |
| @@ -47,7 +48,11 @@ func runPurge(_ *cobra.Command, _ []string) error { | |||
| 47 | 48 | } | |
| 48 | 49 | ||
| 49 | 50 | fsAdapter := filesystem.New() | |
| 50 | - | xdgAdapter := xdgadapter.New() | |
| 51 | + | profile, err := bootstrap.ResolveRuntimeProfile(app.Config) | |
| 52 | + | if err != nil { | |
| 53 | + | return fmt.Errorf("resolve runtime profile: %w", err) | |
| 54 | + | } | |
| 55 | + | xdgAdapter := xdgadapter.New(profile) | |
| 51 | 56 | desktopAdapter := desktop.New() | |
| 52 | 57 | purgeUC := usecase.NewPurgeDataUseCase(fsAdapter, xdgAdapter, desktopAdapter) | |
| 53 | 58 | ||
| @@ -63,7 +68,7 @@ func runPurge(_ *cobra.Command, _ []string) error { | |||
| 63 | 68 | ||
| 64 | 69 | m := model.NewPurgeModel(app.Ctx(), app.Theme, purgeUC, cfg) | |
| 65 | 70 | p := tea.NewProgram(m, tea.WithAltScreen()) | |
| 66 | - | _, err := p.Run() | |
| 71 | + | _, err = p.Run() | |
| 67 | 72 | return err | |
| 68 | 73 | } | |
| 69 | 74 | ||
| @@ -7,6 +7,7 @@ import ( | |||||
| 7 | "github.com/spf13/cobra" | 7 | "github.com/spf13/cobra" | ||
| 8 | 8 | ||||
| 9 | "github.com/bnema/dumber/internal/application/usecase" | 9 | "github.com/bnema/dumber/internal/application/usecase" | ||
| 10 | + | "github.com/bnema/dumber/internal/bootstrap" | |||
| 10 | "github.com/bnema/dumber/internal/cli" | 11 | "github.com/bnema/dumber/internal/cli" | ||
| 11 | "github.com/bnema/dumber/internal/cli/model" | 12 | "github.com/bnema/dumber/internal/cli/model" | ||
| 12 | "github.com/bnema/dumber/internal/cli/styles" | 13 | "github.com/bnema/dumber/internal/cli/styles" | ||
| @@ -47,7 +48,11 @@ func runPurge(_ *cobra.Command, _ []string) error { | |||||
| 47 | } | 48 | } | ||
| 48 | 49 | ||||
| 49 | fsAdapter := filesystem.New() | 50 | fsAdapter := filesystem.New() | ||
| 50 | - | xdgAdapter := xdgadapter.New() | 51 | + | profile, err := bootstrap.ResolveRuntimeProfile(app.Config) |
| 52 | + | if err != nil { | |||
| 53 | + | return fmt.Errorf("resolve runtime profile: %w", err) | |||
| 54 | + | } | |||
| 55 | + | xdgAdapter := xdgadapter.New(profile) | |||
| 51 | desktopAdapter := desktop.New() | 56 | desktopAdapter := desktop.New() | ||
| 52 | purgeUC := usecase.NewPurgeDataUseCase(fsAdapter, xdgAdapter, desktopAdapter) | 57 | purgeUC := usecase.NewPurgeDataUseCase(fsAdapter, xdgAdapter, desktopAdapter) | ||
| 53 | 58 | ||||
| @@ -63,7 +68,7 @@ func runPurge(_ *cobra.Command, _ []string) error { | |||||
| 63 | 68 | ||||
| 64 | m := model.NewPurgeModel(app.Ctx(), app.Theme, purgeUC, cfg) | 69 | m := model.NewPurgeModel(app.Ctx(), app.Theme, purgeUC, cfg) | ||
| 65 | p := tea.NewProgram(m, tea.WithAltScreen()) | 70 | p := tea.NewProgram(m, tea.WithAltScreen()) | ||
| 66 | - | _, err := p.Run() | 71 | + | _, err = p.Run() |
| 67 | return err | 72 | return err | ||
| 68 | } | 73 | } | ||
| 69 | 74 | ||||
M internal/infrastructure/cef/content_injector_test.go +2 -1
| @@ -18,5 +18,6 @@ func TestClipboardSelectionFetchBridgeJS_PostsTrustedFocusSyncRequests(t *testin | |||
| 18 | 18 | require.Contains(t, clipboardSelectionFetchBridgeJS, "X-Dumber-Bridge-Action") | |
| 19 | 19 | require.Contains(t, clipboardSelectionFetchBridgeJS, "focus-sync") | |
| 20 | 20 | require.Contains(t, clipboardSelectionFetchBridgeJS, "focusin") | |
| 21 | - | require.Contains(t, clipboardSelectionFetchBridgeJS, "if (isEditable(document.activeElement)) sendFocusSync();") | |
| 21 | + | require.Contains(t, clipboardSelectionFetchBridgeJS, "isEditable(document.activeElement)") | |
| 22 | + | require.Contains(t, clipboardSelectionFetchBridgeJS, "sendFocusSync()") | |
| 22 | 23 | } | |
| @@ -18,5 +18,6 @@ func TestClipboardSelectionFetchBridgeJS_PostsTrustedFocusSyncRequests(t *testin | |||||
| 18 | require.Contains(t, clipboardSelectionFetchBridgeJS, "X-Dumber-Bridge-Action") | 18 | require.Contains(t, clipboardSelectionFetchBridgeJS, "X-Dumber-Bridge-Action") | ||
| 19 | require.Contains(t, clipboardSelectionFetchBridgeJS, "focus-sync") | 19 | require.Contains(t, clipboardSelectionFetchBridgeJS, "focus-sync") | ||
| 20 | require.Contains(t, clipboardSelectionFetchBridgeJS, "focusin") | 20 | require.Contains(t, clipboardSelectionFetchBridgeJS, "focusin") | ||
| 21 | - | require.Contains(t, clipboardSelectionFetchBridgeJS, "if (isEditable(document.activeElement)) sendFocusSync();") | 21 | + | require.Contains(t, clipboardSelectionFetchBridgeJS, "isEditable(document.activeElement)") |
| 22 | + | require.Contains(t, clipboardSelectionFetchBridgeJS, "sendFocusSync()") | |||
| 22 | } | 23 | } | ||
M internal/infrastructure/cef/engine.go +7 -6
| @@ -9,6 +9,7 @@ import ( | |||
| 9 | 9 | ||
| 10 | 10 | purecef "github.com/bnema/purego-cef/cef" | |
| 11 | 11 | ||
| 12 | + | "github.com/bnema/dumber/internal/application/dto" | |
| 12 | 13 | "github.com/bnema/dumber/internal/application/port" | |
| 13 | 14 | "github.com/bnema/dumber/internal/logging" | |
| 14 | 15 | ) | |
| @@ -296,11 +297,11 @@ func (e *Engine) handleExplicitClipboardBridgeText(viewID port.WebViewID, action | |||
| 296 | 297 | Msg("cef: clipboard orchestration skipped — orchestrator nil") | |
| 297 | 298 | return | |
| 298 | 299 | } | |
| 299 | - | if err := e.clipboardTextOrchestrator.HandleExplicitCopy(e.currentContext(), port.ExplicitClipboardInput{ | |
| 300 | + | if err := e.clipboardTextOrchestrator.HandleExplicitCopy(e.currentContext(), dto.ExplicitClipboardInput{ | |
| 300 | 301 | Text: text, | |
| 301 | 302 | Action: action, | |
| 302 | - | SourceEngine: port.ClipboardSourceCEF, | |
| 303 | - | ViewID: viewID, | |
| 303 | + | SourceEngine: dto.ClipboardSourceCEF, | |
| 304 | + | ViewID: uint64(viewID), | |
| 304 | 305 | }); err != nil { | |
| 305 | 306 | logging.FromContext(e.currentContext()).Debug(). | |
| 306 | 307 | Err(err). | |
| @@ -314,10 +315,10 @@ func (e *Engine) handleClipboardSelectionUpdate(viewID port.WebViewID, text stri | |||
| 314 | 315 | if e == nil || e.clipboardTextOrchestrator == nil { | |
| 315 | 316 | return | |
| 316 | 317 | } | |
| 317 | - | if err := e.clipboardTextOrchestrator.HandleSelectionUpdate(e.currentContext(), port.SelectionClipboardInput{ | |
| 318 | + | if err := e.clipboardTextOrchestrator.HandleSelectionUpdate(e.currentContext(), dto.SelectionClipboardInput{ | |
| 318 | 319 | Text: text, | |
| 319 | - | SourceEngine: port.ClipboardSourceCEF, | |
| 320 | - | ViewID: viewID, | |
| 320 | + | SourceEngine: dto.ClipboardSourceCEF, | |
| 321 | + | ViewID: uint64(viewID), | |
| 321 | 322 | }); err != nil { | |
| 322 | 323 | logging.FromContext(e.currentContext()).Debug(). | |
| 323 | 324 | Err(err). | |
| @@ -9,6 +9,7 @@ import ( | |||||
| 9 | 9 | ||||
| 10 | purecef "github.com/bnema/purego-cef/cef" | 10 | purecef "github.com/bnema/purego-cef/cef" | ||
| 11 | 11 | ||||
| 12 | + | "github.com/bnema/dumber/internal/application/dto" | |||
| 12 | "github.com/bnema/dumber/internal/application/port" | 13 | "github.com/bnema/dumber/internal/application/port" | ||
| 13 | "github.com/bnema/dumber/internal/logging" | 14 | "github.com/bnema/dumber/internal/logging" | ||
| 14 | ) | 15 | ) | ||
| @@ -296,11 +297,11 @@ func (e *Engine) handleExplicitClipboardBridgeText(viewID port.WebViewID, action | |||||
| 296 | Msg("cef: clipboard orchestration skipped — orchestrator nil") | 297 | Msg("cef: clipboard orchestration skipped — orchestrator nil") | ||
| 297 | return | 298 | return | ||
| 298 | } | 299 | } | ||
| 299 | - | if err := e.clipboardTextOrchestrator.HandleExplicitCopy(e.currentContext(), port.ExplicitClipboardInput{ | 300 | + | if err := e.clipboardTextOrchestrator.HandleExplicitCopy(e.currentContext(), dto.ExplicitClipboardInput{ |
| 300 | Text: text, | 301 | Text: text, | ||
| 301 | Action: action, | 302 | Action: action, | ||
| 302 | - | SourceEngine: port.ClipboardSourceCEF, | 303 | + | SourceEngine: dto.ClipboardSourceCEF, |
| 303 | - | ViewID: viewID, | 304 | + | ViewID: uint64(viewID), |
| 304 | }); err != nil { | 305 | }); err != nil { | ||
| 305 | logging.FromContext(e.currentContext()).Debug(). | 306 | logging.FromContext(e.currentContext()).Debug(). | ||
| 306 | Err(err). | 307 | Err(err). | ||
| @@ -314,10 +315,10 @@ func (e *Engine) handleClipboardSelectionUpdate(viewID port.WebViewID, text stri | |||||
| 314 | if e == nil || e.clipboardTextOrchestrator == nil { | 315 | if e == nil || e.clipboardTextOrchestrator == nil { | ||
| 315 | return | 316 | return | ||
| 316 | } | 317 | } | ||
| 317 | - | if err := e.clipboardTextOrchestrator.HandleSelectionUpdate(e.currentContext(), port.SelectionClipboardInput{ | 318 | + | if err := e.clipboardTextOrchestrator.HandleSelectionUpdate(e.currentContext(), dto.SelectionClipboardInput{ |
| 318 | Text: text, | 319 | Text: text, | ||
| 319 | - | SourceEngine: port.ClipboardSourceCEF, | 320 | + | SourceEngine: dto.ClipboardSourceCEF, |
| 320 | - | ViewID: viewID, | 321 | + | ViewID: uint64(viewID), |
| 321 | }); err != nil { | 322 | }); err != nil { | ||
| 322 | logging.FromContext(e.currentContext()).Debug(). | 323 | logging.FromContext(e.currentContext()).Debug(). | ||
| 323 | Err(err). | 324 | Err(err). | ||
M internal/infrastructure/cef/engine_init.go +3 -0
| @@ -95,6 +95,9 @@ func resolvedStateRoot(profile runtimeprofile.Profile, opts port.EngineOptions) | |||
| 95 | 95 | if root := os.Getenv(CEFRootCachePathEnvVar); root != "" { | |
| 96 | 96 | return root | |
| 97 | 97 | } | |
| 98 | + | if opts.CacheDir != "" { | |
| 99 | + | return opts.CacheDir | |
| 100 | + | } | |
| 98 | 101 | if opts.DataDir != "" { | |
| 99 | 102 | return opts.DataDir | |
| 100 | 103 | } | |
| @@ -95,6 +95,9 @@ func resolvedStateRoot(profile runtimeprofile.Profile, opts port.EngineOptions) | |||||
| 95 | if root := os.Getenv(CEFRootCachePathEnvVar); root != "" { | 95 | if root := os.Getenv(CEFRootCachePathEnvVar); root != "" { | ||
| 96 | return root | 96 | return root | ||
| 97 | } | 97 | } | ||
| 98 | + | if opts.CacheDir != "" { | |||
| 99 | + | return opts.CacheDir | |||
| 100 | + | } | |||
| 98 | if opts.DataDir != "" { | 101 | if opts.DataDir != "" { | ||
| 99 | return opts.DataDir | 102 | return opts.DataDir | ||
| 100 | } | 103 | } | ||
M internal/infrastructure/cef/engine_init_test.go +2 -2
| @@ -12,10 +12,10 @@ import ( | |||
| 12 | 12 | "github.com/stretchr/testify/require" | |
| 13 | 13 | ) | |
| 14 | 14 | ||
| 15 | - | func TestResolvedStateRoot_PrefersDataDir(t *testing.T) { | |
| 15 | + | func TestResolvedStateRoot_PrefersCacheDir(t *testing.T) { | |
| 16 | 16 | profile := testCEFDevProfile(t) | |
| 17 | 17 | root := resolvedStateRoot(profile, port.EngineOptions{DataDir: "/tmp/data", CacheDir: "/tmp/cache"}) | |
| 18 | - | require.Equal(t, "/tmp/data", root) | |
| 18 | + | require.Equal(t, "/tmp/cache", root) | |
| 19 | 19 | } | |
| 20 | 20 | ||
| 21 | 21 | func TestResolvedStateRoot_UsesEnvOverride(t *testing.T) { | |
| @@ -12,10 +12,10 @@ import ( | |||||
| 12 | "github.com/stretchr/testify/require" | 12 | "github.com/stretchr/testify/require" | ||
| 13 | ) | 13 | ) | ||
| 14 | 14 | ||||
| 15 | - | func TestResolvedStateRoot_PrefersDataDir(t *testing.T) { | 15 | + | func TestResolvedStateRoot_PrefersCacheDir(t *testing.T) { |
| 16 | profile := testCEFDevProfile(t) | 16 | profile := testCEFDevProfile(t) | ||
| 17 | root := resolvedStateRoot(profile, port.EngineOptions{DataDir: "/tmp/data", CacheDir: "/tmp/cache"}) | 17 | root := resolvedStateRoot(profile, port.EngineOptions{DataDir: "/tmp/data", CacheDir: "/tmp/cache"}) | ||
| 18 | - | require.Equal(t, "/tmp/data", root) | 18 | + | require.Equal(t, "/tmp/cache", root) |
| 19 | } | 19 | } | ||
| 20 | 20 | ||||
| 21 | func TestResolvedStateRoot_UsesEnvOverride(t *testing.T) { | 21 | func TestResolvedStateRoot_UsesEnvOverride(t *testing.T) { | ||
M internal/infrastructure/cef/engine_runtime_test.go +9 -8
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "errors" | |
| 6 | 6 | "testing" | |
| 7 | 7 | ||
| 8 | + | "github.com/bnema/dumber/internal/application/dto" | |
| 8 | 9 | "github.com/bnema/dumber/internal/application/port" | |
| 9 | 10 | "github.com/stretchr/testify/require" | |
| 10 | 11 | ) | |
| @@ -76,7 +77,7 @@ func TestEngineClipboardSelectionUpdate_ForwardsViewID(t *testing.T) { | |||
| 76 | 77 | ||
| 77 | 78 | eng.handleClipboardSelectionUpdate(99, "selected") | |
| 78 | 79 | ||
| 79 | - | require.Equal(t, port.SelectionClipboardInput{Text: "selected", SourceEngine: port.ClipboardSourceCEF, ViewID: 99}, orchestrator.selection) | |
| 80 | + | require.Equal(t, dto.SelectionClipboardInput{Text: "selected", SourceEngine: dto.ClipboardSourceCEF, ViewID: 99}, orchestrator.selection) | |
| 80 | 81 | } | |
| 81 | 82 | ||
| 82 | 83 | func TestEngineExplicitClipboardCopy_ForwardsViewID(t *testing.T) { | |
| @@ -85,30 +86,30 @@ func TestEngineExplicitClipboardCopy_ForwardsViewID(t *testing.T) { | |||
| 85 | 86 | ||
| 86 | 87 | eng.handleExplicitClipboardBridgeText(77, "copy", "copied") | |
| 87 | 88 | ||
| 88 | - | require.Equal(t, port.ExplicitClipboardInput{Text: "copied", SourceEngine: port.ClipboardSourceCEF, ViewID: 77, Action: "copy", NativeHandled: false}, orchestrator.explicit) | |
| 89 | + | require.Equal(t, dto.ExplicitClipboardInput{Text: "copied", SourceEngine: dto.ClipboardSourceCEF, ViewID: 77, Action: "copy", NativeHandled: false}, orchestrator.explicit) | |
| 89 | 90 | } | |
| 90 | 91 | ||
| 91 | 92 | type stubClipboardTextOrchestrator struct{} | |
| 92 | 93 | ||
| 93 | 94 | type recordingClipboardTextOrchestrator struct { | |
| 94 | - | selection port.SelectionClipboardInput | |
| 95 | - | explicit port.ExplicitClipboardInput | |
| 95 | + | selection dto.SelectionClipboardInput | |
| 96 | + | explicit dto.ExplicitClipboardInput | |
| 96 | 97 | } | |
| 97 | 98 | ||
| 98 | - | func (stubClipboardTextOrchestrator) HandleSelectionUpdate(context.Context, port.SelectionClipboardInput) error { | |
| 99 | + | func (stubClipboardTextOrchestrator) HandleSelectionUpdate(context.Context, dto.SelectionClipboardInput) error { | |
| 99 | 100 | return nil | |
| 100 | 101 | } | |
| 101 | 102 | ||
| 102 | - | func (stubClipboardTextOrchestrator) HandleExplicitCopy(context.Context, port.ExplicitClipboardInput) error { | |
| 103 | + | func (stubClipboardTextOrchestrator) HandleExplicitCopy(context.Context, dto.ExplicitClipboardInput) error { | |
| 103 | 104 | return nil | |
| 104 | 105 | } | |
| 105 | 106 | ||
| 106 | - | func (r *recordingClipboardTextOrchestrator) HandleSelectionUpdate(_ context.Context, input port.SelectionClipboardInput) error { | |
| 107 | + | func (r *recordingClipboardTextOrchestrator) HandleSelectionUpdate(_ context.Context, input dto.SelectionClipboardInput) error { | |
| 107 | 108 | r.selection = input | |
| 108 | 109 | return nil | |
| 109 | 110 | } | |
| 110 | 111 | ||
| 111 | - | func (r *recordingClipboardTextOrchestrator) HandleExplicitCopy(_ context.Context, input port.ExplicitClipboardInput) error { | |
| 112 | + | func (r *recordingClipboardTextOrchestrator) HandleExplicitCopy(_ context.Context, input dto.ExplicitClipboardInput) error { | |
| 112 | 113 | r.explicit = input | |
| 113 | 114 | return nil | |
| 114 | 115 | } | |
| @@ -5,6 +5,7 @@ import ( | |||||
| 5 | "errors" | 5 | "errors" | ||
| 6 | "testing" | 6 | "testing" | ||
| 7 | 7 | ||||
| 8 | + | "github.com/bnema/dumber/internal/application/dto" | |||
| 8 | "github.com/bnema/dumber/internal/application/port" | 9 | "github.com/bnema/dumber/internal/application/port" | ||
| 9 | "github.com/stretchr/testify/require" | 10 | "github.com/stretchr/testify/require" | ||
| 10 | ) | 11 | ) | ||
| @@ -76,7 +77,7 @@ func TestEngineClipboardSelectionUpdate_ForwardsViewID(t *testing.T) { | |||||
| 76 | 77 | ||||
| 77 | eng.handleClipboardSelectionUpdate(99, "selected") | 78 | eng.handleClipboardSelectionUpdate(99, "selected") | ||
| 78 | 79 | ||||
| 79 | - | require.Equal(t, port.SelectionClipboardInput{Text: "selected", SourceEngine: port.ClipboardSourceCEF, ViewID: 99}, orchestrator.selection) | 80 | + | require.Equal(t, dto.SelectionClipboardInput{Text: "selected", SourceEngine: dto.ClipboardSourceCEF, ViewID: 99}, orchestrator.selection) |
| 80 | } | 81 | } | ||
| 81 | 82 | ||||
| 82 | func TestEngineExplicitClipboardCopy_ForwardsViewID(t *testing.T) { | 83 | func TestEngineExplicitClipboardCopy_ForwardsViewID(t *testing.T) { | ||
| @@ -85,30 +86,30 @@ func TestEngineExplicitClipboardCopy_ForwardsViewID(t *testing.T) { | |||||
| 85 | 86 | ||||
| 86 | eng.handleExplicitClipboardBridgeText(77, "copy", "copied") | 87 | eng.handleExplicitClipboardBridgeText(77, "copy", "copied") | ||
| 87 | 88 | ||||
| 88 | - | require.Equal(t, port.ExplicitClipboardInput{Text: "copied", SourceEngine: port.ClipboardSourceCEF, ViewID: 77, Action: "copy", NativeHandled: false}, orchestrator.explicit) | 89 | + | require.Equal(t, dto.ExplicitClipboardInput{Text: "copied", SourceEngine: dto.ClipboardSourceCEF, ViewID: 77, Action: "copy", NativeHandled: false}, orchestrator.explicit) |
| 89 | } | 90 | } | ||
| 90 | 91 | ||||
| 91 | type stubClipboardTextOrchestrator struct{} | 92 | type stubClipboardTextOrchestrator struct{} | ||
| 92 | 93 | ||||
| 93 | type recordingClipboardTextOrchestrator struct { | 94 | type recordingClipboardTextOrchestrator struct { | ||
| 94 | - | selection port.SelectionClipboardInput | 95 | + | selection dto.SelectionClipboardInput |
| 95 | - | explicit port.ExplicitClipboardInput | 96 | + | explicit dto.ExplicitClipboardInput |
| 96 | } | 97 | } | ||
| 97 | 98 | ||||
| 98 | - | func (stubClipboardTextOrchestrator) HandleSelectionUpdate(context.Context, port.SelectionClipboardInput) error { | 99 | + | func (stubClipboardTextOrchestrator) HandleSelectionUpdate(context.Context, dto.SelectionClipboardInput) error { |
| 99 | return nil | 100 | return nil | ||
| 100 | } | 101 | } | ||
| 101 | 102 | ||||
| 102 | - | func (stubClipboardTextOrchestrator) HandleExplicitCopy(context.Context, port.ExplicitClipboardInput) error { | 103 | + | func (stubClipboardTextOrchestrator) HandleExplicitCopy(context.Context, dto.ExplicitClipboardInput) error { |
| 103 | return nil | 104 | return nil | ||
| 104 | } | 105 | } | ||
| 105 | 106 | ||||
| 106 | - | func (r *recordingClipboardTextOrchestrator) HandleSelectionUpdate(_ context.Context, input port.SelectionClipboardInput) error { | 107 | + | func (r *recordingClipboardTextOrchestrator) HandleSelectionUpdate(_ context.Context, input dto.SelectionClipboardInput) error { |
| 107 | r.selection = input | 108 | r.selection = input | ||
| 108 | return nil | 109 | return nil | ||
| 109 | } | 110 | } | ||
| 110 | 111 | ||||
| 111 | - | func (r *recordingClipboardTextOrchestrator) HandleExplicitCopy(_ context.Context, input port.ExplicitClipboardInput) error { | 112 | + | func (r *recordingClipboardTextOrchestrator) HandleExplicitCopy(_ context.Context, input dto.ExplicitClipboardInput) error { |
| 112 | r.explicit = input | 113 | r.explicit = input | ||
| 113 | return nil | 114 | return nil | ||
| 114 | } | 115 | } | ||
M internal/infrastructure/cef/renderer_bridge.go +1 -1
| @@ -80,7 +80,7 @@ const rendererBridgeExtensionJS = ` | |||
| 80 | 80 | } | |
| 81 | 81 | setTimeout(function() { | |
| 82 | 82 | var text = capturedText; | |
| 83 | - | if (!text) text = getSelectedText(); | |
| 83 | + | if (!text && !e.defaultPrevented) text = getSelectedText(); | |
| 84 | 84 | if (text) notifyExplicitTextCopy(action, text); | |
| 85 | 85 | }, 0); | |
| 86 | 86 | } | |
| @@ -80,7 +80,7 @@ const rendererBridgeExtensionJS = ` | |||||
| 80 | } | 80 | } | ||
| 81 | setTimeout(function() { | 81 | setTimeout(function() { | ||
| 82 | var text = capturedText; | 82 | var text = capturedText; | ||
| 83 | - | if (!text) text = getSelectedText(); | 83 | + | if (!text && !e.defaultPrevented) text = getSelectedText(); |
| 84 | if (text) notifyExplicitTextCopy(action, text); | 84 | if (text) notifyExplicitTextCopy(action, text); | ||
| 85 | }, 0); | 85 | }, 0); | ||
| 86 | } | 86 | } | ||
M internal/infrastructure/cef/renderer_bridge_test.go +1 -0
| @@ -100,6 +100,7 @@ func TestRendererBridgeExtensionJS_UsesNativeDispatchInExtensionScope(t *testing | |||
| 100 | 100 | func TestRendererBridgeExtensionJS_EncodesTrustedSuccessSemantics(t *testing.T) { | |
| 101 | 101 | require.Contains(t, rendererBridgeExtensionJS, "if (!e.isTrusted) return;") | |
| 102 | 102 | require.Contains(t, rendererBridgeExtensionJS, "setTimeout(function() {") | |
| 103 | + | require.Contains(t, rendererBridgeExtensionJS, "if (!text && !e.defaultPrevented) text = getSelectedText();") | |
| 103 | 104 | require.NotContains(t, rendererBridgeExtensionJS, "document.execCommand = function") | |
| 104 | 105 | require.Contains(t, rendererBridgeExtensionJS, "return result;") | |
| 105 | 106 | } | |
| @@ -100,6 +100,7 @@ func TestRendererBridgeExtensionJS_UsesNativeDispatchInExtensionScope(t *testing | |||||
| 100 | func TestRendererBridgeExtensionJS_EncodesTrustedSuccessSemantics(t *testing.T) { | 100 | func TestRendererBridgeExtensionJS_EncodesTrustedSuccessSemantics(t *testing.T) { | ||
| 101 | require.Contains(t, rendererBridgeExtensionJS, "if (!e.isTrusted) return;") | 101 | require.Contains(t, rendererBridgeExtensionJS, "if (!e.isTrusted) return;") | ||
| 102 | require.Contains(t, rendererBridgeExtensionJS, "setTimeout(function() {") | 102 | require.Contains(t, rendererBridgeExtensionJS, "setTimeout(function() {") | ||
| 103 | + | require.Contains(t, rendererBridgeExtensionJS, "if (!text && !e.defaultPrevented) text = getSelectedText();") | |||
| 103 | require.NotContains(t, rendererBridgeExtensionJS, "document.execCommand = function") | 104 | require.NotContains(t, rendererBridgeExtensionJS, "document.execCommand = function") | ||
| 104 | require.Contains(t, rendererBridgeExtensionJS, "return result;") | 105 | require.Contains(t, rendererBridgeExtensionJS, "return result;") | ||
| 105 | } | 106 | } | ||
M internal/infrastructure/cef/session_spawn_environment.go +4 -5
| @@ -5,17 +5,16 @@ import ( | |||
| 5 | 5 | ||
| 6 | 6 | "github.com/bnema/dumber/internal/application/port" | |
| 7 | 7 | "github.com/bnema/dumber/internal/domain/entity" | |
| 8 | - | "github.com/bnema/dumber/internal/infrastructure/runtimeprofile" | |
| 9 | 8 | ) | |
| 10 | 9 | ||
| 11 | 10 | // SessionSpawnEnvironment exposes the engine-specific launch environment needed | |
| 12 | 11 | // when restoring a session into a freshly spawned browser process. | |
| 13 | 12 | type SessionSpawnEnvironment struct { | |
| 14 | - | profile runtimeprofile.Profile | |
| 13 | + | cefUserDataDir string | |
| 15 | 14 | } | |
| 16 | 15 | ||
| 17 | - | func NewSessionSpawnEnvironment(profile runtimeprofile.Profile) SessionSpawnEnvironment { | |
| 18 | - | return SessionSpawnEnvironment{profile: profile} | |
| 16 | + | func NewSessionSpawnEnvironment(cefUserDataDir string) SessionSpawnEnvironment { | |
| 17 | + | return SessionSpawnEnvironment{cefUserDataDir: cefUserDataDir} | |
| 19 | 18 | } | |
| 20 | 19 | ||
| 21 | 20 | func (SessionSpawnEnvironment) RootCacheEnvVar() string { | |
| @@ -23,7 +22,7 @@ func (SessionSpawnEnvironment) RootCacheEnvVar() string { | |||
| 23 | 22 | } | |
| 24 | 23 | ||
| 25 | 24 | func (e SessionSpawnEnvironment) SessionRootCachePath(sessionID entity.SessionID) string { | |
| 26 | - | return filepath.Join(e.profile.CEFUserDataDir(), "sessions", string(sessionID)) | |
| 25 | + | return filepath.Join(e.cefUserDataDir, "sessions", string(sessionID)) | |
| 27 | 26 | } | |
| 28 | 27 | ||
| 29 | 28 | var _ port.SessionSpawnEnvironment = SessionSpawnEnvironment{} | |
| @@ -5,17 +5,16 @@ import ( | |||||
| 5 | 5 | ||||
| 6 | "github.com/bnema/dumber/internal/application/port" | 6 | "github.com/bnema/dumber/internal/application/port" | ||
| 7 | "github.com/bnema/dumber/internal/domain/entity" | 7 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 8 | - | "github.com/bnema/dumber/internal/infrastructure/runtimeprofile" | |||
| 9 | ) | 8 | ) | ||
| 10 | 9 | ||||
| 11 | // SessionSpawnEnvironment exposes the engine-specific launch environment needed | 10 | // SessionSpawnEnvironment exposes the engine-specific launch environment needed | ||
| 12 | // when restoring a session into a freshly spawned browser process. | 11 | // when restoring a session into a freshly spawned browser process. | ||
| 13 | type SessionSpawnEnvironment struct { | 12 | type SessionSpawnEnvironment struct { | ||
| 14 | - | profile runtimeprofile.Profile | 13 | + | cefUserDataDir string |
| 15 | } | 14 | } | ||
| 16 | 15 | ||||
| 17 | - | func NewSessionSpawnEnvironment(profile runtimeprofile.Profile) SessionSpawnEnvironment { | 16 | + | func NewSessionSpawnEnvironment(cefUserDataDir string) SessionSpawnEnvironment { |
| 18 | - | return SessionSpawnEnvironment{profile: profile} | 17 | + | return SessionSpawnEnvironment{cefUserDataDir: cefUserDataDir} |
| 19 | } | 18 | } | ||
| 20 | 19 | ||||
| 21 | func (SessionSpawnEnvironment) RootCacheEnvVar() string { | 20 | func (SessionSpawnEnvironment) RootCacheEnvVar() string { | ||
| @@ -23,7 +22,7 @@ func (SessionSpawnEnvironment) RootCacheEnvVar() string { | |||||
| 23 | } | 22 | } | ||
| 24 | 23 | ||||
| 25 | func (e SessionSpawnEnvironment) SessionRootCachePath(sessionID entity.SessionID) string { | 24 | func (e SessionSpawnEnvironment) SessionRootCachePath(sessionID entity.SessionID) string { | ||
| 26 | - | return filepath.Join(e.profile.CEFUserDataDir(), "sessions", string(sessionID)) | 25 | + | return filepath.Join(e.cefUserDataDir, "sessions", string(sessionID)) |
| 27 | } | 26 | } | ||
| 28 | 27 | ||||
| 29 | var _ port.SessionSpawnEnvironment = SessionSpawnEnvironment{} | 28 | var _ port.SessionSpawnEnvironment = SessionSpawnEnvironment{} | ||
M internal/infrastructure/cef/webview.go +53 -22
| @@ -34,10 +34,14 @@ var errNoBrowser = errors.New("cef: browser not yet created") | |||
| 34 | 34 | ||
| 35 | 35 | const ( | |
| 36 | 36 | clipboardSelectionDebounceDelay = 300 * time.Millisecond | |
| 37 | - | pendingNavigationRetryDelay = 25 * time.Millisecond | |
| 38 | - | pendingNavigationMaxRetries = 20 | |
| 37 | + | pendingNavigationRetryDelay = 50 * time.Millisecond | |
| 38 | + | pendingNavigationMaxRetries = 80 | |
| 39 | 39 | ) | |
| 40 | 40 | ||
| 41 | + | type stoppableTimer interface { | |
| 42 | + | Stop() bool | |
| 43 | + | } | |
| 44 | + | ||
| 41 | 45 | var cefLoadWatchdogDelays = []time.Duration{ | |
| 42 | 46 | 250 * time.Millisecond, | |
| 43 | 47 | 1 * time.Second, | |
| @@ -89,16 +93,18 @@ type WebView struct { | |||
| 89 | 93 | callbacks *port.WebViewCallbacks | |
| 90 | 94 | ||
| 91 | 95 | // State cache (mutex-protected). | |
| 92 | - | uri string | |
| 93 | - | title string | |
| 94 | - | progress float64 | |
| 95 | - | canGoBack bool | |
| 96 | - | canGoFwd bool | |
| 97 | - | isLoading bool | |
| 98 | - | selectedText string | |
| 99 | - | focusedEditable bool | |
| 100 | - | selectionDebounceTimer *time.Timer | |
| 101 | - | selectionDebounceSeq uint64 | |
| 96 | + | uri string | |
| 97 | + | title string | |
| 98 | + | progress float64 | |
| 99 | + | canGoBack bool | |
| 100 | + | canGoFwd bool | |
| 101 | + | isLoading bool | |
| 102 | + | selectedText string | |
| 103 | + | focusedEditable bool | |
| 104 | + | selectionDebounceTimer stoppableTimer | |
| 105 | + | selectionDebounceSeq uint64 | |
| 106 | + | selectionDebounceDelay *time.Duration | |
| 107 | + | selectionDebounceSchedule func(time.Duration, func()) stoppableTimer | |
| 102 | 108 | ||
| 103 | 109 | // Last known hover URI for middle-click → new tab. | |
| 104 | 110 | lastHoverURI string | |
| @@ -169,7 +175,7 @@ func (wv *WebView) LoadURI(_ context.Context, uri string) error { | |||
| 169 | 175 | if browser == nil { | |
| 170 | 176 | return nil | |
| 171 | 177 | } | |
| 172 | - | wv.schedulePendingNavigationReplay(browser, 0) | |
| 178 | + | wv.schedulePendingNavigationReplay(0) | |
| 173 | 179 | return nil | |
| 174 | 180 | } | |
| 175 | 181 | ||
| @@ -677,6 +683,22 @@ func (wv *WebView) cancelSelectionDebounce() { | |||
| 677 | 683 | } | |
| 678 | 684 | } | |
| 679 | 685 | ||
| 686 | + | func (wv *WebView) selectionDebounceInterval() time.Duration { | |
| 687 | + | if wv == nil || wv.selectionDebounceDelay == nil { | |
| 688 | + | return clipboardSelectionDebounceDelay | |
| 689 | + | } | |
| 690 | + | return *wv.selectionDebounceDelay | |
| 691 | + | } | |
| 692 | + | ||
| 693 | + | func (wv *WebView) selectionDebounceScheduler() func(time.Duration, func()) stoppableTimer { | |
| 694 | + | if wv != nil && wv.selectionDebounceSchedule != nil { | |
| 695 | + | return wv.selectionDebounceSchedule | |
| 696 | + | } | |
| 697 | + | return func(delay time.Duration, fn func()) stoppableTimer { | |
| 698 | + | return time.AfterFunc(delay, fn) | |
| 699 | + | } | |
| 700 | + | } | |
| 701 | + | ||
| 680 | 702 | func (wv *WebView) scheduleSelectionUpdate(text string) { | |
| 681 | 703 | if wv == nil || wv.destroyed.Load() { | |
| 682 | 704 | return | |
| @@ -700,7 +722,15 @@ func (wv *WebView) scheduleSelectionUpdate(text string) { | |||
| 700 | 722 | if timer := wv.selectionDebounceTimer; timer != nil { | |
| 701 | 723 | timer.Stop() | |
| 702 | 724 | } | |
| 703 | - | wv.selectionDebounceTimer = time.AfterFunc(clipboardSelectionDebounceDelay, func() { | |
| 725 | + | delay := wv.selectionDebounceInterval() | |
| 726 | + | if delay <= 0 { | |
| 727 | + | wv.selectionDebounceTimer = nil | |
| 728 | + | wv.mu.Unlock() | |
| 729 | + | wv.flushSelectionUpdate(seq, text) | |
| 730 | + | return | |
| 731 | + | } | |
| 732 | + | scheduler := wv.selectionDebounceScheduler() | |
| 733 | + | wv.selectionDebounceTimer = scheduler(delay, func() { | |
| 704 | 734 | wv.flushSelectionUpdate(seq, text) | |
| 705 | 735 | }) | |
| 706 | 736 | wv.mu.Unlock() | |
| @@ -812,12 +842,12 @@ func (wv *WebView) pendingNavigationURI() string { | |||
| 812 | 842 | return wv.pendingURI | |
| 813 | 843 | } | |
| 814 | 844 | ||
| 815 | - | func (wv *WebView) schedulePendingNavigationReplay(browser purecef.Browser, attempt int) { | |
| 816 | - | if wv == nil || browser == nil || wv.destroyed.Load() { | |
| 845 | + | func (wv *WebView) schedulePendingNavigationReplay(attempt int) { | |
| 846 | + | if wv == nil || wv.destroyed.Load() { | |
| 817 | 847 | return | |
| 818 | 848 | } | |
| 819 | 849 | task := cefNewTask(cefTaskFunc(func() { | |
| 820 | - | wv.replayPendingNavigation(browser, attempt) | |
| 850 | + | wv.replayPendingNavigation(attempt) | |
| 821 | 851 | })) | |
| 822 | 852 | if task == nil { | |
| 823 | 853 | return | |
| @@ -845,18 +875,19 @@ func (wv *WebView) schedulePendingNavigationReplay(browser purecef.Browser, atte | |||
| 845 | 875 | return | |
| 846 | 876 | } | |
| 847 | 877 | cefScheduleAfter(pendingNavigationRetryDelay, func() { | |
| 848 | - | wv.schedulePendingNavigationReplay(browser, attempt+1) | |
| 878 | + | wv.schedulePendingNavigationReplay(attempt + 1) | |
| 849 | 879 | }) | |
| 850 | 880 | } | |
| 851 | 881 | ||
| 852 | - | func (wv *WebView) replayPendingNavigation(browser purecef.Browser, attempt int) { | |
| 853 | - | if wv == nil || browser == nil || wv.destroyed.Load() { | |
| 882 | + | func (wv *WebView) replayPendingNavigation(attempt int) { | |
| 883 | + | if wv == nil || wv.destroyed.Load() { | |
| 854 | 884 | return | |
| 855 | 885 | } | |
| 856 | 886 | wv.mu.RLock() | |
| 857 | 887 | uri := wv.pendingURI | |
| 888 | + | browser := wv.browser | |
| 858 | 889 | wv.mu.RUnlock() | |
| 859 | - | if uri == "" { | |
| 890 | + | if uri == "" || browser == nil { | |
| 860 | 891 | return | |
| 861 | 892 | } | |
| 862 | 893 | frame := browser.GetMainFrame() | |
| @@ -876,7 +907,7 @@ func (wv *WebView) replayPendingNavigation(browser purecef.Browser, attempt int) | |||
| 876 | 907 | Str("uri", logging.TruncateURL(uri, logging.PermissionLogURLMaxLen)). | |
| 877 | 908 | Msg("cef: pending navigation replay waiting for main frame") | |
| 878 | 909 | } | |
| 879 | - | wv.schedulePendingNavigationReplay(browser, attempt+1) | |
| 910 | + | wv.schedulePendingNavigationReplay(attempt + 1) | |
| 880 | 911 | return | |
| 881 | 912 | } | |
| 882 | 913 | currentURL := frame.GetURL() | |
| @@ -34,10 +34,14 @@ var errNoBrowser = errors.New("cef: browser not yet created") | |||||
| 34 | 34 | ||||
| 35 | const ( | 35 | const ( | ||
| 36 | clipboardSelectionDebounceDelay = 300 * time.Millisecond | 36 | clipboardSelectionDebounceDelay = 300 * time.Millisecond | ||
| 37 | - | pendingNavigationRetryDelay = 25 * time.Millisecond | 37 | + | pendingNavigationRetryDelay = 50 * time.Millisecond |
| 38 | - | pendingNavigationMaxRetries = 20 | 38 | + | pendingNavigationMaxRetries = 80 |
| 39 | ) | 39 | ) | ||
| 40 | 40 | ||||
| 41 | + | type stoppableTimer interface { | |||
| 42 | + | Stop() bool | |||
| 43 | + | } | |||
| 44 | + | ||||
| 41 | var cefLoadWatchdogDelays = []time.Duration{ | 45 | var cefLoadWatchdogDelays = []time.Duration{ | ||
| 42 | 250 * time.Millisecond, | 46 | 250 * time.Millisecond, | ||
| 43 | 1 * time.Second, | 47 | 1 * time.Second, | ||
| @@ -89,16 +93,18 @@ type WebView struct { | |||||
| 89 | callbacks *port.WebViewCallbacks | 93 | callbacks *port.WebViewCallbacks | ||
| 90 | 94 | ||||
| 91 | // State cache (mutex-protected). | 95 | // State cache (mutex-protected). | ||
| 92 | - | uri string | 96 | + | uri string |
| 93 | - | title string | 97 | + | title string |
| 94 | - | progress float64 | 98 | + | progress float64 |
| 95 | - | canGoBack bool | 99 | + | canGoBack bool |
| 96 | - | canGoFwd bool | 100 | + | canGoFwd bool |
| 97 | - | isLoading bool | 101 | + | isLoading bool |
| 98 | - | selectedText string | 102 | + | selectedText string |
| 99 | - | focusedEditable bool | 103 | + | focusedEditable bool |
| 100 | - | selectionDebounceTimer *time.Timer | 104 | + | selectionDebounceTimer stoppableTimer |
| 101 | - | selectionDebounceSeq uint64 | 105 | + | selectionDebounceSeq uint64 |
| 106 | + | selectionDebounceDelay *time.Duration | |||
| 107 | + | selectionDebounceSchedule func(time.Duration, func()) stoppableTimer | |||
| 102 | 108 | ||||
| 103 | // Last known hover URI for middle-click → new tab. | 109 | // Last known hover URI for middle-click → new tab. | ||
| 104 | lastHoverURI string | 110 | lastHoverURI string | ||
| @@ -169,7 +175,7 @@ func (wv *WebView) LoadURI(_ context.Context, uri string) error { | |||||
| 169 | if browser == nil { | 175 | if browser == nil { | ||
| 170 | return nil | 176 | return nil | ||
| 171 | } | 177 | } | ||
| 172 | - | wv.schedulePendingNavigationReplay(browser, 0) | 178 | + | wv.schedulePendingNavigationReplay(0) |
| 173 | return nil | 179 | return nil | ||
| 174 | } | 180 | } | ||
| 175 | 181 | ||||
| @@ -677,6 +683,22 @@ func (wv *WebView) cancelSelectionDebounce() { | |||||
| 677 | } | 683 | } | ||
| 678 | } | 684 | } | ||
| 679 | 685 | ||||
| 686 | + | func (wv *WebView) selectionDebounceInterval() time.Duration { | |||
| 687 | + | if wv == nil || wv.selectionDebounceDelay == nil { | |||
| 688 | + | return clipboardSelectionDebounceDelay | |||
| 689 | + | } | |||
| 690 | + | return *wv.selectionDebounceDelay | |||
| 691 | + | } | |||
| 692 | + | ||||
| 693 | + | func (wv *WebView) selectionDebounceScheduler() func(time.Duration, func()) stoppableTimer { | |||
| 694 | + | if wv != nil && wv.selectionDebounceSchedule != nil { | |||
| 695 | + | return wv.selectionDebounceSchedule | |||
| 696 | + | } | |||
| 697 | + | return func(delay time.Duration, fn func()) stoppableTimer { | |||
| 698 | + | return time.AfterFunc(delay, fn) | |||
| 699 | + | } | |||
| 700 | + | } | |||
| 701 | + | ||||
| 680 | func (wv *WebView) scheduleSelectionUpdate(text string) { | 702 | func (wv *WebView) scheduleSelectionUpdate(text string) { | ||
| 681 | if wv == nil || wv.destroyed.Load() { | 703 | if wv == nil || wv.destroyed.Load() { | ||
| 682 | return | 704 | return | ||
| @@ -700,7 +722,15 @@ func (wv *WebView) scheduleSelectionUpdate(text string) { | |||||
| 700 | if timer := wv.selectionDebounceTimer; timer != nil { | 722 | if timer := wv.selectionDebounceTimer; timer != nil { | ||
| 701 | timer.Stop() | 723 | timer.Stop() | ||
| 702 | } | 724 | } | ||
| 703 | - | wv.selectionDebounceTimer = time.AfterFunc(clipboardSelectionDebounceDelay, func() { | 725 | + | delay := wv.selectionDebounceInterval() |
| 726 | + | if delay <= 0 { | |||
| 727 | + | wv.selectionDebounceTimer = nil | |||
| 728 | + | wv.mu.Unlock() | |||
| 729 | + | wv.flushSelectionUpdate(seq, text) | |||
| 730 | + | return | |||
| 731 | + | } | |||
| 732 | + | scheduler := wv.selectionDebounceScheduler() | |||
| 733 | + | wv.selectionDebounceTimer = scheduler(delay, func() { | |||
| 704 | wv.flushSelectionUpdate(seq, text) | 734 | wv.flushSelectionUpdate(seq, text) | ||
| 705 | }) | 735 | }) | ||
| 706 | wv.mu.Unlock() | 736 | wv.mu.Unlock() | ||
| @@ -812,12 +842,12 @@ func (wv *WebView) pendingNavigationURI() string { | |||||
| 812 | return wv.pendingURI | 842 | return wv.pendingURI | ||
| 813 | } | 843 | } | ||
| 814 | 844 | ||||
| 815 | - | func (wv *WebView) schedulePendingNavigationReplay(browser purecef.Browser, attempt int) { | 845 | + | func (wv *WebView) schedulePendingNavigationReplay(attempt int) { |
| 816 | - | if wv == nil || browser == nil || wv.destroyed.Load() { | 846 | + | if wv == nil || wv.destroyed.Load() { |
| 817 | return | 847 | return | ||
| 818 | } | 848 | } | ||
| 819 | task := cefNewTask(cefTaskFunc(func() { | 849 | task := cefNewTask(cefTaskFunc(func() { | ||
| 820 | - | wv.replayPendingNavigation(browser, attempt) | 850 | + | wv.replayPendingNavigation(attempt) |
| 821 | })) | 851 | })) | ||
| 822 | if task == nil { | 852 | if task == nil { | ||
| 823 | return | 853 | return | ||
| @@ -845,18 +875,19 @@ func (wv *WebView) schedulePendingNavigationReplay(browser purecef.Browser, atte | |||||
| 845 | return | 875 | return | ||
| 846 | } | 876 | } | ||
| 847 | cefScheduleAfter(pendingNavigationRetryDelay, func() { | 877 | cefScheduleAfter(pendingNavigationRetryDelay, func() { | ||
| 848 | - | wv.schedulePendingNavigationReplay(browser, attempt+1) | 878 | + | wv.schedulePendingNavigationReplay(attempt + 1) |
| 849 | }) | 879 | }) | ||
| 850 | } | 880 | } | ||
| 851 | 881 | ||||
| 852 | - | func (wv *WebView) replayPendingNavigation(browser purecef.Browser, attempt int) { | 882 | + | func (wv *WebView) replayPendingNavigation(attempt int) { |
| 853 | - | if wv == nil || browser == nil || wv.destroyed.Load() { | 883 | + | if wv == nil || wv.destroyed.Load() { |
| 854 | return | 884 | return | ||
| 855 | } | 885 | } | ||
| 856 | wv.mu.RLock() | 886 | wv.mu.RLock() | ||
| 857 | uri := wv.pendingURI | 887 | uri := wv.pendingURI | ||
| 888 | + | browser := wv.browser | |||
| 858 | wv.mu.RUnlock() | 889 | wv.mu.RUnlock() | ||
| 859 | - | if uri == "" { | 890 | + | if uri == "" || browser == nil { |
| 860 | return | 891 | return | ||
| 861 | } | 892 | } | ||
| 862 | frame := browser.GetMainFrame() | 893 | frame := browser.GetMainFrame() | ||
| @@ -876,7 +907,7 @@ func (wv *WebView) replayPendingNavigation(browser purecef.Browser, attempt int) | |||||
| 876 | Str("uri", logging.TruncateURL(uri, logging.PermissionLogURLMaxLen)). | 907 | Str("uri", logging.TruncateURL(uri, logging.PermissionLogURLMaxLen)). | ||
| 877 | Msg("cef: pending navigation replay waiting for main frame") | 908 | Msg("cef: pending navigation replay waiting for main frame") | ||
| 878 | } | 909 | } | ||
| 879 | - | wv.schedulePendingNavigationReplay(browser, attempt+1) | 910 | + | wv.schedulePendingNavigationReplay(attempt + 1) |
| 880 | return | 911 | return | ||
| 881 | } | 912 | } | ||
| 882 | currentURL := frame.GetURL() | 913 | currentURL := frame.GetURL() | ||
M internal/infrastructure/cef/webview_handlers.go +8 -6
| @@ -376,16 +376,16 @@ func (h *handlerSet) OnTextSelectionChanged(_ purecef.Browser, selectedText stri | |||
| 376 | 376 | return | |
| 377 | 377 | } | |
| 378 | 378 | previous, changed := h.wv.setSelectedText(selectedText) | |
| 379 | - | if !changed || h.wv.ctx == nil { | |
| 379 | + | if !changed { | |
| 380 | 380 | return | |
| 381 | 381 | } | |
| 382 | - | if selectedText == "" { | |
| 382 | + | if h.wv.ctx != nil && selectedText == "" { | |
| 383 | 383 | if previous != "" { | |
| 384 | 384 | logging.FromContext(h.wv.ctx).Debug(). | |
| 385 | 385 | Int("prev_text_len", len(previous)). | |
| 386 | 386 | Msg("cef: text selection cleared") | |
| 387 | 387 | } | |
| 388 | - | } else { | |
| 388 | + | } else if h.wv.ctx != nil { | |
| 389 | 389 | logging.FromContext(h.wv.ctx).Debug(). | |
| 390 | 390 | Int("text_len", len(selectedText)). | |
| 391 | 391 | Msg("cef: text selection changed") | |
| @@ -547,7 +547,9 @@ func (h *handlerSet) OnLoadingStateChange(_ purecef.Browser, isloading, cangobac | |||
| 547 | 547 | h.wv.mu.RLock() | |
| 548 | 548 | host := h.wv.host | |
| 549 | 549 | h.wv.mu.RUnlock() | |
| 550 | - | syncWindowlessBrowserFocus(host) | |
| 550 | + | if host != nil { | |
| 551 | + | syncWindowlessBrowserFocus(host) | |
| 552 | + | } | |
| 551 | 553 | } | |
| 552 | 554 | ||
| 553 | 555 | h.wv.mu.RLock() | |
| @@ -611,7 +613,7 @@ func (h *handlerSet) OnLoadEnd(_ purecef.Browser, frame purecef.Frame, httpStatu | |||
| 611 | 613 | log.Debug(). | |
| 612 | 614 | Str("pending_uri", logging.TruncateURL(pendingURI, logging.PermissionLogURLMaxLen)). | |
| 613 | 615 | Msg("cef: replaying pending navigation after about:blank load end") | |
| 614 | - | h.wv.schedulePendingNavigationReplay(h.wv.browser, 0) | |
| 616 | + | h.wv.schedulePendingNavigationReplay(0) | |
| 615 | 617 | } | |
| 616 | 618 | } | |
| 617 | 619 | ||
| @@ -738,7 +740,7 @@ func (h *handlerSet) OnAfterCreated(browser purecef.Browser) { | |||
| 738 | 740 | host.Invalidate(purecef.PaintElementTypePetView) | |
| 739 | 741 | } | |
| 740 | 742 | if hasPendingNavigation { | |
| 741 | - | h.wv.schedulePendingNavigationReplay(browser, 0) | |
| 743 | + | h.wv.schedulePendingNavigationReplay(0) | |
| 742 | 744 | } | |
| 743 | 745 | ||
| 744 | 746 | h.wv.scheduleStartBeginFrameLoop() | |
| @@ -376,16 +376,16 @@ func (h *handlerSet) OnTextSelectionChanged(_ purecef.Browser, selectedText stri | |||||
| 376 | return | 376 | return | ||
| 377 | } | 377 | } | ||
| 378 | previous, changed := h.wv.setSelectedText(selectedText) | 378 | previous, changed := h.wv.setSelectedText(selectedText) | ||
| 379 | - | if !changed || h.wv.ctx == nil { | 379 | + | if !changed { |
| 380 | return | 380 | return | ||
| 381 | } | 381 | } | ||
| 382 | - | if selectedText == "" { | 382 | + | if h.wv.ctx != nil && selectedText == "" { |
| 383 | if previous != "" { | 383 | if previous != "" { | ||
| 384 | logging.FromContext(h.wv.ctx).Debug(). | 384 | logging.FromContext(h.wv.ctx).Debug(). | ||
| 385 | Int("prev_text_len", len(previous)). | 385 | Int("prev_text_len", len(previous)). | ||
| 386 | Msg("cef: text selection cleared") | 386 | Msg("cef: text selection cleared") | ||
| 387 | } | 387 | } | ||
| 388 | - | } else { | 388 | + | } else if h.wv.ctx != nil { |
| 389 | logging.FromContext(h.wv.ctx).Debug(). | 389 | logging.FromContext(h.wv.ctx).Debug(). | ||
| 390 | Int("text_len", len(selectedText)). | 390 | Int("text_len", len(selectedText)). | ||
| 391 | Msg("cef: text selection changed") | 391 | Msg("cef: text selection changed") | ||
| @@ -547,7 +547,9 @@ func (h *handlerSet) OnLoadingStateChange(_ purecef.Browser, isloading, cangobac | |||||
| 547 | h.wv.mu.RLock() | 547 | h.wv.mu.RLock() | ||
| 548 | host := h.wv.host | 548 | host := h.wv.host | ||
| 549 | h.wv.mu.RUnlock() | 549 | h.wv.mu.RUnlock() | ||
| 550 | - | syncWindowlessBrowserFocus(host) | 550 | + | if host != nil { |
| 551 | + | syncWindowlessBrowserFocus(host) | |||
| 552 | + | } | |||
| 551 | } | 553 | } | ||
| 552 | 554 | ||||
| 553 | h.wv.mu.RLock() | 555 | h.wv.mu.RLock() | ||
| @@ -611,7 +613,7 @@ func (h *handlerSet) OnLoadEnd(_ purecef.Browser, frame purecef.Frame, httpStatu | |||||
| 611 | log.Debug(). | 613 | log.Debug(). | ||
| 612 | Str("pending_uri", logging.TruncateURL(pendingURI, logging.PermissionLogURLMaxLen)). | 614 | Str("pending_uri", logging.TruncateURL(pendingURI, logging.PermissionLogURLMaxLen)). | ||
| 613 | Msg("cef: replaying pending navigation after about:blank load end") | 615 | Msg("cef: replaying pending navigation after about:blank load end") | ||
| 614 | - | h.wv.schedulePendingNavigationReplay(h.wv.browser, 0) | 616 | + | h.wv.schedulePendingNavigationReplay(0) |
| 615 | } | 617 | } | ||
| 616 | } | 618 | } | ||
| 617 | 619 | ||||
| @@ -738,7 +740,7 @@ func (h *handlerSet) OnAfterCreated(browser purecef.Browser) { | |||||
| 738 | host.Invalidate(purecef.PaintElementTypePetView) | 740 | host.Invalidate(purecef.PaintElementTypePetView) | ||
| 739 | } | 741 | } | ||
| 740 | if hasPendingNavigation { | 742 | if hasPendingNavigation { | ||
| 741 | - | h.wv.schedulePendingNavigationReplay(browser, 0) | 743 | + | h.wv.schedulePendingNavigationReplay(0) |
| 742 | } | 744 | } | ||
| 743 | 745 | ||||
| 744 | h.wv.scheduleStartBeginFrameLoop() | 746 | h.wv.scheduleStartBeginFrameLoop() | ||
M internal/infrastructure/cef/webview_handlers_test.go +80 -40
| @@ -12,16 +12,48 @@ import ( | |||
| 12 | 12 | "github.com/stretchr/testify/mock" | |
| 13 | 13 | "github.com/stretchr/testify/require" | |
| 14 | 14 | ||
| 15 | + | "github.com/bnema/dumber/internal/application/dto" | |
| 15 | 16 | "github.com/bnema/dumber/internal/application/port" | |
| 16 | 17 | portmocks "github.com/bnema/dumber/internal/application/port/mocks" | |
| 17 | 18 | ) | |
| 18 | 19 | ||
| 19 | 20 | type clipboardOrchestratorRecorder struct { | |
| 20 | 21 | mu sync.Mutex | |
| 21 | - | selection port.SelectionClipboardInput | |
| 22 | + | selection dto.SelectionClipboardInput | |
| 22 | 23 | selectionCalls int | |
| 23 | 24 | } | |
| 24 | 25 | ||
| 26 | + | type controlledSelectionTimer struct { | |
| 27 | + | stopped bool | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | func (t *controlledSelectionTimer) Stop() bool { | |
| 31 | + | alreadyStopped := t.stopped | |
| 32 | + | t.stopped = true | |
| 33 | + | return !alreadyStopped | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | type controlledSelectionScheduler struct { | |
| 37 | + | timers []*controlledSelectionTimer | |
| 38 | + | callbacks []func() | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | func (s *controlledSelectionScheduler) schedule(_ time.Duration, fn func()) stoppableTimer { | |
| 42 | + | timer := &controlledSelectionTimer{} | |
| 43 | + | s.timers = append(s.timers, timer) | |
| 44 | + | s.callbacks = append(s.callbacks, fn) | |
| 45 | + | return timer | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | func (s *controlledSelectionScheduler) fire(index int) { | |
| 49 | + | if index < 0 || index >= len(s.callbacks) { | |
| 50 | + | return | |
| 51 | + | } | |
| 52 | + | if !s.timers[index].stopped { | |
| 53 | + | s.callbacks[index]() | |
| 54 | + | } | |
| 55 | + | } | |
| 56 | + | ||
| 25 | 57 | func newTestPipeline(w, h, s int32) *renderPipeline { | |
| 26 | 58 | rp := &renderPipeline{scale: s} | |
| 27 | 59 | rp.widthAtomic.Store(w) | |
| @@ -103,15 +135,17 @@ func TestOnLoadEndDoesNotDispatchBrowserLevelCompletion(t *testing.T) { | |||
| 103 | 135 | func TestOnTextSelectionChanged_ForwardsSelectionToClipboardOrchestrator(t *testing.T) { | |
| 104 | 136 | recorder := &clipboardOrchestratorRecorder{} | |
| 105 | 137 | orchestrator := portmocks.NewMockClipboardTextOrchestrator(t) | |
| 106 | - | orchestrator.EXPECT().HandleSelectionUpdate(mock.Anything, mock.Anything).Run(func(_ context.Context, input port.SelectionClipboardInput) { | |
| 138 | + | orchestrator.EXPECT().HandleSelectionUpdate(mock.Anything, mock.Anything).Run(func(_ context.Context, input dto.SelectionClipboardInput) { | |
| 107 | 139 | recorder.mu.Lock() | |
| 108 | 140 | defer recorder.mu.Unlock() | |
| 109 | 141 | recorder.selection = input | |
| 110 | 142 | recorder.selectionCalls++ | |
| 111 | 143 | }).Return(nil).Once() | |
| 144 | + | zeroDelay := time.Duration(0) | |
| 112 | 145 | wv := &WebView{ | |
| 113 | - | ctx: context.Background(), | |
| 114 | - | id: 42, | |
| 146 | + | ctx: context.Background(), | |
| 147 | + | id: 42, | |
| 148 | + | selectionDebounceDelay: &zeroDelay, | |
| 115 | 149 | engine: &Engine{ | |
| 116 | 150 | clipboardTextOrchestrator: orchestrator, | |
| 117 | 151 | }, | |
| @@ -120,28 +154,28 @@ func TestOnTextSelectionChanged_ForwardsSelectionToClipboardOrchestrator(t *test | |||
| 120 | 154 | ||
| 121 | 155 | h.OnTextSelectionChanged(nil, "selected text", nil) | |
| 122 | 156 | ||
| 123 | - | require.Eventually(t, func() bool { | |
| 124 | - | recorder.mu.Lock() | |
| 125 | - | defer recorder.mu.Unlock() | |
| 126 | - | return recorder.selectionCalls == 1 && | |
| 127 | - | recorder.selection.Text == "selected text" && | |
| 128 | - | recorder.selection.SourceEngine == port.ClipboardSourceCEF && | |
| 129 | - | recorder.selection.ViewID == port.WebViewID(42) | |
| 130 | - | }, 2*time.Second, 10*time.Millisecond) | |
| 157 | + | recorder.mu.Lock() | |
| 158 | + | defer recorder.mu.Unlock() | |
| 159 | + | require.Equal(t, 1, recorder.selectionCalls) | |
| 160 | + | require.Equal(t, "selected text", recorder.selection.Text) | |
| 161 | + | require.Equal(t, dto.ClipboardSourceCEF, recorder.selection.SourceEngine) | |
| 162 | + | require.Equal(t, uint64(42), recorder.selection.ViewID) | |
| 131 | 163 | } | |
| 132 | 164 | ||
| 133 | 165 | func TestOnTextSelectionChanged_DebouncesAndCollapsesRapidUpdates(t *testing.T) { | |
| 134 | 166 | recorder := &clipboardOrchestratorRecorder{} | |
| 167 | + | scheduler := &controlledSelectionScheduler{} | |
| 135 | 168 | orchestrator := portmocks.NewMockClipboardTextOrchestrator(t) | |
| 136 | - | orchestrator.EXPECT().HandleSelectionUpdate(mock.Anything, mock.Anything).Run(func(_ context.Context, input port.SelectionClipboardInput) { | |
| 169 | + | orchestrator.EXPECT().HandleSelectionUpdate(mock.Anything, mock.Anything).Run(func(_ context.Context, input dto.SelectionClipboardInput) { | |
| 137 | 170 | recorder.mu.Lock() | |
| 138 | 171 | defer recorder.mu.Unlock() | |
| 139 | 172 | recorder.selection = input | |
| 140 | 173 | recorder.selectionCalls++ | |
| 141 | 174 | }).Return(nil).Once() | |
| 142 | 175 | wv := &WebView{ | |
| 143 | - | ctx: context.Background(), | |
| 144 | - | id: 42, | |
| 176 | + | ctx: context.Background(), | |
| 177 | + | id: 42, | |
| 178 | + | selectionDebounceSchedule: scheduler.schedule, | |
| 145 | 179 | engine: &Engine{ | |
| 146 | 180 | clipboardTextOrchestrator: orchestrator, | |
| 147 | 181 | }, | |
| @@ -155,27 +189,36 @@ func TestOnTextSelectionChanged_DebouncesAndCollapsesRapidUpdates(t *testing.T) | |||
| 155 | 189 | gotCalls := recorder.selectionCalls | |
| 156 | 190 | recorder.mu.Unlock() | |
| 157 | 191 | require.Zero(t, gotCalls) | |
| 192 | + | require.Len(t, scheduler.callbacks, 2) | |
| 158 | 193 | ||
| 159 | - | require.Eventually(t, func() bool { | |
| 160 | - | recorder.mu.Lock() | |
| 161 | - | defer recorder.mu.Unlock() | |
| 162 | - | return recorder.selectionCalls == 1 && recorder.selection.Text == "second selection" | |
| 163 | - | }, 2*time.Second, 10*time.Millisecond) | |
| 194 | + | scheduler.fire(0) | |
| 195 | + | recorder.mu.Lock() | |
| 196 | + | gotCalls = recorder.selectionCalls | |
| 197 | + | recorder.mu.Unlock() | |
| 198 | + | require.Zero(t, gotCalls) | |
| 199 | + | ||
| 200 | + | scheduler.fire(1) | |
| 201 | + | recorder.mu.Lock() | |
| 202 | + | defer recorder.mu.Unlock() | |
| 203 | + | require.Equal(t, 1, recorder.selectionCalls) | |
| 204 | + | require.Equal(t, "second selection", recorder.selection.Text) | |
| 164 | 205 | require.Equal(t, "second selection", wv.selectedTextSnapshot()) | |
| 165 | 206 | } | |
| 166 | 207 | ||
| 167 | 208 | func TestOnTextSelectionChanged_SuppressesAutoCopyWhenFocusedNodeEditableAndResumesWhenCleared(t *testing.T) { | |
| 168 | 209 | recorder := &clipboardOrchestratorRecorder{} | |
| 210 | + | scheduler := &controlledSelectionScheduler{} | |
| 169 | 211 | orchestrator := portmocks.NewMockClipboardTextOrchestrator(t) | |
| 170 | - | orchestrator.EXPECT().HandleSelectionUpdate(mock.Anything, mock.Anything).Run(func(_ context.Context, input port.SelectionClipboardInput) { | |
| 212 | + | orchestrator.EXPECT().HandleSelectionUpdate(mock.Anything, mock.Anything).Run(func(_ context.Context, input dto.SelectionClipboardInput) { | |
| 171 | 213 | recorder.mu.Lock() | |
| 172 | 214 | defer recorder.mu.Unlock() | |
| 173 | 215 | recorder.selection = input | |
| 174 | 216 | recorder.selectionCalls++ | |
| 175 | 217 | }).Return(nil).Once() | |
| 176 | 218 | wv := &WebView{ | |
| 177 | - | ctx: context.Background(), | |
| 178 | - | id: 42, | |
| 219 | + | ctx: context.Background(), | |
| 220 | + | id: 42, | |
| 221 | + | selectionDebounceSchedule: scheduler.schedule, | |
| 179 | 222 | engine: &Engine{ | |
| 180 | 223 | clipboardTextOrchestrator: orchestrator, | |
| 181 | 224 | }, | |
| @@ -204,12 +247,7 @@ func TestOnTextSelectionChanged_SuppressesAutoCopyWhenFocusedNodeEditableAndResu | |||
| 204 | 247 | gotCalls := recorder.selectionCalls | |
| 205 | 248 | recorder.mu.Unlock() | |
| 206 | 249 | require.Zero(t, gotCalls) | |
| 207 | - | start := time.Now() | |
| 208 | - | require.Eventually(t, func() bool { | |
| 209 | - | recorder.mu.Lock() | |
| 210 | - | defer recorder.mu.Unlock() | |
| 211 | - | return recorder.selectionCalls == 0 && time.Since(start) >= clipboardSelectionDebounceDelay+50*time.Millisecond | |
| 212 | - | }, 2*time.Second, 10*time.Millisecond) | |
| 250 | + | require.Empty(t, scheduler.callbacks) | |
| 213 | 251 | ||
| 214 | 252 | newRendererBridgeProcessMessage = func(name string) purecef.ProcessMessage { | |
| 215 | 253 | return newTestBridgeProcessMessage(name, false) | |
| @@ -229,18 +267,22 @@ func TestOnTextSelectionChanged_SuppressesAutoCopyWhenFocusedNodeEditableAndResu | |||
| 229 | 267 | gotCalls = recorder.selectionCalls | |
| 230 | 268 | recorder.mu.Unlock() | |
| 231 | 269 | require.Zero(t, gotCalls) | |
| 232 | - | require.Eventually(t, func() bool { | |
| 233 | - | recorder.mu.Lock() | |
| 234 | - | defer recorder.mu.Unlock() | |
| 235 | - | return recorder.selectionCalls == 1 && recorder.selection.Text == "free selection" | |
| 236 | - | }, 2*time.Second, 10*time.Millisecond) | |
| 270 | + | require.Len(t, scheduler.callbacks, 1) | |
| 271 | + | ||
| 272 | + | scheduler.fire(0) | |
| 273 | + | recorder.mu.Lock() | |
| 274 | + | defer recorder.mu.Unlock() | |
| 275 | + | require.Equal(t, 1, recorder.selectionCalls) | |
| 276 | + | require.Equal(t, "free selection", recorder.selection.Text) | |
| 237 | 277 | } | |
| 238 | 278 | ||
| 239 | 279 | func TestOnTextSelectionChanged_DoesNotEmitLateDebouncedUpdateAfterDestroy(t *testing.T) { | |
| 280 | + | scheduler := &controlledSelectionScheduler{} | |
| 240 | 281 | orchestrator := portmocks.NewMockClipboardTextOrchestrator(t) | |
| 241 | 282 | wv := &WebView{ | |
| 242 | - | ctx: context.Background(), | |
| 243 | - | id: 42, | |
| 283 | + | ctx: context.Background(), | |
| 284 | + | id: 42, | |
| 285 | + | selectionDebounceSchedule: scheduler.schedule, | |
| 244 | 286 | engine: &Engine{ | |
| 245 | 287 | clipboardTextOrchestrator: orchestrator, | |
| 246 | 288 | }, | |
| @@ -248,12 +290,10 @@ func TestOnTextSelectionChanged_DoesNotEmitLateDebouncedUpdateAfterDestroy(t *te | |||
| 248 | 290 | h := &handlerSet{wv: wv} | |
| 249 | 291 | ||
| 250 | 292 | h.OnTextSelectionChanged(nil, "selected text", nil) | |
| 293 | + | require.Len(t, scheduler.callbacks, 1) | |
| 251 | 294 | wv.Destroy() | |
| 295 | + | scheduler.fire(0) | |
| 252 | 296 | ||
| 253 | - | start := time.Now() | |
| 254 | - | require.Eventually(t, func() bool { | |
| 255 | - | return time.Since(start) >= clipboardSelectionDebounceDelay+50*time.Millisecond | |
| 256 | - | }, 2*time.Second, 10*time.Millisecond) | |
| 257 | 297 | orchestrator.AssertNotCalled(t, "HandleSelectionUpdate", mock.Anything, mock.Anything) | |
| 258 | 298 | require.Equal(t, "selected text", wv.selectedTextSnapshot()) | |
| 259 | 299 | } | |
| @@ -12,16 +12,48 @@ import ( | |||||
| 12 | "github.com/stretchr/testify/mock" | 12 | "github.com/stretchr/testify/mock" | ||
| 13 | "github.com/stretchr/testify/require" | 13 | "github.com/stretchr/testify/require" | ||
| 14 | 14 | ||||
| 15 | + | "github.com/bnema/dumber/internal/application/dto" | |||
| 15 | "github.com/bnema/dumber/internal/application/port" | 16 | "github.com/bnema/dumber/internal/application/port" | ||
| 16 | portmocks "github.com/bnema/dumber/internal/application/port/mocks" | 17 | portmocks "github.com/bnema/dumber/internal/application/port/mocks" | ||
| 17 | ) | 18 | ) | ||
| 18 | 19 | ||||
| 19 | type clipboardOrchestratorRecorder struct { | 20 | type clipboardOrchestratorRecorder struct { | ||
| 20 | mu sync.Mutex | 21 | mu sync.Mutex | ||
| 21 | - | selection port.SelectionClipboardInput | 22 | + | selection dto.SelectionClipboardInput |
| 22 | selectionCalls int | 23 | selectionCalls int | ||
| 23 | } | 24 | } | ||
| 24 | 25 | ||||
| 26 | + | type controlledSelectionTimer struct { | |||
| 27 | + | stopped bool | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | func (t *controlledSelectionTimer) Stop() bool { | |||
| 31 | + | alreadyStopped := t.stopped | |||
| 32 | + | t.stopped = true | |||
| 33 | + | return !alreadyStopped | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | type controlledSelectionScheduler struct { | |||
| 37 | + | timers []*controlledSelectionTimer | |||
| 38 | + | callbacks []func() | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | func (s *controlledSelectionScheduler) schedule(_ time.Duration, fn func()) stoppableTimer { | |||
| 42 | + | timer := &controlledSelectionTimer{} | |||
| 43 | + | s.timers = append(s.timers, timer) | |||
| 44 | + | s.callbacks = append(s.callbacks, fn) | |||
| 45 | + | return timer | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | func (s *controlledSelectionScheduler) fire(index int) { | |||
| 49 | + | if index < 0 || index >= len(s.callbacks) { | |||
| 50 | + | return | |||
| 51 | + | } | |||
| 52 | + | if !s.timers[index].stopped { | |||
| 53 | + | s.callbacks[index]() | |||
| 54 | + | } | |||
| 55 | + | } | |||
| 56 | + | ||||
| 25 | func newTestPipeline(w, h, s int32) *renderPipeline { | 57 | func newTestPipeline(w, h, s int32) *renderPipeline { | ||
| 26 | rp := &renderPipeline{scale: s} | 58 | rp := &renderPipeline{scale: s} | ||
| 27 | rp.widthAtomic.Store(w) | 59 | rp.widthAtomic.Store(w) | ||
| @@ -103,15 +135,17 @@ func TestOnLoadEndDoesNotDispatchBrowserLevelCompletion(t *testing.T) { | |||||
| 103 | func TestOnTextSelectionChanged_ForwardsSelectionToClipboardOrchestrator(t *testing.T) { | 135 | func TestOnTextSelectionChanged_ForwardsSelectionToClipboardOrchestrator(t *testing.T) { | ||
| 104 | recorder := &clipboardOrchestratorRecorder{} | 136 | recorder := &clipboardOrchestratorRecorder{} | ||
| 105 | orchestrator := portmocks.NewMockClipboardTextOrchestrator(t) | 137 | orchestrator := portmocks.NewMockClipboardTextOrchestrator(t) | ||
| 106 | - | orchestrator.EXPECT().HandleSelectionUpdate(mock.Anything, mock.Anything).Run(func(_ context.Context, input port.SelectionClipboardInput) { | 138 | + | orchestrator.EXPECT().HandleSelectionUpdate(mock.Anything, mock.Anything).Run(func(_ context.Context, input dto.SelectionClipboardInput) { |
| 107 | recorder.mu.Lock() | 139 | recorder.mu.Lock() | ||
| 108 | defer recorder.mu.Unlock() | 140 | defer recorder.mu.Unlock() | ||
| 109 | recorder.selection = input | 141 | recorder.selection = input | ||
| 110 | recorder.selectionCalls++ | 142 | recorder.selectionCalls++ | ||
| 111 | }).Return(nil).Once() | 143 | }).Return(nil).Once() | ||
| 144 | + | zeroDelay := time.Duration(0) | |||
| 112 | wv := &WebView{ | 145 | wv := &WebView{ | ||
| 113 | - | ctx: context.Background(), | 146 | + | ctx: context.Background(), |
| 114 | - | id: 42, | 147 | + | id: 42, |
| 148 | + | selectionDebounceDelay: &zeroDelay, | |||
| 115 | engine: &Engine{ | 149 | engine: &Engine{ | ||
| 116 | clipboardTextOrchestrator: orchestrator, | 150 | clipboardTextOrchestrator: orchestrator, | ||
| 117 | }, | 151 | }, | ||
| @@ -120,28 +154,28 @@ func TestOnTextSelectionChanged_ForwardsSelectionToClipboardOrchestrator(t *test | |||||
| 120 | 154 | ||||
| 121 | h.OnTextSelectionChanged(nil, "selected text", nil) | 155 | h.OnTextSelectionChanged(nil, "selected text", nil) | ||
| 122 | 156 | ||||
| 123 | - | require.Eventually(t, func() bool { | 157 | + | recorder.mu.Lock() |
| 124 | - | recorder.mu.Lock() | 158 | + | defer recorder.mu.Unlock() |
| 125 | - | defer recorder.mu.Unlock() | 159 | + | require.Equal(t, 1, recorder.selectionCalls) |
| 126 | - | return recorder.selectionCalls == 1 && | 160 | + | require.Equal(t, "selected text", recorder.selection.Text) |
| 127 | - | recorder.selection.Text == "selected text" && | 161 | + | require.Equal(t, dto.ClipboardSourceCEF, recorder.selection.SourceEngine) |
| 128 | - | recorder.selection.SourceEngine == port.ClipboardSourceCEF && | 162 | + | require.Equal(t, uint64(42), recorder.selection.ViewID) |
| 129 | - | recorder.selection.ViewID == port.WebViewID(42) | |||
| 130 | - | }, 2*time.Second, 10*time.Millisecond) | |||
| 131 | } | 163 | } | ||
| 132 | 164 | ||||
| 133 | func TestOnTextSelectionChanged_DebouncesAndCollapsesRapidUpdates(t *testing.T) { | 165 | func TestOnTextSelectionChanged_DebouncesAndCollapsesRapidUpdates(t *testing.T) { | ||
| 134 | recorder := &clipboardOrchestratorRecorder{} | 166 | recorder := &clipboardOrchestratorRecorder{} | ||
| 167 | + | scheduler := &controlledSelectionScheduler{} | |||
| 135 | orchestrator := portmocks.NewMockClipboardTextOrchestrator(t) | 168 | orchestrator := portmocks.NewMockClipboardTextOrchestrator(t) | ||
| 136 | - | orchestrator.EXPECT().HandleSelectionUpdate(mock.Anything, mock.Anything).Run(func(_ context.Context, input port.SelectionClipboardInput) { | 169 | + | orchestrator.EXPECT().HandleSelectionUpdate(mock.Anything, mock.Anything).Run(func(_ context.Context, input dto.SelectionClipboardInput) { |
| 137 | recorder.mu.Lock() | 170 | recorder.mu.Lock() | ||
| 138 | defer recorder.mu.Unlock() | 171 | defer recorder.mu.Unlock() | ||
| 139 | recorder.selection = input | 172 | recorder.selection = input | ||
| 140 | recorder.selectionCalls++ | 173 | recorder.selectionCalls++ | ||
| 141 | }).Return(nil).Once() | 174 | }).Return(nil).Once() | ||
| 142 | wv := &WebView{ | 175 | wv := &WebView{ | ||
| 143 | - | ctx: context.Background(), | 176 | + | ctx: context.Background(), |
| 144 | - | id: 42, | 177 | + | id: 42, |
| 178 | + | selectionDebounceSchedule: scheduler.schedule, | |||
| 145 | engine: &Engine{ | 179 | engine: &Engine{ | ||
| 146 | clipboardTextOrchestrator: orchestrator, | 180 | clipboardTextOrchestrator: orchestrator, | ||
| 147 | }, | 181 | }, | ||
| @@ -155,27 +189,36 @@ func TestOnTextSelectionChanged_DebouncesAndCollapsesRapidUpdates(t *testing.T) | |||||
| 155 | gotCalls := recorder.selectionCalls | 189 | gotCalls := recorder.selectionCalls | ||
| 156 | recorder.mu.Unlock() | 190 | recorder.mu.Unlock() | ||
| 157 | require.Zero(t, gotCalls) | 191 | require.Zero(t, gotCalls) | ||
| 192 | + | require.Len(t, scheduler.callbacks, 2) | |||
| 158 | 193 | ||||
| 159 | - | require.Eventually(t, func() bool { | 194 | + | scheduler.fire(0) |
| 160 | - | recorder.mu.Lock() | 195 | + | recorder.mu.Lock() |
| 161 | - | defer recorder.mu.Unlock() | 196 | + | gotCalls = recorder.selectionCalls |
| 162 | - | return recorder.selectionCalls == 1 && recorder.selection.Text == "second selection" | 197 | + | recorder.mu.Unlock() |
| 163 | - | }, 2*time.Second, 10*time.Millisecond) | 198 | + | require.Zero(t, gotCalls) |
| 199 | + | ||||
| 200 | + | scheduler.fire(1) | |||
| 201 | + | recorder.mu.Lock() | |||
| 202 | + | defer recorder.mu.Unlock() | |||
| 203 | + | require.Equal(t, 1, recorder.selectionCalls) | |||
| 204 | + | require.Equal(t, "second selection", recorder.selection.Text) | |||
| 164 | require.Equal(t, "second selection", wv.selectedTextSnapshot()) | 205 | require.Equal(t, "second selection", wv.selectedTextSnapshot()) | ||
| 165 | } | 206 | } | ||
| 166 | 207 | ||||
| 167 | func TestOnTextSelectionChanged_SuppressesAutoCopyWhenFocusedNodeEditableAndResumesWhenCleared(t *testing.T) { | 208 | func TestOnTextSelectionChanged_SuppressesAutoCopyWhenFocusedNodeEditableAndResumesWhenCleared(t *testing.T) { | ||
| 168 | recorder := &clipboardOrchestratorRecorder{} | 209 | recorder := &clipboardOrchestratorRecorder{} | ||
| 210 | + | scheduler := &controlledSelectionScheduler{} | |||
| 169 | orchestrator := portmocks.NewMockClipboardTextOrchestrator(t) | 211 | orchestrator := portmocks.NewMockClipboardTextOrchestrator(t) | ||
| 170 | - | orchestrator.EXPECT().HandleSelectionUpdate(mock.Anything, mock.Anything).Run(func(_ context.Context, input port.SelectionClipboardInput) { | 212 | + | orchestrator.EXPECT().HandleSelectionUpdate(mock.Anything, mock.Anything).Run(func(_ context.Context, input dto.SelectionClipboardInput) { |
| 171 | recorder.mu.Lock() | 213 | recorder.mu.Lock() | ||
| 172 | defer recorder.mu.Unlock() | 214 | defer recorder.mu.Unlock() | ||
| 173 | recorder.selection = input | 215 | recorder.selection = input | ||
| 174 | recorder.selectionCalls++ | 216 | recorder.selectionCalls++ | ||
| 175 | }).Return(nil).Once() | 217 | }).Return(nil).Once() | ||
| 176 | wv := &WebView{ | 218 | wv := &WebView{ | ||
| 177 | - | ctx: context.Background(), | 219 | + | ctx: context.Background(), |
| 178 | - | id: 42, | 220 | + | id: 42, |
| 221 | + | selectionDebounceSchedule: scheduler.schedule, | |||
| 179 | engine: &Engine{ | 222 | engine: &Engine{ | ||
| 180 | clipboardTextOrchestrator: orchestrator, | 223 | clipboardTextOrchestrator: orchestrator, | ||
| 181 | }, | 224 | }, | ||
| @@ -204,12 +247,7 @@ func TestOnTextSelectionChanged_SuppressesAutoCopyWhenFocusedNodeEditableAndResu | |||||
| 204 | gotCalls := recorder.selectionCalls | 247 | gotCalls := recorder.selectionCalls | ||
| 205 | recorder.mu.Unlock() | 248 | recorder.mu.Unlock() | ||
| 206 | require.Zero(t, gotCalls) | 249 | require.Zero(t, gotCalls) | ||
| 207 | - | start := time.Now() | 250 | + | require.Empty(t, scheduler.callbacks) |
| 208 | - | require.Eventually(t, func() bool { | |||
| 209 | - | recorder.mu.Lock() | |||
| 210 | - | defer recorder.mu.Unlock() | |||
| 211 | - | return recorder.selectionCalls == 0 && time.Since(start) >= clipboardSelectionDebounceDelay+50*time.Millisecond | |||
| 212 | - | }, 2*time.Second, 10*time.Millisecond) | |||
| 213 | 251 | ||||
| 214 | newRendererBridgeProcessMessage = func(name string) purecef.ProcessMessage { | 252 | newRendererBridgeProcessMessage = func(name string) purecef.ProcessMessage { | ||
| 215 | return newTestBridgeProcessMessage(name, false) | 253 | return newTestBridgeProcessMessage(name, false) | ||
| @@ -229,18 +267,22 @@ func TestOnTextSelectionChanged_SuppressesAutoCopyWhenFocusedNodeEditableAndResu | |||||
| 229 | gotCalls = recorder.selectionCalls | 267 | gotCalls = recorder.selectionCalls | ||
| 230 | recorder.mu.Unlock() | 268 | recorder.mu.Unlock() | ||
| 231 | require.Zero(t, gotCalls) | 269 | require.Zero(t, gotCalls) | ||
| 232 | - | require.Eventually(t, func() bool { | 270 | + | require.Len(t, scheduler.callbacks, 1) |
| 233 | - | recorder.mu.Lock() | 271 | + | |
| 234 | - | defer recorder.mu.Unlock() | 272 | + | scheduler.fire(0) |
| 235 | - | return recorder.selectionCalls == 1 && recorder.selection.Text == "free selection" | 273 | + | recorder.mu.Lock() |
| 236 | - | }, 2*time.Second, 10*time.Millisecond) | 274 | + | defer recorder.mu.Unlock() |
| 275 | + | require.Equal(t, 1, recorder.selectionCalls) | |||
| 276 | + | require.Equal(t, "free selection", recorder.selection.Text) | |||
| 237 | } | 277 | } | ||
| 238 | 278 | ||||
| 239 | func TestOnTextSelectionChanged_DoesNotEmitLateDebouncedUpdateAfterDestroy(t *testing.T) { | 279 | func TestOnTextSelectionChanged_DoesNotEmitLateDebouncedUpdateAfterDestroy(t *testing.T) { | ||
| 280 | + | scheduler := &controlledSelectionScheduler{} | |||
| 240 | orchestrator := portmocks.NewMockClipboardTextOrchestrator(t) | 281 | orchestrator := portmocks.NewMockClipboardTextOrchestrator(t) | ||
| 241 | wv := &WebView{ | 282 | wv := &WebView{ | ||
| 242 | - | ctx: context.Background(), | 283 | + | ctx: context.Background(), |
| 243 | - | id: 42, | 284 | + | id: 42, |
| 285 | + | selectionDebounceSchedule: scheduler.schedule, | |||
| 244 | engine: &Engine{ | 286 | engine: &Engine{ | ||
| 245 | clipboardTextOrchestrator: orchestrator, | 287 | clipboardTextOrchestrator: orchestrator, | ||
| 246 | }, | 288 | }, | ||
| @@ -248,12 +290,10 @@ func TestOnTextSelectionChanged_DoesNotEmitLateDebouncedUpdateAfterDestroy(t *te | |||||
| 248 | h := &handlerSet{wv: wv} | 290 | h := &handlerSet{wv: wv} | ||
| 249 | 291 | ||||
| 250 | h.OnTextSelectionChanged(nil, "selected text", nil) | 292 | h.OnTextSelectionChanged(nil, "selected text", nil) | ||
| 293 | + | require.Len(t, scheduler.callbacks, 1) | |||
| 251 | wv.Destroy() | 294 | wv.Destroy() | ||
| 295 | + | scheduler.fire(0) | |||
| 252 | 296 | ||||
| 253 | - | start := time.Now() | |||
| 254 | - | require.Eventually(t, func() bool { | |||
| 255 | - | return time.Since(start) >= clipboardSelectionDebounceDelay+50*time.Millisecond | |||
| 256 | - | }, 2*time.Second, 10*time.Millisecond) | |||
| 257 | orchestrator.AssertNotCalled(t, "HandleSelectionUpdate", mock.Anything, mock.Anything) | 297 | orchestrator.AssertNotCalled(t, "HandleSelectionUpdate", mock.Anything, mock.Anything) | ||
| 258 | require.Equal(t, "selected text", wv.selectedTextSnapshot()) | 298 | require.Equal(t, "selected text", wv.selectedTextSnapshot()) | ||
| 259 | } | 299 | } | ||
M internal/infrastructure/cef/webview_navigation_test.go +38 -6
| @@ -17,8 +17,8 @@ func TestWebViewReplayPendingNavigation_LoadsQueuedURIWhenMainFrameAvailable(t * | |||
| 17 | 17 | frame.EXPECT().LoadURL("https://github.com/bnema").Once() | |
| 18 | 18 | browser.EXPECT().GetMainFrame().Return(frame).Once() | |
| 19 | 19 | ||
| 20 | - | wv := &WebView{ctx: context.Background(), pendingURI: "https://github.com/bnema"} | |
| 21 | - | wv.replayPendingNavigation(browser, 0) | |
| 20 | + | wv := &WebView{ctx: context.Background(), browser: browser, pendingURI: "https://github.com/bnema"} | |
| 21 | + | wv.replayPendingNavigation(0) | |
| 22 | 22 | ||
| 23 | 23 | wv.mu.RLock() | |
| 24 | 24 | defer wv.mu.RUnlock() | |
| @@ -46,8 +46,8 @@ func TestWebViewReplayPendingNavigation_RetriesWhenMainFrameUnavailable(t *testi | |||
| 46 | 46 | return 1 | |
| 47 | 47 | } | |
| 48 | 48 | ||
| 49 | - | wv := &WebView{ctx: context.Background(), pendingURI: "https://github.com/bnema"} | |
| 50 | - | wv.replayPendingNavigation(browser, 0) | |
| 49 | + | wv := &WebView{ctx: context.Background(), browser: browser, pendingURI: "https://github.com/bnema"} | |
| 50 | + | wv.replayPendingNavigation(0) | |
| 51 | 51 | ||
| 52 | 52 | require.True(t, scheduled) | |
| 53 | 53 | wv.mu.RLock() | |
| @@ -88,12 +88,44 @@ func TestWebViewSchedulePendingNavigationReplay_RetriesWhenTaskPostFails(t *test | |||
| 88 | 88 | return 1 | |
| 89 | 89 | } | |
| 90 | 90 | ||
| 91 | - | wv := &WebView{ctx: context.Background(), pendingURI: "https://github.com/bnema"} | |
| 92 | - | wv.schedulePendingNavigationReplay(browser, 0) | |
| 91 | + | wv := &WebView{ctx: context.Background(), browser: browser, pendingURI: "https://github.com/bnema"} | |
| 92 | + | wv.schedulePendingNavigationReplay(0) | |
| 93 | 93 | ||
| 94 | 94 | require.True(t, retried) | |
| 95 | 95 | } | |
| 96 | 96 | ||
| 97 | + | func TestWebViewReplayPendingNavigation_UsesCurrentBrowserAtExecutionTime(t *testing.T) { | |
| 98 | + | staleBrowser := cefmocks.NewMockBrowser(t) | |
| 99 | + | activeBrowser := cefmocks.NewMockBrowser(t) | |
| 100 | + | frame := cefmocks.NewMockFrame(t) | |
| 101 | + | frame.EXPECT().GetURL().Return("").Once() | |
| 102 | + | frame.EXPECT().LoadURL("https://github.com/bnema").Once() | |
| 103 | + | activeBrowser.EXPECT().GetMainFrame().Return(frame).Once() | |
| 104 | + | ||
| 105 | + | oldTask := cefNewTask | |
| 106 | + | oldPost := cefPostTask | |
| 107 | + | defer func() { | |
| 108 | + | cefNewTask = oldTask | |
| 109 | + | cefPostTask = oldPost | |
| 110 | + | }() | |
| 111 | + | var scheduled purecef.Task | |
| 112 | + | cefNewTask = func(task purecef.Task) purecef.Task { return task } | |
| 113 | + | cefPostTask = func(threadID purecef.ThreadID, task purecef.Task) int32 { | |
| 114 | + | require.Equal(t, purecef.ThreadIDTidUi, threadID) | |
| 115 | + | scheduled = task | |
| 116 | + | return 1 | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | wv := &WebView{ctx: context.Background(), browser: staleBrowser, pendingURI: "https://github.com/bnema"} | |
| 120 | + | wv.schedulePendingNavigationReplay(0) | |
| 121 | + | wv.mu.Lock() | |
| 122 | + | wv.browser = activeBrowser | |
| 123 | + | wv.mu.Unlock() | |
| 124 | + | ||
| 125 | + | require.NotNil(t, scheduled) | |
| 126 | + | scheduled.Execute() | |
| 127 | + | } | |
| 128 | + | ||
| 97 | 129 | func TestWebViewLoadURI_QueuesPendingNavigationReplayForExistingBrowser(t *testing.T) { | |
| 98 | 130 | browser := cefmocks.NewMockBrowser(t) | |
| 99 | 131 | frame := cefmocks.NewMockFrame(t) | |
| @@ -17,8 +17,8 @@ func TestWebViewReplayPendingNavigation_LoadsQueuedURIWhenMainFrameAvailable(t * | |||||
| 17 | frame.EXPECT().LoadURL("https://github.com/bnema").Once() | 17 | frame.EXPECT().LoadURL("https://github.com/bnema").Once() | ||
| 18 | browser.EXPECT().GetMainFrame().Return(frame).Once() | 18 | browser.EXPECT().GetMainFrame().Return(frame).Once() | ||
| 19 | 19 | ||||
| 20 | - | wv := &WebView{ctx: context.Background(), pendingURI: "https://github.com/bnema"} | 20 | + | wv := &WebView{ctx: context.Background(), browser: browser, pendingURI: "https://github.com/bnema"} |
| 21 | - | wv.replayPendingNavigation(browser, 0) | 21 | + | wv.replayPendingNavigation(0) |
| 22 | 22 | ||||
| 23 | wv.mu.RLock() | 23 | wv.mu.RLock() | ||
| 24 | defer wv.mu.RUnlock() | 24 | defer wv.mu.RUnlock() | ||
| @@ -46,8 +46,8 @@ func TestWebViewReplayPendingNavigation_RetriesWhenMainFrameUnavailable(t *testi | |||||
| 46 | return 1 | 46 | return 1 | ||
| 47 | } | 47 | } | ||
| 48 | 48 | ||||
| 49 | - | wv := &WebView{ctx: context.Background(), pendingURI: "https://github.com/bnema"} | 49 | + | wv := &WebView{ctx: context.Background(), browser: browser, pendingURI: "https://github.com/bnema"} |
| 50 | - | wv.replayPendingNavigation(browser, 0) | 50 | + | wv.replayPendingNavigation(0) |
| 51 | 51 | ||||
| 52 | require.True(t, scheduled) | 52 | require.True(t, scheduled) | ||
| 53 | wv.mu.RLock() | 53 | wv.mu.RLock() | ||
| @@ -88,12 +88,44 @@ func TestWebViewSchedulePendingNavigationReplay_RetriesWhenTaskPostFails(t *test | |||||
| 88 | return 1 | 88 | return 1 | ||
| 89 | } | 89 | } | ||
| 90 | 90 | ||||
| 91 | - | wv := &WebView{ctx: context.Background(), pendingURI: "https://github.com/bnema"} | 91 | + | wv := &WebView{ctx: context.Background(), browser: browser, pendingURI: "https://github.com/bnema"} |
| 92 | - | wv.schedulePendingNavigationReplay(browser, 0) | 92 | + | wv.schedulePendingNavigationReplay(0) |
| 93 | 93 | ||||
| 94 | require.True(t, retried) | 94 | require.True(t, retried) | ||
| 95 | } | 95 | } | ||
| 96 | 96 | ||||
| 97 | + | func TestWebViewReplayPendingNavigation_UsesCurrentBrowserAtExecutionTime(t *testing.T) { | |||
| 98 | + | staleBrowser := cefmocks.NewMockBrowser(t) | |||
| 99 | + | activeBrowser := cefmocks.NewMockBrowser(t) | |||
| 100 | + | frame := cefmocks.NewMockFrame(t) | |||
| 101 | + | frame.EXPECT().GetURL().Return("").Once() | |||
| 102 | + | frame.EXPECT().LoadURL("https://github.com/bnema").Once() | |||
| 103 | + | activeBrowser.EXPECT().GetMainFrame().Return(frame).Once() | |||
| 104 | + | ||||
| 105 | + | oldTask := cefNewTask | |||
| 106 | + | oldPost := cefPostTask | |||
| 107 | + | defer func() { | |||
| 108 | + | cefNewTask = oldTask | |||
| 109 | + | cefPostTask = oldPost | |||
| 110 | + | }() | |||
| 111 | + | var scheduled purecef.Task | |||
| 112 | + | cefNewTask = func(task purecef.Task) purecef.Task { return task } | |||
| 113 | + | cefPostTask = func(threadID purecef.ThreadID, task purecef.Task) int32 { | |||
| 114 | + | require.Equal(t, purecef.ThreadIDTidUi, threadID) | |||
| 115 | + | scheduled = task | |||
| 116 | + | return 1 | |||
| 117 | + | } | |||
| 118 | + | ||||
| 119 | + | wv := &WebView{ctx: context.Background(), browser: staleBrowser, pendingURI: "https://github.com/bnema"} | |||
| 120 | + | wv.schedulePendingNavigationReplay(0) | |||
| 121 | + | wv.mu.Lock() | |||
| 122 | + | wv.browser = activeBrowser | |||
| 123 | + | wv.mu.Unlock() | |||
| 124 | + | ||||
| 125 | + | require.NotNil(t, scheduled) | |||
| 126 | + | scheduled.Execute() | |||
| 127 | + | } | |||
| 128 | + | ||||
| 97 | func TestWebViewLoadURI_QueuesPendingNavigationReplayForExistingBrowser(t *testing.T) { | 129 | func TestWebViewLoadURI_QueuesPendingNavigationReplayForExistingBrowser(t *testing.T) { | ||
| 98 | browser := cefmocks.NewMockBrowser(t) | 130 | browser := cefmocks.NewMockBrowser(t) | ||
| 99 | frame := cefmocks.NewMockFrame(t) | 131 | frame := cefmocks.NewMockFrame(t) | ||
M internal/infrastructure/handlers/clipboard.go +7 -6
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "encoding/json" | |
| 6 | 6 | ||
| 7 | + | "github.com/bnema/dumber/internal/application/dto" | |
| 7 | 8 | "github.com/bnema/dumber/internal/application/port" | |
| 8 | 9 | "github.com/bnema/dumber/internal/logging" | |
| 9 | 10 | ) | |
| @@ -44,10 +45,10 @@ func (h *ClipboardHandler) HandleAutoCopySelection() port.WebUIMessageHandler { | |||
| 44 | 45 | if h.orchestrator == nil { | |
| 45 | 46 | return nil, nil | |
| 46 | 47 | } | |
| 47 | - | if err := h.orchestrator.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{ | |
| 48 | + | if err := h.orchestrator.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{ | |
| 48 | 49 | Text: req.Text, | |
| 49 | - | SourceEngine: port.ClipboardSourceWebKit, | |
| 50 | - | ViewID: webviewID, | |
| 50 | + | SourceEngine: dto.ClipboardSourceWebKit, | |
| 51 | + | ViewID: uint64(webviewID), | |
| 51 | 52 | }); err != nil { | |
| 52 | 53 | log.Debug().Err(err).Msg("clipboard selection handling failed") | |
| 53 | 54 | } | |
| @@ -69,11 +70,11 @@ func (h *ClipboardHandler) HandleExplicitCopy() port.WebUIMessageHandler { | |||
| 69 | 70 | if h.orchestrator == nil { | |
| 70 | 71 | return nil, nil | |
| 71 | 72 | } | |
| 72 | - | if err := h.orchestrator.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{ | |
| 73 | + | if err := h.orchestrator.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{ | |
| 73 | 74 | Text: req.Text, | |
| 74 | 75 | Action: req.Action, | |
| 75 | - | SourceEngine: port.ClipboardSourceWebKit, | |
| 76 | - | ViewID: webviewID, | |
| 76 | + | SourceEngine: dto.ClipboardSourceWebKit, | |
| 77 | + | ViewID: uint64(webviewID), | |
| 77 | 78 | NativeHandled: true, | |
| 78 | 79 | }); err != nil { | |
| 79 | 80 | log.Debug().Err(err).Msg("clipboard explicit handling failed") | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "encoding/json" | 5 | "encoding/json" | ||
| 6 | 6 | ||||
| 7 | + | "github.com/bnema/dumber/internal/application/dto" | |||
| 7 | "github.com/bnema/dumber/internal/application/port" | 8 | "github.com/bnema/dumber/internal/application/port" | ||
| 8 | "github.com/bnema/dumber/internal/logging" | 9 | "github.com/bnema/dumber/internal/logging" | ||
| 9 | ) | 10 | ) | ||
| @@ -44,10 +45,10 @@ func (h *ClipboardHandler) HandleAutoCopySelection() port.WebUIMessageHandler { | |||||
| 44 | if h.orchestrator == nil { | 45 | if h.orchestrator == nil { | ||
| 45 | return nil, nil | 46 | return nil, nil | ||
| 46 | } | 47 | } | ||
| 47 | - | if err := h.orchestrator.HandleSelectionUpdate(ctx, port.SelectionClipboardInput{ | 48 | + | if err := h.orchestrator.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{ |
| 48 | Text: req.Text, | 49 | Text: req.Text, | ||
| 49 | - | SourceEngine: port.ClipboardSourceWebKit, | 50 | + | SourceEngine: dto.ClipboardSourceWebKit, |
| 50 | - | ViewID: webviewID, | 51 | + | ViewID: uint64(webviewID), |
| 51 | }); err != nil { | 52 | }); err != nil { | ||
| 52 | log.Debug().Err(err).Msg("clipboard selection handling failed") | 53 | log.Debug().Err(err).Msg("clipboard selection handling failed") | ||
| 53 | } | 54 | } | ||
| @@ -69,11 +70,11 @@ func (h *ClipboardHandler) HandleExplicitCopy() port.WebUIMessageHandler { | |||||
| 69 | if h.orchestrator == nil { | 70 | if h.orchestrator == nil { | ||
| 70 | return nil, nil | 71 | return nil, nil | ||
| 71 | } | 72 | } | ||
| 72 | - | if err := h.orchestrator.HandleExplicitCopy(ctx, port.ExplicitClipboardInput{ | 73 | + | if err := h.orchestrator.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{ |
| 73 | Text: req.Text, | 74 | Text: req.Text, | ||
| 74 | Action: req.Action, | 75 | Action: req.Action, | ||
| 75 | - | SourceEngine: port.ClipboardSourceWebKit, | 76 | + | SourceEngine: dto.ClipboardSourceWebKit, |
| 76 | - | ViewID: webviewID, | 77 | + | ViewID: uint64(webviewID), |
| 77 | NativeHandled: true, | 78 | NativeHandled: true, | ||
| 78 | }); err != nil { | 79 | }); err != nil { | ||
| 79 | log.Debug().Err(err).Msg("clipboard explicit handling failed") | 80 | log.Debug().Err(err).Msg("clipboard explicit handling failed") | ||
M internal/infrastructure/handlers/clipboard_test.go +9 -9
| @@ -6,24 +6,24 @@ import ( | |||
| 6 | 6 | "sync" | |
| 7 | 7 | "testing" | |
| 8 | 8 | ||
| 9 | - | "github.com/bnema/dumber/internal/application/port" | |
| 9 | + | "github.com/bnema/dumber/internal/application/dto" | |
| 10 | 10 | "github.com/stretchr/testify/require" | |
| 11 | 11 | ) | |
| 12 | 12 | ||
| 13 | 13 | type recordingClipboardOrchestrator struct { | |
| 14 | 14 | mu sync.Mutex | |
| 15 | - | selection port.SelectionClipboardInput | |
| 16 | - | explicit port.ExplicitClipboardInput | |
| 15 | + | selection dto.SelectionClipboardInput | |
| 16 | + | explicit dto.ExplicitClipboardInput | |
| 17 | 17 | } | |
| 18 | 18 | ||
| 19 | - | func (r *recordingClipboardOrchestrator) HandleSelectionUpdate(_ context.Context, input port.SelectionClipboardInput) error { | |
| 19 | + | func (r *recordingClipboardOrchestrator) HandleSelectionUpdate(_ context.Context, input dto.SelectionClipboardInput) error { | |
| 20 | 20 | r.mu.Lock() | |
| 21 | 21 | defer r.mu.Unlock() | |
| 22 | 22 | r.selection = input | |
| 23 | 23 | return nil | |
| 24 | 24 | } | |
| 25 | 25 | ||
| 26 | - | func (r *recordingClipboardOrchestrator) HandleExplicitCopy(_ context.Context, input port.ExplicitClipboardInput) error { | |
| 26 | + | func (r *recordingClipboardOrchestrator) HandleExplicitCopy(_ context.Context, input dto.ExplicitClipboardInput) error { | |
| 27 | 27 | r.mu.Lock() | |
| 28 | 28 | defer r.mu.Unlock() | |
| 29 | 29 | r.explicit = input | |
| @@ -41,8 +41,8 @@ func TestClipboardHandler_HandleAutoCopySelection_DelegatesToOrchestrator(t *tes | |||
| 41 | 41 | orchestrator.mu.Lock() | |
| 42 | 42 | defer orchestrator.mu.Unlock() | |
| 43 | 43 | require.Equal(t, "selected text", orchestrator.selection.Text) | |
| 44 | - | require.Equal(t, port.ClipboardSourceWebKit, orchestrator.selection.SourceEngine) | |
| 45 | - | require.Equal(t, port.WebViewID(42), orchestrator.selection.ViewID) | |
| 44 | + | require.Equal(t, dto.ClipboardSourceWebKit, orchestrator.selection.SourceEngine) | |
| 45 | + | require.Equal(t, uint64(42), orchestrator.selection.ViewID) | |
| 46 | 46 | } | |
| 47 | 47 | ||
| 48 | 48 | func TestClipboardHandler_HandleExplicitCopy_DelegatesToOrchestrator(t *testing.T) { | |
| @@ -57,7 +57,7 @@ func TestClipboardHandler_HandleExplicitCopy_DelegatesToOrchestrator(t *testing. | |||
| 57 | 57 | defer orchestrator.mu.Unlock() | |
| 58 | 58 | require.Equal(t, "copied text", orchestrator.explicit.Text) | |
| 59 | 59 | require.Equal(t, "copy", orchestrator.explicit.Action) | |
| 60 | - | require.Equal(t, port.ClipboardSourceWebKit, orchestrator.explicit.SourceEngine) | |
| 61 | - | require.Equal(t, port.WebViewID(42), orchestrator.explicit.ViewID) | |
| 60 | + | require.Equal(t, dto.ClipboardSourceWebKit, orchestrator.explicit.SourceEngine) | |
| 61 | + | require.Equal(t, uint64(42), orchestrator.explicit.ViewID) | |
| 62 | 62 | require.True(t, orchestrator.explicit.NativeHandled) | |
| 63 | 63 | } | |
| @@ -6,24 +6,24 @@ import ( | |||||
| 6 | "sync" | 6 | "sync" | ||
| 7 | "testing" | 7 | "testing" | ||
| 8 | 8 | ||||
| 9 | - | "github.com/bnema/dumber/internal/application/port" | 9 | + | "github.com/bnema/dumber/internal/application/dto" |
| 10 | "github.com/stretchr/testify/require" | 10 | "github.com/stretchr/testify/require" | ||
| 11 | ) | 11 | ) | ||
| 12 | 12 | ||||
| 13 | type recordingClipboardOrchestrator struct { | 13 | type recordingClipboardOrchestrator struct { | ||
| 14 | mu sync.Mutex | 14 | mu sync.Mutex | ||
| 15 | - | selection port.SelectionClipboardInput | 15 | + | selection dto.SelectionClipboardInput |
| 16 | - | explicit port.ExplicitClipboardInput | 16 | + | explicit dto.ExplicitClipboardInput |
| 17 | } | 17 | } | ||
| 18 | 18 | ||||
| 19 | - | func (r *recordingClipboardOrchestrator) HandleSelectionUpdate(_ context.Context, input port.SelectionClipboardInput) error { | 19 | + | func (r *recordingClipboardOrchestrator) HandleSelectionUpdate(_ context.Context, input dto.SelectionClipboardInput) error { |
| 20 | r.mu.Lock() | 20 | r.mu.Lock() | ||
| 21 | defer r.mu.Unlock() | 21 | defer r.mu.Unlock() | ||
| 22 | r.selection = input | 22 | r.selection = input | ||
| 23 | return nil | 23 | return nil | ||
| 24 | } | 24 | } | ||
| 25 | 25 | ||||
| 26 | - | func (r *recordingClipboardOrchestrator) HandleExplicitCopy(_ context.Context, input port.ExplicitClipboardInput) error { | 26 | + | func (r *recordingClipboardOrchestrator) HandleExplicitCopy(_ context.Context, input dto.ExplicitClipboardInput) error { |
| 27 | r.mu.Lock() | 27 | r.mu.Lock() | ||
| 28 | defer r.mu.Unlock() | 28 | defer r.mu.Unlock() | ||
| 29 | r.explicit = input | 29 | r.explicit = input | ||
| @@ -41,8 +41,8 @@ func TestClipboardHandler_HandleAutoCopySelection_DelegatesToOrchestrator(t *tes | |||||
| 41 | orchestrator.mu.Lock() | 41 | orchestrator.mu.Lock() | ||
| 42 | defer orchestrator.mu.Unlock() | 42 | defer orchestrator.mu.Unlock() | ||
| 43 | require.Equal(t, "selected text", orchestrator.selection.Text) | 43 | require.Equal(t, "selected text", orchestrator.selection.Text) | ||
| 44 | - | require.Equal(t, port.ClipboardSourceWebKit, orchestrator.selection.SourceEngine) | 44 | + | require.Equal(t, dto.ClipboardSourceWebKit, orchestrator.selection.SourceEngine) |
| 45 | - | require.Equal(t, port.WebViewID(42), orchestrator.selection.ViewID) | 45 | + | require.Equal(t, uint64(42), orchestrator.selection.ViewID) |
| 46 | } | 46 | } | ||
| 47 | 47 | ||||
| 48 | func TestClipboardHandler_HandleExplicitCopy_DelegatesToOrchestrator(t *testing.T) { | 48 | func TestClipboardHandler_HandleExplicitCopy_DelegatesToOrchestrator(t *testing.T) { | ||
| @@ -57,7 +57,7 @@ func TestClipboardHandler_HandleExplicitCopy_DelegatesToOrchestrator(t *testing. | |||||
| 57 | defer orchestrator.mu.Unlock() | 57 | defer orchestrator.mu.Unlock() | ||
| 58 | require.Equal(t, "copied text", orchestrator.explicit.Text) | 58 | require.Equal(t, "copied text", orchestrator.explicit.Text) | ||
| 59 | require.Equal(t, "copy", orchestrator.explicit.Action) | 59 | require.Equal(t, "copy", orchestrator.explicit.Action) | ||
| 60 | - | require.Equal(t, port.ClipboardSourceWebKit, orchestrator.explicit.SourceEngine) | 60 | + | require.Equal(t, dto.ClipboardSourceWebKit, orchestrator.explicit.SourceEngine) |
| 61 | - | require.Equal(t, port.WebViewID(42), orchestrator.explicit.ViewID) | 61 | + | require.Equal(t, uint64(42), orchestrator.explicit.ViewID) |
| 62 | require.True(t, orchestrator.explicit.NativeHandled) | 62 | require.True(t, orchestrator.explicit.NativeHandled) | ||
| 63 | } | 63 | } | ||
M internal/infrastructure/xdg/adapter.go +22 -7
| @@ -6,14 +6,29 @@ import ( | |||
| 6 | 6 | ||
| 7 | 7 | "github.com/bnema/dumber/internal/application/port" | |
| 8 | 8 | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 9 | + | "github.com/bnema/dumber/internal/infrastructure/runtimeprofile" | |
| 9 | 10 | ) | |
| 10 | 11 | ||
| 11 | 12 | // Adapter implements port.XDGPaths using config.GetXDGDirs(). | |
| 12 | - | type Adapter struct{} | |
| 13 | + | type Adapter struct { | |
| 14 | + | mode runtimeprofile.Mode | |
| 15 | + | runtimeDir string | |
| 16 | + | } | |
| 13 | 17 | ||
| 14 | 18 | // New creates a new XDG paths adapter. | |
| 15 | - | func New() *Adapter { | |
| 16 | - | return &Adapter{} | |
| 19 | + | func New(profile runtimeprofile.Profile) *Adapter { | |
| 20 | + | runtimeDir := "" | |
| 21 | + | switch profile.Mode { | |
| 22 | + | case runtimeprofile.ModeDev: | |
| 23 | + | if profile.Shared.RootDir != "" { | |
| 24 | + | runtimeDir = filepath.Join(profile.Shared.RootDir, "runtime") | |
| 25 | + | } | |
| 26 | + | case runtimeprofile.ModeProd: | |
| 27 | + | if profile.Shared.StateDir != "" { | |
| 28 | + | runtimeDir = filepath.Join(profile.Shared.StateDir, "runtime") | |
| 29 | + | } | |
| 30 | + | } | |
| 31 | + | return &Adapter{mode: profile.Mode, runtimeDir: runtimeDir} | |
| 17 | 32 | } | |
| 18 | 33 | ||
| 19 | 34 | func (a *Adapter) ConfigDir() (string, error) { | |
| @@ -29,19 +44,19 @@ func (a *Adapter) StateDir() (string, error) { | |||
| 29 | 44 | } | |
| 30 | 45 | ||
| 31 | 46 | func (a *Adapter) RuntimeDir() (string, error) { | |
| 32 | - | if os.Getenv("ENV") != "dev" { | |
| 47 | + | if a == nil || a.mode != runtimeprofile.ModeDev { | |
| 33 | 48 | if dir := os.Getenv("XDG_RUNTIME_DIR"); dir != "" { | |
| 34 | 49 | return dir, nil | |
| 35 | 50 | } | |
| 36 | 51 | } | |
| 52 | + | if a != nil && a.runtimeDir != "" { | |
| 53 | + | return a.runtimeDir, nil | |
| 54 | + | } | |
| 37 | 55 | ||
| 38 | 56 | stateDir, err := a.StateDir() | |
| 39 | 57 | if err != nil { | |
| 40 | 58 | return "", err | |
| 41 | 59 | } | |
| 42 | - | if os.Getenv("ENV") == "dev" { | |
| 43 | - | return filepath.Join(filepath.Dir(stateDir), "runtime"), nil | |
| 44 | - | } | |
| 45 | 60 | return filepath.Join(stateDir, "runtime"), nil | |
| 46 | 61 | } | |
| 47 | 62 | ||
| @@ -6,14 +6,29 @@ import ( | |||||
| 6 | 6 | ||||
| 7 | "github.com/bnema/dumber/internal/application/port" | 7 | "github.com/bnema/dumber/internal/application/port" | ||
| 8 | "github.com/bnema/dumber/internal/infrastructure/config" | 8 | "github.com/bnema/dumber/internal/infrastructure/config" | ||
| 9 | + | "github.com/bnema/dumber/internal/infrastructure/runtimeprofile" | |||
| 9 | ) | 10 | ) | ||
| 10 | 11 | ||||
| 11 | // Adapter implements port.XDGPaths using config.GetXDGDirs(). | 12 | // Adapter implements port.XDGPaths using config.GetXDGDirs(). | ||
| 12 | - | type Adapter struct{} | 13 | + | type Adapter struct { |
| 14 | + | mode runtimeprofile.Mode | |||
| 15 | + | runtimeDir string | |||
| 16 | + | } | |||
| 13 | 17 | ||||
| 14 | // New creates a new XDG paths adapter. | 18 | // New creates a new XDG paths adapter. | ||
| 15 | - | func New() *Adapter { | 19 | + | func New(profile runtimeprofile.Profile) *Adapter { |
| 16 | - | return &Adapter{} | 20 | + | runtimeDir := "" |
| 21 | + | switch profile.Mode { | |||
| 22 | + | case runtimeprofile.ModeDev: | |||
| 23 | + | if profile.Shared.RootDir != "" { | |||
| 24 | + | runtimeDir = filepath.Join(profile.Shared.RootDir, "runtime") | |||
| 25 | + | } | |||
| 26 | + | case runtimeprofile.ModeProd: | |||
| 27 | + | if profile.Shared.StateDir != "" { | |||
| 28 | + | runtimeDir = filepath.Join(profile.Shared.StateDir, "runtime") | |||
| 29 | + | } | |||
| 30 | + | } | |||
| 31 | + | return &Adapter{mode: profile.Mode, runtimeDir: runtimeDir} | |||
| 17 | } | 32 | } | ||
| 18 | 33 | ||||
| 19 | func (a *Adapter) ConfigDir() (string, error) { | 34 | func (a *Adapter) ConfigDir() (string, error) { | ||
| @@ -29,19 +44,19 @@ func (a *Adapter) StateDir() (string, error) { | |||||
| 29 | } | 44 | } | ||
| 30 | 45 | ||||
| 31 | func (a *Adapter) RuntimeDir() (string, error) { | 46 | func (a *Adapter) RuntimeDir() (string, error) { | ||
| 32 | - | if os.Getenv("ENV") != "dev" { | 47 | + | if a == nil || a.mode != runtimeprofile.ModeDev { |
| 33 | if dir := os.Getenv("XDG_RUNTIME_DIR"); dir != "" { | 48 | if dir := os.Getenv("XDG_RUNTIME_DIR"); dir != "" { | ||
| 34 | return dir, nil | 49 | return dir, nil | ||
| 35 | } | 50 | } | ||
| 36 | } | 51 | } | ||
| 52 | + | if a != nil && a.runtimeDir != "" { | |||
| 53 | + | return a.runtimeDir, nil | |||
| 54 | + | } | |||
| 37 | 55 | ||||
| 38 | stateDir, err := a.StateDir() | 56 | stateDir, err := a.StateDir() | ||
| 39 | if err != nil { | 57 | if err != nil { | ||
| 40 | return "", err | 58 | return "", err | ||
| 41 | } | 59 | } | ||
| 42 | - | if os.Getenv("ENV") == "dev" { | |||
| 43 | - | return filepath.Join(filepath.Dir(stateDir), "runtime"), nil | |||
| 44 | - | } | |||
| 45 | return filepath.Join(stateDir, "runtime"), nil | 60 | return filepath.Join(stateDir, "runtime"), nil | ||
| 46 | } | 61 | } | ||
| 47 | 62 | ||||
M internal/infrastructure/xdg/adapter_test.go +18 -17
| @@ -5,12 +5,13 @@ import ( | |||
| 5 | 5 | "path/filepath" | |
| 6 | 6 | "testing" | |
| 7 | 7 | ||
| 8 | + | "github.com/bnema/dumber/internal/infrastructure/runtimeprofile" | |
| 8 | 9 | "github.com/stretchr/testify/assert" | |
| 9 | 10 | "github.com/stretchr/testify/require" | |
| 10 | 11 | ) | |
| 11 | 12 | ||
| 12 | 13 | func TestAdapter_DownloadDir(t *testing.T) { | |
| 13 | - | adapter := New() | |
| 14 | + | adapter := New(runtimeprofile.Profile{}) | |
| 14 | 15 | ||
| 15 | 16 | t.Run("returns XDG_DOWNLOAD_DIR when set", func(t *testing.T) { | |
| 16 | 17 | t.Setenv("XDG_DOWNLOAD_DIR", "/custom/downloads") | |
| @@ -37,10 +38,8 @@ func TestAdapter_DownloadDir(t *testing.T) { | |||
| 37 | 38 | } | |
| 38 | 39 | ||
| 39 | 40 | func TestAdapter_RuntimeDir(t *testing.T) { | |
| 40 | - | adapter := New() | |
| 41 | - | ||
| 42 | 41 | t.Run("returns XDG_RUNTIME_DIR when set outside dev", func(t *testing.T) { | |
| 43 | - | t.Setenv("ENV", "") | |
| 42 | + | adapter := New(runtimeprofile.Profile{Mode: runtimeprofile.ModeProd}) | |
| 44 | 43 | t.Setenv("XDG_RUNTIME_DIR", "/custom/runtime") | |
| 45 | 44 | ||
| 46 | 45 | dir, err := adapter.RuntimeDir() | |
| @@ -49,10 +48,14 @@ func TestAdapter_RuntimeDir(t *testing.T) { | |||
| 49 | 48 | assert.Equal(t, "/custom/runtime", dir) | |
| 50 | 49 | }) | |
| 51 | 50 | ||
| 52 | - | t.Run("falls back to state runtime dir when XDG_RUNTIME_DIR not set", func(t *testing.T) { | |
| 53 | - | t.Setenv("ENV", "") | |
| 51 | + | t.Run("falls back to injected prod runtime dir when XDG_RUNTIME_DIR not set", func(t *testing.T) { | |
| 52 | + | adapter := New(runtimeprofile.Profile{ | |
| 53 | + | Mode: runtimeprofile.ModeProd, | |
| 54 | + | Shared: runtimeprofile.SharedPaths{ | |
| 55 | + | StateDir: "/tmp/dumber-state/dumber", | |
| 56 | + | }, | |
| 57 | + | }) | |
| 54 | 58 | t.Setenv("XDG_RUNTIME_DIR", "") | |
| 55 | - | t.Setenv("XDG_STATE_HOME", "/tmp/dumber-state") | |
| 56 | 59 | ||
| 57 | 60 | dir, err := adapter.RuntimeDir() | |
| 58 | 61 | ||
| @@ -60,16 +63,14 @@ func TestAdapter_RuntimeDir(t *testing.T) { | |||
| 60 | 63 | assert.Equal(t, "/tmp/dumber-state/dumber/runtime", dir) | |
| 61 | 64 | }) | |
| 62 | 65 | ||
| 63 | - | t.Run("uses sandbox runtime dir in dev even when XDG_RUNTIME_DIR is set", func(t *testing.T) { | |
| 66 | + | t.Run("uses injected sandbox runtime dir in dev even when XDG_RUNTIME_DIR is set", func(t *testing.T) { | |
| 64 | 67 | wd := t.TempDir() | |
| 65 | - | oldWD, err := os.Getwd() | |
| 66 | - | require.NoError(t, err) | |
| 67 | - | require.NoError(t, os.Chdir(wd)) | |
| 68 | - | defer func() { | |
| 69 | - | require.NoError(t, os.Chdir(oldWD)) | |
| 70 | - | }() | |
| 71 | - | ||
| 72 | - | t.Setenv("ENV", "dev") | |
| 68 | + | adapter := New(runtimeprofile.Profile{ | |
| 69 | + | Mode: runtimeprofile.ModeDev, | |
| 70 | + | Shared: runtimeprofile.SharedPaths{ | |
| 71 | + | RootDir: filepath.Join(wd, ".dev", "dumber"), | |
| 72 | + | }, | |
| 73 | + | }) | |
| 73 | 74 | t.Setenv("XDG_RUNTIME_DIR", "/shared/runtime") | |
| 74 | 75 | ||
| 75 | 76 | dir, err := adapter.RuntimeDir() | |
| @@ -80,7 +81,7 @@ func TestAdapter_RuntimeDir(t *testing.T) { | |||
| 80 | 81 | } | |
| 81 | 82 | ||
| 82 | 83 | func TestNew(t *testing.T) { | |
| 83 | - | adapter := New() | |
| 84 | + | adapter := New(runtimeprofile.Profile{}) | |
| 84 | 85 | ||
| 85 | 86 | assert.NotNil(t, adapter) | |
| 86 | 87 | } | |
| @@ -5,12 +5,13 @@ import ( | |||||
| 5 | "path/filepath" | 5 | "path/filepath" | ||
| 6 | "testing" | 6 | "testing" | ||
| 7 | 7 | ||||
| 8 | + | "github.com/bnema/dumber/internal/infrastructure/runtimeprofile" | |||
| 8 | "github.com/stretchr/testify/assert" | 9 | "github.com/stretchr/testify/assert" | ||
| 9 | "github.com/stretchr/testify/require" | 10 | "github.com/stretchr/testify/require" | ||
| 10 | ) | 11 | ) | ||
| 11 | 12 | ||||
| 12 | func TestAdapter_DownloadDir(t *testing.T) { | 13 | func TestAdapter_DownloadDir(t *testing.T) { | ||
| 13 | - | adapter := New() | 14 | + | adapter := New(runtimeprofile.Profile{}) |
| 14 | 15 | ||||
| 15 | t.Run("returns XDG_DOWNLOAD_DIR when set", func(t *testing.T) { | 16 | t.Run("returns XDG_DOWNLOAD_DIR when set", func(t *testing.T) { | ||
| 16 | t.Setenv("XDG_DOWNLOAD_DIR", "/custom/downloads") | 17 | t.Setenv("XDG_DOWNLOAD_DIR", "/custom/downloads") | ||
| @@ -37,10 +38,8 @@ func TestAdapter_DownloadDir(t *testing.T) { | |||||
| 37 | } | 38 | } | ||
| 38 | 39 | ||||
| 39 | func TestAdapter_RuntimeDir(t *testing.T) { | 40 | func TestAdapter_RuntimeDir(t *testing.T) { | ||
| 40 | - | adapter := New() | |||
| 41 | - | ||||
| 42 | t.Run("returns XDG_RUNTIME_DIR when set outside dev", func(t *testing.T) { | 41 | t.Run("returns XDG_RUNTIME_DIR when set outside dev", func(t *testing.T) { | ||
| 43 | - | t.Setenv("ENV", "") | 42 | + | adapter := New(runtimeprofile.Profile{Mode: runtimeprofile.ModeProd}) |
| 44 | t.Setenv("XDG_RUNTIME_DIR", "/custom/runtime") | 43 | t.Setenv("XDG_RUNTIME_DIR", "/custom/runtime") | ||
| 45 | 44 | ||||
| 46 | dir, err := adapter.RuntimeDir() | 45 | dir, err := adapter.RuntimeDir() | ||
| @@ -49,10 +48,14 @@ func TestAdapter_RuntimeDir(t *testing.T) { | |||||
| 49 | assert.Equal(t, "/custom/runtime", dir) | 48 | assert.Equal(t, "/custom/runtime", dir) | ||
| 50 | }) | 49 | }) | ||
| 51 | 50 | ||||
| 52 | - | t.Run("falls back to state runtime dir when XDG_RUNTIME_DIR not set", func(t *testing.T) { | 51 | + | t.Run("falls back to injected prod runtime dir when XDG_RUNTIME_DIR not set", func(t *testing.T) { |
| 53 | - | t.Setenv("ENV", "") | 52 | + | adapter := New(runtimeprofile.Profile{ |
| 53 | + | Mode: runtimeprofile.ModeProd, | |||
| 54 | + | Shared: runtimeprofile.SharedPaths{ | |||
| 55 | + | StateDir: "/tmp/dumber-state/dumber", | |||
| 56 | + | }, | |||
| 57 | + | }) | |||
| 54 | t.Setenv("XDG_RUNTIME_DIR", "") | 58 | t.Setenv("XDG_RUNTIME_DIR", "") | ||
| 55 | - | t.Setenv("XDG_STATE_HOME", "/tmp/dumber-state") | |||
| 56 | 59 | ||||
| 57 | dir, err := adapter.RuntimeDir() | 60 | dir, err := adapter.RuntimeDir() | ||
| 58 | 61 | ||||
| @@ -60,16 +63,14 @@ func TestAdapter_RuntimeDir(t *testing.T) { | |||||
| 60 | assert.Equal(t, "/tmp/dumber-state/dumber/runtime", dir) | 63 | assert.Equal(t, "/tmp/dumber-state/dumber/runtime", dir) | ||
| 61 | }) | 64 | }) | ||
| 62 | 65 | ||||
| 63 | - | t.Run("uses sandbox runtime dir in dev even when XDG_RUNTIME_DIR is set", func(t *testing.T) { | 66 | + | t.Run("uses injected sandbox runtime dir in dev even when XDG_RUNTIME_DIR is set", func(t *testing.T) { |
| 64 | wd := t.TempDir() | 67 | wd := t.TempDir() | ||
| 65 | - | oldWD, err := os.Getwd() | 68 | + | adapter := New(runtimeprofile.Profile{ |
| 66 | - | require.NoError(t, err) | 69 | + | Mode: runtimeprofile.ModeDev, |
| 67 | - | require.NoError(t, os.Chdir(wd)) | 70 | + | Shared: runtimeprofile.SharedPaths{ |
| 68 | - | defer func() { | 71 | + | RootDir: filepath.Join(wd, ".dev", "dumber"), |
| 69 | - | require.NoError(t, os.Chdir(oldWD)) | 72 | + | }, |
| 70 | - | }() | 73 | + | }) |
| 71 | - | ||||
| 72 | - | t.Setenv("ENV", "dev") | |||
| 73 | t.Setenv("XDG_RUNTIME_DIR", "/shared/runtime") | 74 | t.Setenv("XDG_RUNTIME_DIR", "/shared/runtime") | ||
| 74 | 75 | ||||
| 75 | dir, err := adapter.RuntimeDir() | 76 | dir, err := adapter.RuntimeDir() | ||
| @@ -80,7 +81,7 @@ func TestAdapter_RuntimeDir(t *testing.T) { | |||||
| 80 | } | 81 | } | ||
| 81 | 82 | ||||
| 82 | func TestNew(t *testing.T) { | 83 | func TestNew(t *testing.T) { | ||
| 83 | - | adapter := New() | 84 | + | adapter := New(runtimeprofile.Profile{}) |
| 84 | 85 | ||||
| 85 | assert.NotNil(t, adapter) | 86 | assert.NotNil(t, adapter) | ||
| 86 | } | 87 | } | ||
M internal/ui/component/progress_bar.go +11 -3
| @@ -157,7 +157,11 @@ func (pb *ProgressBar) startAnimation() { | |||
| 157 | 157 | }) | |
| 158 | 158 | ||
| 159 | 159 | pb.animationTimer = glib.TimeoutAdd(progressIntervalMs, &cb, 0) | |
| 160 | - | logging.FromContext(pb.ctx). | |
| 160 | + | ctx := pb.ctx | |
| 161 | + | if ctx == nil { | |
| 162 | + | ctx = context.Background() | |
| 163 | + | } | |
| 164 | + | logging.FromContext(ctx). | |
| 161 | 165 | Trace(). | |
| 162 | 166 | Uint64("progress_event_seq", pb.progressEventSeq). | |
| 163 | 167 | Float64("current_value", pb.currentValue). | |
| @@ -203,7 +207,7 @@ func (pb *ProgressBar) Show() { | |||
| 203 | 207 | // Reset timeout timer on every Show call | |
| 204 | 208 | pb.resetTimeout() | |
| 205 | 209 | ||
| 206 | - | logging.FromContext(pb.ctx). | |
| 210 | + | logging.FromContext(ctx). | |
| 207 | 211 | Debug(). | |
| 208 | 212 | Bool("visible", pb.visible). | |
| 209 | 213 | Float64("current_value", pb.currentValue). | |
| @@ -226,7 +230,11 @@ func (pb *ProgressBar) resetTimeout() { | |||
| 226 | 230 | defer pb.mu.Unlock() | |
| 227 | 231 | ||
| 228 | 232 | now := time.Now() | |
| 229 | - | logging.FromContext(pb.ctx). | |
| 233 | + | ctx := pb.ctx | |
| 234 | + | if ctx == nil { | |
| 235 | + | ctx = context.Background() | |
| 236 | + | } | |
| 237 | + | logging.FromContext(ctx). | |
| 230 | 238 | Debug(). | |
| 231 | 239 | Float64("current_value", pb.currentValue). | |
| 232 | 240 | Float64("target_value", pb.targetValue). | |
| @@ -157,7 +157,11 @@ func (pb *ProgressBar) startAnimation() { | |||||
| 157 | }) | 157 | }) | ||
| 158 | 158 | ||||
| 159 | pb.animationTimer = glib.TimeoutAdd(progressIntervalMs, &cb, 0) | 159 | pb.animationTimer = glib.TimeoutAdd(progressIntervalMs, &cb, 0) | ||
| 160 | - | logging.FromContext(pb.ctx). | 160 | + | ctx := pb.ctx |
| 161 | + | if ctx == nil { | |||
| 162 | + | ctx = context.Background() | |||
| 163 | + | } | |||
| 164 | + | logging.FromContext(ctx). | |||
| 161 | Trace(). | 165 | Trace(). | ||
| 162 | Uint64("progress_event_seq", pb.progressEventSeq). | 166 | Uint64("progress_event_seq", pb.progressEventSeq). | ||
| 163 | Float64("current_value", pb.currentValue). | 167 | Float64("current_value", pb.currentValue). | ||
| @@ -203,7 +207,7 @@ func (pb *ProgressBar) Show() { | |||||
| 203 | // Reset timeout timer on every Show call | 207 | // Reset timeout timer on every Show call | ||
| 204 | pb.resetTimeout() | 208 | pb.resetTimeout() | ||
| 205 | 209 | ||||
| 206 | - | logging.FromContext(pb.ctx). | 210 | + | logging.FromContext(ctx). |
| 207 | Debug(). | 211 | Debug(). | ||
| 208 | Bool("visible", pb.visible). | 212 | Bool("visible", pb.visible). | ||
| 209 | Float64("current_value", pb.currentValue). | 213 | Float64("current_value", pb.currentValue). | ||
| @@ -226,7 +230,11 @@ func (pb *ProgressBar) resetTimeout() { | |||||
| 226 | defer pb.mu.Unlock() | 230 | defer pb.mu.Unlock() | ||
| 227 | 231 | ||||
| 228 | now := time.Now() | 232 | now := time.Now() | ||
| 229 | - | logging.FromContext(pb.ctx). | 233 | + | ctx := pb.ctx |
| 234 | + | if ctx == nil { | |||
| 235 | + | ctx = context.Background() | |||
| 236 | + | } | |||
| 237 | + | logging.FromContext(ctx). | |||
| 230 | Debug(). | 238 | Debug(). | ||
| 231 | Float64("current_value", pb.currentValue). | 239 | Float64("current_value", pb.currentValue). | ||
| 232 | Float64("target_value", pb.targetValue). | 240 | Float64("target_value", pb.targetValue). | ||