detached 2064b93

refactor(tests): move remaining fake doubles to mockery

commit
2064b93dc06e80f6bda2fb20f2d73cf664cd585d
author
brice · · 2026-06-23 15:22 UTC
committer
brice · · 2026-06-23 20:01 UTC
parent
a605e6b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
29 files · +3146 -519
M .mockery.yaml +63 -0
@@ -2,6 +2,47 @@ template: testify
22 template-data:
33 unroll-variadic: true
44 packages:
5+ github.com/bnema/dumber/internal/bootstrap:
6+ config:
7+ dir: '{{.InterfaceDir}}/mocks'
8+ filename: '{{ .InterfaceFile | base }}'
9+ pkgname: mocks
10+ interfaces:
11+ configChangeManager:
12+ config:
13+ structname: MockConfigChangeManager
14+ github.com/bnema/dumber/internal/cli/cmd:
15+ config:
16+ dir: '{{.InterfaceDir}}/mocks'
17+ filename: '{{ .InterfaceFile | base }}'
18+ pkgname: mocks
19+ interfaces:
20+ sessionManager:
21+ config:
22+ structname: MockSessionManager
23+ github.com/bnema/dumber/internal/infrastructure/clipboard:
24+ config:
25+ dir: '{{.InterfaceDir}}/mocks'
26+ filename: '{{ .InterfaceFile | base }}'
27+ pkgname: mocks
28+ interfaces:
29+ toolkitClipboard:
30+ config:
31+ structname: MockToolkitClipboard
32+ github.com/bnema/dumber/internal/infrastructure/systemviewsbridge:
33+ config:
34+ dir: '{{.InterfaceDir}}/mocks'
35+ filename: '{{ .InterfaceFile | base }}'
36+ pkgname: mocks
37+ interfaces:
38+ Transport: {}
39+ github.com/bnema/purego-pipewire/pipewire:
40+ config:
41+ dir: internal/infrastructure/audio/pipewire/mocks
42+ filename: '{{ .InterfaceFile | base }}'
43+ pkgname: mocks
44+ interfaces:
45+ Player: {}
546 github.com/bnema/dumber/internal/application/port:
647 config:
748 dir: '{{.InterfaceDir}}/mocks'
@@ -58,6 +99,12 @@ packages:
5899 DesktopIntegration: {}
59100 DownloadEventHandler: {}
60101 DownloadPreparer: {}
102+ FaviconBlobStore: {}
103+ FaviconFetcher: {}
104+ FaviconImageConverter: {}
105+ FaviconInvalidators: {}
106+ FaviconRefreshScheduler: {}
107+ FaviconRepository: {}
61108 FaviconService: {}
62109 FileSystem: {}
63110 FilterManager: {}
@@ -134,3 +181,19 @@ packages:
134181 SpinnerWidget: {}
135182 Widget: {}
136183 WidgetFactory: {}
184+ github.com/bnema/dumber/internal/ui/coordinator:
185+ config:
186+ dir: '{{.InterfaceDir}}/mocks'
187+ filename: '{{ .InterfaceFile | base }}'
188+ pkgname: mocks
189+ interfaces:
190+ OmniboxProvider: {}
191+ github.com/bnema/dumber/internal/ui/dialog:
192+ config:
193+ dir: '{{.InterfaceDir}}/mocks'
194+ filename: '{{ .InterfaceFile | base }}'
195+ pkgname: mocks
196+ interfaces:
197+ permissionPopup:
198+ config:
199+ structname: MockPermissionPopup
M cmd/systemviews/bootstrap_test.go +45 -45
@@ -17,8 +17,8 @@ import (
1717 func TestNewBridgeApp_WiresHistoryService(t *testing.T) {
1818 t.Parallel()
1919
20- bridge := &fakeBridgeService{historyEntries: []*entity.HistoryEntry{{Title: "Example", URL: "https://example.com"}}}
21- app := newBridgeApp(&fakeDOM{}, "dumb://history", bridge)
20+ bridge := &bridgeServiceRecorder{historyEntries: []*entity.HistoryEntry{{Title: "Example", URL: "https://example.com"}}}
21+ app := newBridgeApp(&recordingDOM{}, "dumb://history", bridge)
2222
2323 require.NoError(t, app.LoadInitial(context.Background()))
2424 assert.True(t, bridge.calledHistory.Load())
@@ -30,7 +30,7 @@ func TestNewBridgeApp_WiresHistoryService(t *testing.T) {
3030 func TestNewBridgeApp_UsesCurrentConfigForNonConfigRoutes(t *testing.T) {
3131 t.Parallel()
3232
33- bridge := &fakeBridgeService{
33+ bridge := &bridgeServiceRecorder{
3434 historyEntries: []*entity.HistoryEntry{{Title: "Example", URL: "https://example.com"}},
3535 currentConfig: dto.SystemviewConfigPayload{
3636 Appearance: dto.WebUIAppearanceConfig{
@@ -47,7 +47,7 @@ func TestNewBridgeApp_UsesCurrentConfigForNonConfigRoutes(t *testing.T) {
4747 },
4848 },
4949 }
50- dom := &fakeDOM{mounts: make(chan string, 4)}
50+ dom := &recordingDOM{mounts: make(chan string, 4)}
5151 app := newBridgeApp(dom, "dumb://history", bridge)
5252
5353 require.NoError(t, app.Run())
@@ -62,12 +62,12 @@ func TestNewBridgeApp_UsesCurrentConfigForNonConfigRoutes(t *testing.T) {
6262 func TestNewBridgeApp_WiresFavoritesService(t *testing.T) {
6363 t.Parallel()
6464
65- bridge := &fakeBridgeService{
65+ bridge := &bridgeServiceRecorder{
6666 favorites: []*entity.Favorite{{Title: "Example", URL: "https://example.com"}},
6767 folders: []*entity.Folder{{Name: "Read Later"}},
6868 tags: []*entity.Tag{{Name: "Go"}},
6969 }
70- app := newBridgeApp(&fakeDOM{}, "dumb://favorites", bridge)
70+ app := newBridgeApp(&recordingDOM{}, "dumb://favorites", bridge)
7171
7272 require.NoError(t, app.LoadInitial(context.Background()))
7373 assert.False(t, bridge.calledHistory.Load())
@@ -79,11 +79,11 @@ func TestNewBridgeApp_WiresFavoritesService(t *testing.T) {
7979 func TestNewBridgeApp_WiresConfigService(t *testing.T) {
8080 t.Parallel()
8181
82- bridge := &fakeBridgeService{
82+ bridge := &bridgeServiceRecorder{
8383 currentConfig: dto.SystemviewConfigPayload{EngineType: "webkit"},
8484 keybindings: port.KeybindingsConfig{Groups: []port.KeybindingGroup{{DisplayName: "Default"}}},
8585 }
86- app := newBridgeApp(&fakeDOM{}, "dumb://config", bridge)
86+ app := newBridgeApp(&recordingDOM{}, "dumb://config", bridge)
8787
8888 require.NoError(t, app.LoadInitial(context.Background()))
8989 assert.False(t, bridge.calledHistory.Load())
@@ -92,12 +92,12 @@ func TestNewBridgeApp_WiresConfigService(t *testing.T) {
9292 assert.True(t, bridge.calledKeybindings.Load())
9393 }
9494
95-type fakeDOM struct {
95+type recordingDOM struct {
9696 html string
9797 mounts chan string
9898 }
9999
100-func (f *fakeDOM) Mount(html string) error {
100+func (f *recordingDOM) Mount(html string) error {
101101 f.html = html
102102 if f.mounts != nil {
103103 select {
@@ -131,9 +131,9 @@ func receiveMountContaining(t *testing.T, mounts <-chan string, values ...string
131131 }
132132 }
133133
134-// Handwritten fake intentionally tracks state across history, favorites, config,
134+// Recording fixture intentionally tracks state across history, favorites, config,
135135 // and keybindings boundaries for the composite bootstrap assertions.
136-type fakeBridgeService struct {
136+type bridgeServiceRecorder struct {
137137 calledHistory atomic.Bool
138138 calledFavorites atomic.Bool
139139 calledConfig atomic.Bool
@@ -147,112 +147,112 @@ type fakeBridgeService struct {
147147 keybindings port.KeybindingsConfig
148148 }
149149
150-func (f *fakeBridgeService) Timeline(context.Context, int, int) ([]*entity.HistoryEntry, error) {
150+func (f *bridgeServiceRecorder) Timeline(context.Context, int, int) ([]*entity.HistoryEntry, error) {
151151 f.calledHistory.Store(true)
152152 return f.historyEntries, nil
153153 }
154154
155-func (f *fakeBridgeService) TimelineByDomain(context.Context, string, int, int) ([]*entity.HistoryEntry, error) {
155+func (f *bridgeServiceRecorder) TimelineByDomain(context.Context, string, int, int) ([]*entity.HistoryEntry, error) {
156156 f.calledHistory.Store(true)
157157 return f.historyEntries, nil
158158 }
159159
160-func (f *fakeBridgeService) TimelineWindow(_ context.Context, before time.Time, _ string) (*entity.HistoryWindow, error) {
160+func (f *bridgeServiceRecorder) TimelineWindow(_ context.Context, before time.Time, _ string) (*entity.HistoryWindow, error) {
161161 f.calledHistory.Store(true)
162162 return &entity.HistoryWindow{Entries: f.historyEntries, Before: before, After: before.Add(-24 * time.Hour)}, nil
163163 }
164164
165-func (*fakeBridgeService) Search(context.Context, string, int) ([]*entity.HistoryEntry, error) {
165+func (*bridgeServiceRecorder) Search(context.Context, string, int) ([]*entity.HistoryEntry, error) {
166166 return nil, nil
167167 }
168168
169-func (*fakeBridgeService) DeleteEntry(context.Context, int64) error { return nil }
169+func (*bridgeServiceRecorder) DeleteEntry(context.Context, int64) error { return nil }
170170
171-func (*fakeBridgeService) DeleteRange(context.Context, string) error { return nil }
171+func (*bridgeServiceRecorder) DeleteRange(context.Context, string) error { return nil }
172172
173-func (*fakeBridgeService) Stats(context.Context) (*entity.HistoryStats, error) {
173+func (*bridgeServiceRecorder) Stats(context.Context) (*entity.HistoryStats, error) {
174174 return nil, nil
175175 }
176176
177-func (*fakeBridgeService) Analytics(context.Context) (*entity.HistoryAnalytics, error) {
177+func (*bridgeServiceRecorder) Analytics(context.Context) (*entity.HistoryAnalytics, error) {
178178 return nil, nil
179179 }
180180
181-func (*fakeBridgeService) DomainStats(context.Context, int) ([]*entity.DomainStat, error) {
181+func (*bridgeServiceRecorder) DomainStats(context.Context, int) ([]*entity.DomainStat, error) {
182182 return nil, nil
183183 }
184184
185-func (*fakeBridgeService) DeleteDomain(context.Context, string) error { return nil }
185+func (*bridgeServiceRecorder) DeleteDomain(context.Context, string) error { return nil }
186186
187-func (f *fakeBridgeService) List(context.Context) ([]*entity.Favorite, error) {
187+func (f *bridgeServiceRecorder) List(context.Context) ([]*entity.Favorite, error) {
188188 f.calledFavorites.Store(true)
189189 return f.favorites, nil
190190 }
191191
192-func (*fakeBridgeService) CreateFavorite(context.Context, dto.FavoriteCreateInput) (*entity.Favorite, error) {
192+func (*bridgeServiceRecorder) CreateFavorite(context.Context, dto.FavoriteCreateInput) (*entity.Favorite, error) {
193193 return nil, nil
194194 }
195195
196-func (*fakeBridgeService) UpdateFavorite(context.Context, dto.FavoriteUpdateInput) (*entity.Favorite, error) {
196+func (*bridgeServiceRecorder) UpdateFavorite(context.Context, dto.FavoriteUpdateInput) (*entity.Favorite, error) {
197197 return nil, nil
198198 }
199199
200-func (*fakeBridgeService) DeleteFavorite(context.Context, int64) error { return nil }
200+func (*bridgeServiceRecorder) DeleteFavorite(context.Context, int64) error { return nil }
201201
202-func (f *fakeBridgeService) ListFolders(context.Context) ([]*entity.Folder, error) {
202+func (f *bridgeServiceRecorder) ListFolders(context.Context) ([]*entity.Folder, error) {
203203 return f.folders, nil
204204 }
205205
206-func (f *fakeBridgeService) ListTags(context.Context) ([]*entity.Tag, error) {
206+func (f *bridgeServiceRecorder) ListTags(context.Context) ([]*entity.Tag, error) {
207207 return f.tags, nil
208208 }
209209
210-func (*fakeBridgeService) SetShortcut(context.Context, int64, *int) error { return nil }
210+func (*bridgeServiceRecorder) SetShortcut(context.Context, int64, *int) error { return nil }
211211
212-func (*fakeBridgeService) SetFolder(context.Context, int64, *int64) error { return nil }
212+func (*bridgeServiceRecorder) SetFolder(context.Context, int64, *int64) error { return nil }
213213
214-func (*fakeBridgeService) CreateFolder(context.Context, string, string, *int64) (*entity.Folder, error) {
214+func (*bridgeServiceRecorder) CreateFolder(context.Context, string, string, *int64) (*entity.Folder, error) {
215215 return nil, nil
216216 }
217217
218-func (*fakeBridgeService) UpdateFolder(context.Context, int64, string, string) error { return nil }
218+func (*bridgeServiceRecorder) UpdateFolder(context.Context, int64, string, string) error { return nil }
219219
220-func (*fakeBridgeService) DeleteFolder(context.Context, int64) error { return nil }
220+func (*bridgeServiceRecorder) DeleteFolder(context.Context, int64) error { return nil }
221221
222-func (*fakeBridgeService) CreateTag(context.Context, string, string) (*entity.Tag, error) {
222+func (*bridgeServiceRecorder) CreateTag(context.Context, string, string) (*entity.Tag, error) {
223223 return nil, nil
224224 }
225225
226-func (*fakeBridgeService) UpdateTag(context.Context, int64, string, string) error { return nil }
226+func (*bridgeServiceRecorder) UpdateTag(context.Context, int64, string, string) error { return nil }
227227
228-func (*fakeBridgeService) DeleteTag(context.Context, int64) error { return nil }
228+func (*bridgeServiceRecorder) DeleteTag(context.Context, int64) error { return nil }
229229
230-func (*fakeBridgeService) AssignTag(context.Context, int64, int64) error { return nil }
230+func (*bridgeServiceRecorder) AssignTag(context.Context, int64, int64) error { return nil }
231231
232-func (*fakeBridgeService) RemoveTag(context.Context, int64, int64) error { return nil }
232+func (*bridgeServiceRecorder) RemoveTag(context.Context, int64, int64) error { return nil }
233233
234-func (f *fakeBridgeService) Current(context.Context) (dto.SystemviewConfigPayload, error) {
234+func (f *bridgeServiceRecorder) Current(context.Context) (dto.SystemviewConfigPayload, error) {
235235 f.calledConfig.Store(true)
236236 return f.currentConfig, nil
237237 }
238238
239-func (*fakeBridgeService) Default(context.Context) (dto.SystemviewConfigPayload, error) {
239+func (*bridgeServiceRecorder) Default(context.Context) (dto.SystemviewConfigPayload, error) {
240240 return dto.SystemviewConfigPayload{}, nil
241241 }
242242
243-func (*fakeBridgeService) Save(context.Context, dto.WebUIConfig) error { return nil }
243+func (*bridgeServiceRecorder) Save(context.Context, dto.WebUIConfig) error { return nil }
244244
245-func (f *fakeBridgeService) GetKeybindings(context.Context) (port.KeybindingsConfig, error) {
245+func (f *bridgeServiceRecorder) GetKeybindings(context.Context) (port.KeybindingsConfig, error) {
246246 f.calledKeybindings.Store(true)
247247 return f.keybindings, nil
248248 }
249249
250-func (*fakeBridgeService) SetKeybinding(context.Context, port.SetKeybindingRequest) (port.SetKeybindingResponse, error) {
250+func (*bridgeServiceRecorder) SetKeybinding(context.Context, port.SetKeybindingRequest) (port.SetKeybindingResponse, error) {
251251 return port.SetKeybindingResponse{}, nil
252252 }
253253
254-func (*fakeBridgeService) ResetKeybinding(context.Context, port.ResetKeybindingRequest) error {
254+func (*bridgeServiceRecorder) ResetKeybinding(context.Context, port.ResetKeybindingRequest) error {
255255 return nil
256256 }
257257
258-func (*fakeBridgeService) ResetAllKeybindings(context.Context) error { return nil }
258+func (*bridgeServiceRecorder) ResetAllKeybindings(context.Context) error { return nil }
A internal/application/port/mocks/favicon_blob_store.go +525 -0
@@ -0,0 +1,525 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "github.com/bnema/dumber/internal/domain/favicon"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewMockFaviconBlobStore creates a new instance of MockFaviconBlobStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewMockFaviconBlobStore(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *MockFaviconBlobStore {
20+ mock := &MockFaviconBlobStore{}
21+ mock.Mock.Test(t)
22+
23+ t.Cleanup(func() { mock.AssertExpectations(t) })
24+
25+ return mock
26+}
27+
28+// MockFaviconBlobStore is an autogenerated mock type for the FaviconBlobStore type
29+type MockFaviconBlobStore struct {
30+ mock.Mock
31+}
32+
33+type MockFaviconBlobStore_Expecter struct {
34+ mock *mock.Mock
35+}
36+
37+func (_m *MockFaviconBlobStore) EXPECT() *MockFaviconBlobStore_Expecter {
38+ return &MockFaviconBlobStore_Expecter{mock: &_m.Mock}
39+}
40+
41+// ReadOriginal provides a mock function for the type MockFaviconBlobStore
42+func (_mock *MockFaviconBlobStore) ReadOriginal(ctx context.Context, key favicon.Key) ([]byte, string, error) {
43+ ret := _mock.Called(ctx, key)
44+
45+ if len(ret) == 0 {
46+ panic("no return value specified for ReadOriginal")
47+ }
48+
49+ var r0 []byte
50+ var r1 string
51+ var r2 error
52+ if returnFunc, ok := ret.Get(0).(func(context.Context, favicon.Key) ([]byte, string, error)); ok {
53+ return returnFunc(ctx, key)
54+ }
55+ if returnFunc, ok := ret.Get(0).(func(context.Context, favicon.Key) []byte); ok {
56+ r0 = returnFunc(ctx, key)
57+ } else {
58+ if ret.Get(0) != nil {
59+ r0 = ret.Get(0).([]byte)
60+ }
61+ }
62+ if returnFunc, ok := ret.Get(1).(func(context.Context, favicon.Key) string); ok {
63+ r1 = returnFunc(ctx, key)
64+ } else {
65+ r1 = ret.Get(1).(string)
66+ }
67+ if returnFunc, ok := ret.Get(2).(func(context.Context, favicon.Key) error); ok {
68+ r2 = returnFunc(ctx, key)
69+ } else {
70+ r2 = ret.Error(2)
71+ }
72+ return r0, r1, r2
73+}
74+
75+// MockFaviconBlobStore_ReadOriginal_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadOriginal'
76+type MockFaviconBlobStore_ReadOriginal_Call struct {
77+ *mock.Call
78+}
79+
80+// ReadOriginal is a helper method to define mock.On call
81+// - ctx context.Context
82+// - key favicon.Key
83+func (_e *MockFaviconBlobStore_Expecter) ReadOriginal(ctx any, key any) *MockFaviconBlobStore_ReadOriginal_Call {
84+ return &MockFaviconBlobStore_ReadOriginal_Call{Call: _e.mock.On("ReadOriginal", ctx, key)}
85+}
86+
87+func (_c *MockFaviconBlobStore_ReadOriginal_Call) Run(run func(ctx context.Context, key favicon.Key)) *MockFaviconBlobStore_ReadOriginal_Call {
88+ _c.Call.Run(func(args mock.Arguments) {
89+ var arg0 context.Context
90+ if args[0] != nil {
91+ arg0 = args[0].(context.Context)
92+ }
93+ var arg1 favicon.Key
94+ if args[1] != nil {
95+ arg1 = args[1].(favicon.Key)
96+ }
97+ run(
98+ arg0,
99+ arg1,
100+ )
101+ })
102+ return _c
103+}
104+
105+func (_c *MockFaviconBlobStore_ReadOriginal_Call) Return(bytes []byte, s string, err error) *MockFaviconBlobStore_ReadOriginal_Call {
106+ _c.Call.Return(bytes, s, err)
107+ return _c
108+}
109+
110+func (_c *MockFaviconBlobStore_ReadOriginal_Call) RunAndReturn(run func(ctx context.Context, key favicon.Key) ([]byte, string, error)) *MockFaviconBlobStore_ReadOriginal_Call {
111+ _c.Call.Return(run)
112+ return _c
113+}
114+
115+// ReadPNG provides a mock function for the type MockFaviconBlobStore
116+func (_mock *MockFaviconBlobStore) ReadPNG(ctx context.Context, key favicon.Key) ([]byte, string, error) {
117+ ret := _mock.Called(ctx, key)
118+
119+ if len(ret) == 0 {
120+ panic("no return value specified for ReadPNG")
121+ }
122+
123+ var r0 []byte
124+ var r1 string
125+ var r2 error
126+ if returnFunc, ok := ret.Get(0).(func(context.Context, favicon.Key) ([]byte, string, error)); ok {
127+ return returnFunc(ctx, key)
128+ }
129+ if returnFunc, ok := ret.Get(0).(func(context.Context, favicon.Key) []byte); ok {
130+ r0 = returnFunc(ctx, key)
131+ } else {
132+ if ret.Get(0) != nil {
133+ r0 = ret.Get(0).([]byte)
134+ }
135+ }
136+ if returnFunc, ok := ret.Get(1).(func(context.Context, favicon.Key) string); ok {
137+ r1 = returnFunc(ctx, key)
138+ } else {
139+ r1 = ret.Get(1).(string)
140+ }
141+ if returnFunc, ok := ret.Get(2).(func(context.Context, favicon.Key) error); ok {
142+ r2 = returnFunc(ctx, key)
143+ } else {
144+ r2 = ret.Error(2)
145+ }
146+ return r0, r1, r2
147+}
148+
149+// MockFaviconBlobStore_ReadPNG_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadPNG'
150+type MockFaviconBlobStore_ReadPNG_Call struct {
151+ *mock.Call
152+}
153+
154+// ReadPNG is a helper method to define mock.On call
155+// - ctx context.Context
156+// - key favicon.Key
157+func (_e *MockFaviconBlobStore_Expecter) ReadPNG(ctx any, key any) *MockFaviconBlobStore_ReadPNG_Call {
158+ return &MockFaviconBlobStore_ReadPNG_Call{Call: _e.mock.On("ReadPNG", ctx, key)}
159+}
160+
161+func (_c *MockFaviconBlobStore_ReadPNG_Call) Run(run func(ctx context.Context, key favicon.Key)) *MockFaviconBlobStore_ReadPNG_Call {
162+ _c.Call.Run(func(args mock.Arguments) {
163+ var arg0 context.Context
164+ if args[0] != nil {
165+ arg0 = args[0].(context.Context)
166+ }
167+ var arg1 favicon.Key
168+ if args[1] != nil {
169+ arg1 = args[1].(favicon.Key)
170+ }
171+ run(
172+ arg0,
173+ arg1,
174+ )
175+ })
176+ return _c
177+}
178+
179+func (_c *MockFaviconBlobStore_ReadPNG_Call) Return(bytes []byte, s string, err error) *MockFaviconBlobStore_ReadPNG_Call {
180+ _c.Call.Return(bytes, s, err)
181+ return _c
182+}
183+
184+func (_c *MockFaviconBlobStore_ReadPNG_Call) RunAndReturn(run func(ctx context.Context, key favicon.Key) ([]byte, string, error)) *MockFaviconBlobStore_ReadPNG_Call {
185+ _c.Call.Return(run)
186+ return _c
187+}
188+
189+// ReadSizedPNG provides a mock function for the type MockFaviconBlobStore
190+func (_mock *MockFaviconBlobStore) ReadSizedPNG(ctx context.Context, key favicon.Key, size int) ([]byte, string, error) {
191+ ret := _mock.Called(ctx, key, size)
192+
193+ if len(ret) == 0 {
194+ panic("no return value specified for ReadSizedPNG")
195+ }
196+
197+ var r0 []byte
198+ var r1 string
199+ var r2 error
200+ if returnFunc, ok := ret.Get(0).(func(context.Context, favicon.Key, int) ([]byte, string, error)); ok {
201+ return returnFunc(ctx, key, size)
202+ }
203+ if returnFunc, ok := ret.Get(0).(func(context.Context, favicon.Key, int) []byte); ok {
204+ r0 = returnFunc(ctx, key, size)
205+ } else {
206+ if ret.Get(0) != nil {
207+ r0 = ret.Get(0).([]byte)
208+ }
209+ }
210+ if returnFunc, ok := ret.Get(1).(func(context.Context, favicon.Key, int) string); ok {
211+ r1 = returnFunc(ctx, key, size)
212+ } else {
213+ r1 = ret.Get(1).(string)
214+ }
215+ if returnFunc, ok := ret.Get(2).(func(context.Context, favicon.Key, int) error); ok {
216+ r2 = returnFunc(ctx, key, size)
217+ } else {
218+ r2 = ret.Error(2)
219+ }
220+ return r0, r1, r2
221+}
222+
223+// MockFaviconBlobStore_ReadSizedPNG_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadSizedPNG'
224+type MockFaviconBlobStore_ReadSizedPNG_Call struct {
225+ *mock.Call
226+}
227+
228+// ReadSizedPNG is a helper method to define mock.On call
229+// - ctx context.Context
230+// - key favicon.Key
231+// - size int
232+func (_e *MockFaviconBlobStore_Expecter) ReadSizedPNG(ctx any, key any, size any) *MockFaviconBlobStore_ReadSizedPNG_Call {
233+ return &MockFaviconBlobStore_ReadSizedPNG_Call{Call: _e.mock.On("ReadSizedPNG", ctx, key, size)}
234+}
235+
236+func (_c *MockFaviconBlobStore_ReadSizedPNG_Call) Run(run func(ctx context.Context, key favicon.Key, size int)) *MockFaviconBlobStore_ReadSizedPNG_Call {
237+ _c.Call.Run(func(args mock.Arguments) {
238+ var arg0 context.Context
239+ if args[0] != nil {
240+ arg0 = args[0].(context.Context)
241+ }
242+ var arg1 favicon.Key
243+ if args[1] != nil {
244+ arg1 = args[1].(favicon.Key)
245+ }
246+ var arg2 int
247+ if args[2] != nil {
248+ arg2 = args[2].(int)
249+ }
250+ run(
251+ arg0,
252+ arg1,
253+ arg2,
254+ )
255+ })
256+ return _c
257+}
258+
259+func (_c *MockFaviconBlobStore_ReadSizedPNG_Call) Return(bytes []byte, s string, err error) *MockFaviconBlobStore_ReadSizedPNG_Call {
260+ _c.Call.Return(bytes, s, err)
261+ return _c
262+}
263+
264+func (_c *MockFaviconBlobStore_ReadSizedPNG_Call) RunAndReturn(run func(ctx context.Context, key favicon.Key, size int) ([]byte, string, error)) *MockFaviconBlobStore_ReadSizedPNG_Call {
265+ _c.Call.Return(run)
266+ return _c
267+}
268+
269+// RemoveDerived provides a mock function for the type MockFaviconBlobStore
270+func (_mock *MockFaviconBlobStore) RemoveDerived(ctx context.Context, key favicon.Key) error {
271+ ret := _mock.Called(ctx, key)
272+
273+ if len(ret) == 0 {
274+ panic("no return value specified for RemoveDerived")
275+ }
276+
277+ var r0 error
278+ if returnFunc, ok := ret.Get(0).(func(context.Context, favicon.Key) error); ok {
279+ r0 = returnFunc(ctx, key)
280+ } else {
281+ r0 = ret.Error(0)
282+ }
283+ return r0
284+}
285+
286+// MockFaviconBlobStore_RemoveDerived_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveDerived'
287+type MockFaviconBlobStore_RemoveDerived_Call struct {
288+ *mock.Call
289+}
290+
291+// RemoveDerived is a helper method to define mock.On call
292+// - ctx context.Context
293+// - key favicon.Key
294+func (_e *MockFaviconBlobStore_Expecter) RemoveDerived(ctx any, key any) *MockFaviconBlobStore_RemoveDerived_Call {
295+ return &MockFaviconBlobStore_RemoveDerived_Call{Call: _e.mock.On("RemoveDerived", ctx, key)}
296+}
297+
298+func (_c *MockFaviconBlobStore_RemoveDerived_Call) Run(run func(ctx context.Context, key favicon.Key)) *MockFaviconBlobStore_RemoveDerived_Call {
299+ _c.Call.Run(func(args mock.Arguments) {
300+ var arg0 context.Context
301+ if args[0] != nil {
302+ arg0 = args[0].(context.Context)
303+ }
304+ var arg1 favicon.Key
305+ if args[1] != nil {
306+ arg1 = args[1].(favicon.Key)
307+ }
308+ run(
309+ arg0,
310+ arg1,
311+ )
312+ })
313+ return _c
314+}
315+
316+func (_c *MockFaviconBlobStore_RemoveDerived_Call) Return(err error) *MockFaviconBlobStore_RemoveDerived_Call {
317+ _c.Call.Return(err)
318+ return _c
319+}
320+
321+func (_c *MockFaviconBlobStore_RemoveDerived_Call) RunAndReturn(run func(ctx context.Context, key favicon.Key) error) *MockFaviconBlobStore_RemoveDerived_Call {
322+ _c.Call.Return(run)
323+ return _c
324+}
325+
326+// WriteOriginal provides a mock function for the type MockFaviconBlobStore
327+func (_mock *MockFaviconBlobStore) WriteOriginal(ctx context.Context, key favicon.Key, data []byte, contentType string) error {
328+ ret := _mock.Called(ctx, key, data, contentType)
329+
330+ if len(ret) == 0 {
331+ panic("no return value specified for WriteOriginal")
332+ }
333+
334+ var r0 error
335+ if returnFunc, ok := ret.Get(0).(func(context.Context, favicon.Key, []byte, string) error); ok {
336+ r0 = returnFunc(ctx, key, data, contentType)
337+ } else {
338+ r0 = ret.Error(0)
339+ }
340+ return r0
341+}
342+
343+// MockFaviconBlobStore_WriteOriginal_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteOriginal'
344+type MockFaviconBlobStore_WriteOriginal_Call struct {
345+ *mock.Call
346+}
347+
348+// WriteOriginal is a helper method to define mock.On call
349+// - ctx context.Context
350+// - key favicon.Key
351+// - data []byte
352+// - contentType string
353+func (_e *MockFaviconBlobStore_Expecter) WriteOriginal(ctx any, key any, data any, contentType any) *MockFaviconBlobStore_WriteOriginal_Call {
354+ return &MockFaviconBlobStore_WriteOriginal_Call{Call: _e.mock.On("WriteOriginal", ctx, key, data, contentType)}
355+}
356+
357+func (_c *MockFaviconBlobStore_WriteOriginal_Call) Run(run func(ctx context.Context, key favicon.Key, data []byte, contentType string)) *MockFaviconBlobStore_WriteOriginal_Call {
358+ _c.Call.Run(func(args mock.Arguments) {
359+ var arg0 context.Context
360+ if args[0] != nil {
361+ arg0 = args[0].(context.Context)
362+ }
363+ var arg1 favicon.Key
364+ if args[1] != nil {
365+ arg1 = args[1].(favicon.Key)
366+ }
367+ var arg2 []byte
368+ if args[2] != nil {
369+ arg2 = args[2].([]byte)
370+ }
371+ var arg3 string
372+ if args[3] != nil {
373+ arg3 = args[3].(string)
374+ }
375+ run(
376+ arg0,
377+ arg1,
378+ arg2,
379+ arg3,
380+ )
381+ })
382+ return _c
383+}
384+
385+func (_c *MockFaviconBlobStore_WriteOriginal_Call) Return(err error) *MockFaviconBlobStore_WriteOriginal_Call {
386+ _c.Call.Return(err)
387+ return _c
388+}
389+
390+func (_c *MockFaviconBlobStore_WriteOriginal_Call) RunAndReturn(run func(ctx context.Context, key favicon.Key, data []byte, contentType string) error) *MockFaviconBlobStore_WriteOriginal_Call {
391+ _c.Call.Return(run)
392+ return _c
393+}
394+
395+// WritePNG provides a mock function for the type MockFaviconBlobStore
396+func (_mock *MockFaviconBlobStore) WritePNG(ctx context.Context, key favicon.Key, data []byte) error {
397+ ret := _mock.Called(ctx, key, data)
398+
399+ if len(ret) == 0 {
400+ panic("no return value specified for WritePNG")
401+ }
402+
403+ var r0 error
404+ if returnFunc, ok := ret.Get(0).(func(context.Context, favicon.Key, []byte) error); ok {
405+ r0 = returnFunc(ctx, key, data)
406+ } else {
407+ r0 = ret.Error(0)
408+ }
409+ return r0
410+}
411+
412+// MockFaviconBlobStore_WritePNG_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WritePNG'
413+type MockFaviconBlobStore_WritePNG_Call struct {
414+ *mock.Call
415+}
416+
417+// WritePNG is a helper method to define mock.On call
418+// - ctx context.Context
419+// - key favicon.Key
420+// - data []byte
421+func (_e *MockFaviconBlobStore_Expecter) WritePNG(ctx any, key any, data any) *MockFaviconBlobStore_WritePNG_Call {
422+ return &MockFaviconBlobStore_WritePNG_Call{Call: _e.mock.On("WritePNG", ctx, key, data)}
423+}
424+
425+func (_c *MockFaviconBlobStore_WritePNG_Call) Run(run func(ctx context.Context, key favicon.Key, data []byte)) *MockFaviconBlobStore_WritePNG_Call {
426+ _c.Call.Run(func(args mock.Arguments) {
427+ var arg0 context.Context
428+ if args[0] != nil {
429+ arg0 = args[0].(context.Context)
430+ }
431+ var arg1 favicon.Key
432+ if args[1] != nil {
433+ arg1 = args[1].(favicon.Key)
434+ }
435+ var arg2 []byte
436+ if args[2] != nil {
437+ arg2 = args[2].([]byte)
438+ }
439+ run(
440+ arg0,
441+ arg1,
442+ arg2,
443+ )
444+ })
445+ return _c
446+}
447+
448+func (_c *MockFaviconBlobStore_WritePNG_Call) Return(err error) *MockFaviconBlobStore_WritePNG_Call {
449+ _c.Call.Return(err)
450+ return _c
451+}
452+
453+func (_c *MockFaviconBlobStore_WritePNG_Call) RunAndReturn(run func(ctx context.Context, key favicon.Key, data []byte) error) *MockFaviconBlobStore_WritePNG_Call {
454+ _c.Call.Return(run)
455+ return _c
456+}
457+
458+// WriteSizedPNG provides a mock function for the type MockFaviconBlobStore
459+func (_mock *MockFaviconBlobStore) WriteSizedPNG(ctx context.Context, key favicon.Key, size int, data []byte) error {
460+ ret := _mock.Called(ctx, key, size, data)
461+
462+ if len(ret) == 0 {
463+ panic("no return value specified for WriteSizedPNG")
464+ }
465+
466+ var r0 error
467+ if returnFunc, ok := ret.Get(0).(func(context.Context, favicon.Key, int, []byte) error); ok {
468+ r0 = returnFunc(ctx, key, size, data)
469+ } else {
470+ r0 = ret.Error(0)
471+ }
472+ return r0
473+}
474+
475+// MockFaviconBlobStore_WriteSizedPNG_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteSizedPNG'
476+type MockFaviconBlobStore_WriteSizedPNG_Call struct {
477+ *mock.Call
478+}
479+
480+// WriteSizedPNG is a helper method to define mock.On call
481+// - ctx context.Context
482+// - key favicon.Key
483+// - size int
484+// - data []byte
485+func (_e *MockFaviconBlobStore_Expecter) WriteSizedPNG(ctx any, key any, size any, data any) *MockFaviconBlobStore_WriteSizedPNG_Call {
486+ return &MockFaviconBlobStore_WriteSizedPNG_Call{Call: _e.mock.On("WriteSizedPNG", ctx, key, size, data)}
487+}
488+
489+func (_c *MockFaviconBlobStore_WriteSizedPNG_Call) Run(run func(ctx context.Context, key favicon.Key, size int, data []byte)) *MockFaviconBlobStore_WriteSizedPNG_Call {
490+ _c.Call.Run(func(args mock.Arguments) {
491+ var arg0 context.Context
492+ if args[0] != nil {
493+ arg0 = args[0].(context.Context)
494+ }
495+ var arg1 favicon.Key
496+ if args[1] != nil {
497+ arg1 = args[1].(favicon.Key)
498+ }
499+ var arg2 int
500+ if args[2] != nil {
501+ arg2 = args[2].(int)
502+ }
503+ var arg3 []byte
504+ if args[3] != nil {
505+ arg3 = args[3].([]byte)
506+ }
507+ run(
508+ arg0,
509+ arg1,
510+ arg2,
511+ arg3,
512+ )
513+ })
514+ return _c
515+}
516+
517+func (_c *MockFaviconBlobStore_WriteSizedPNG_Call) Return(err error) *MockFaviconBlobStore_WriteSizedPNG_Call {
518+ _c.Call.Return(err)
519+ return _c
520+}
521+
522+func (_c *MockFaviconBlobStore_WriteSizedPNG_Call) RunAndReturn(run func(ctx context.Context, key favicon.Key, size int, data []byte) error) *MockFaviconBlobStore_WriteSizedPNG_Call {
523+ _c.Call.Return(run)
524+ return _c
525+}
A internal/application/port/mocks/favicon_converter.go +119 -0
@@ -0,0 +1,119 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "github.com/bnema/dumber/internal/application/port"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewMockFaviconImageConverter creates a new instance of MockFaviconImageConverter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewMockFaviconImageConverter(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *MockFaviconImageConverter {
20+ mock := &MockFaviconImageConverter{}
21+ mock.Mock.Test(t)
22+
23+ t.Cleanup(func() { mock.AssertExpectations(t) })
24+
25+ return mock
26+}
27+
28+// MockFaviconImageConverter is an autogenerated mock type for the FaviconImageConverter type
29+type MockFaviconImageConverter struct {
30+ mock.Mock
31+}
32+
33+type MockFaviconImageConverter_Expecter struct {
34+ mock *mock.Mock
35+}
36+
37+func (_m *MockFaviconImageConverter) EXPECT() *MockFaviconImageConverter_Expecter {
38+ return &MockFaviconImageConverter_Expecter{mock: &_m.Mock}
39+}
40+
41+// Convert provides a mock function for the type MockFaviconImageConverter
42+func (_mock *MockFaviconImageConverter) Convert(ctx context.Context, original []byte, contentType string, sizes []int) (*port.ConvertedFavicon, error) {
43+ ret := _mock.Called(ctx, original, contentType, sizes)
44+
45+ if len(ret) == 0 {
46+ panic("no return value specified for Convert")
47+ }
48+
49+ var r0 *port.ConvertedFavicon
50+ var r1 error
51+ if returnFunc, ok := ret.Get(0).(func(context.Context, []byte, string, []int) (*port.ConvertedFavicon, error)); ok {
52+ return returnFunc(ctx, original, contentType, sizes)
53+ }
54+ if returnFunc, ok := ret.Get(0).(func(context.Context, []byte, string, []int) *port.ConvertedFavicon); ok {
55+ r0 = returnFunc(ctx, original, contentType, sizes)
56+ } else {
57+ if ret.Get(0) != nil {
58+ r0 = ret.Get(0).(*port.ConvertedFavicon)
59+ }
60+ }
61+ if returnFunc, ok := ret.Get(1).(func(context.Context, []byte, string, []int) error); ok {
62+ r1 = returnFunc(ctx, original, contentType, sizes)
63+ } else {
64+ r1 = ret.Error(1)
65+ }
66+ return r0, r1
67+}
68+
69+// MockFaviconImageConverter_Convert_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Convert'
70+type MockFaviconImageConverter_Convert_Call struct {
71+ *mock.Call
72+}
73+
74+// Convert is a helper method to define mock.On call
75+// - ctx context.Context
76+// - original []byte
77+// - contentType string
78+// - sizes []int
79+func (_e *MockFaviconImageConverter_Expecter) Convert(ctx any, original any, contentType any, sizes any) *MockFaviconImageConverter_Convert_Call {
80+ return &MockFaviconImageConverter_Convert_Call{Call: _e.mock.On("Convert", ctx, original, contentType, sizes)}
81+}
82+
83+func (_c *MockFaviconImageConverter_Convert_Call) Run(run func(ctx context.Context, original []byte, contentType string, sizes []int)) *MockFaviconImageConverter_Convert_Call {
84+ _c.Call.Run(func(args mock.Arguments) {
85+ var arg0 context.Context
86+ if args[0] != nil {
87+ arg0 = args[0].(context.Context)
88+ }
89+ var arg1 []byte
90+ if args[1] != nil {
91+ arg1 = args[1].([]byte)
92+ }
93+ var arg2 string
94+ if args[2] != nil {
95+ arg2 = args[2].(string)
96+ }
97+ var arg3 []int
98+ if args[3] != nil {
99+ arg3 = args[3].([]int)
100+ }
101+ run(
102+ arg0,
103+ arg1,
104+ arg2,
105+ arg3,
106+ )
107+ })
108+ return _c
109+}
110+
111+func (_c *MockFaviconImageConverter_Convert_Call) Return(convertedFavicon *port.ConvertedFavicon, err error) *MockFaviconImageConverter_Convert_Call {
112+ _c.Call.Return(convertedFavicon, err)
113+ return _c
114+}
115+
116+func (_c *MockFaviconImageConverter_Convert_Call) RunAndReturn(run func(ctx context.Context, original []byte, contentType string, sizes []int) (*port.ConvertedFavicon, error)) *MockFaviconImageConverter_Convert_Call {
117+ _c.Call.Return(run)
118+ return _c
119+}
A internal/application/port/mocks/favicon_fetcher.go +107 -0
@@ -0,0 +1,107 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "github.com/bnema/dumber/internal/application/port"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewMockFaviconFetcher creates a new instance of MockFaviconFetcher. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewMockFaviconFetcher(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *MockFaviconFetcher {
20+ mock := &MockFaviconFetcher{}
21+ mock.Mock.Test(t)
22+
23+ t.Cleanup(func() { mock.AssertExpectations(t) })
24+
25+ return mock
26+}
27+
28+// MockFaviconFetcher is an autogenerated mock type for the FaviconFetcher type
29+type MockFaviconFetcher struct {
30+ mock.Mock
31+}
32+
33+type MockFaviconFetcher_Expecter struct {
34+ mock *mock.Mock
35+}
36+
37+func (_m *MockFaviconFetcher) EXPECT() *MockFaviconFetcher_Expecter {
38+ return &MockFaviconFetcher_Expecter{mock: &_m.Mock}
39+}
40+
41+// Fetch provides a mock function for the type MockFaviconFetcher
42+func (_mock *MockFaviconFetcher) Fetch(ctx context.Context, request port.FaviconFetchRequest) (*port.FaviconFetchedIcon, error) {
43+ ret := _mock.Called(ctx, request)
44+
45+ if len(ret) == 0 {
46+ panic("no return value specified for Fetch")
47+ }
48+
49+ var r0 *port.FaviconFetchedIcon
50+ var r1 error
51+ if returnFunc, ok := ret.Get(0).(func(context.Context, port.FaviconFetchRequest) (*port.FaviconFetchedIcon, error)); ok {
52+ return returnFunc(ctx, request)
53+ }
54+ if returnFunc, ok := ret.Get(0).(func(context.Context, port.FaviconFetchRequest) *port.FaviconFetchedIcon); ok {
55+ r0 = returnFunc(ctx, request)
56+ } else {
57+ if ret.Get(0) != nil {
58+ r0 = ret.Get(0).(*port.FaviconFetchedIcon)
59+ }
60+ }
61+ if returnFunc, ok := ret.Get(1).(func(context.Context, port.FaviconFetchRequest) error); ok {
62+ r1 = returnFunc(ctx, request)
63+ } else {
64+ r1 = ret.Error(1)
65+ }
66+ return r0, r1
67+}
68+
69+// MockFaviconFetcher_Fetch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Fetch'
70+type MockFaviconFetcher_Fetch_Call struct {
71+ *mock.Call
72+}
73+
74+// Fetch is a helper method to define mock.On call
75+// - ctx context.Context
76+// - request port.FaviconFetchRequest
77+func (_e *MockFaviconFetcher_Expecter) Fetch(ctx any, request any) *MockFaviconFetcher_Fetch_Call {
78+ return &MockFaviconFetcher_Fetch_Call{Call: _e.mock.On("Fetch", ctx, request)}
79+}
80+
81+func (_c *MockFaviconFetcher_Fetch_Call) Run(run func(ctx context.Context, request port.FaviconFetchRequest)) *MockFaviconFetcher_Fetch_Call {
82+ _c.Call.Run(func(args mock.Arguments) {
83+ var arg0 context.Context
84+ if args[0] != nil {
85+ arg0 = args[0].(context.Context)
86+ }
87+ var arg1 port.FaviconFetchRequest
88+ if args[1] != nil {
89+ arg1 = args[1].(port.FaviconFetchRequest)
90+ }
91+ run(
92+ arg0,
93+ arg1,
94+ )
95+ })
96+ return _c
97+}
98+
99+func (_c *MockFaviconFetcher_Fetch_Call) Return(faviconFetchedIcon *port.FaviconFetchedIcon, err error) *MockFaviconFetcher_Fetch_Call {
100+ _c.Call.Return(faviconFetchedIcon, err)
101+ return _c
102+}
103+
104+func (_c *MockFaviconFetcher_Fetch_Call) RunAndReturn(run func(ctx context.Context, request port.FaviconFetchRequest) (*port.FaviconFetchedIcon, error)) *MockFaviconFetcher_Fetch_Call {
105+ _c.Call.Return(run)
106+ return _c
107+}
A internal/application/port/mocks/favicon_invalidator.go +96 -0
@@ -0,0 +1,96 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "github.com/bnema/dumber/internal/domain/favicon"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewMockFaviconInvalidators creates a new instance of MockFaviconInvalidators. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewMockFaviconInvalidators(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *MockFaviconInvalidators {
20+ mock := &MockFaviconInvalidators{}
21+ mock.Mock.Test(t)
22+
23+ t.Cleanup(func() { mock.AssertExpectations(t) })
24+
25+ return mock
26+}
27+
28+// MockFaviconInvalidators is an autogenerated mock type for the FaviconInvalidators type
29+type MockFaviconInvalidators struct {
30+ mock.Mock
31+}
32+
33+type MockFaviconInvalidators_Expecter struct {
34+ mock *mock.Mock
35+}
36+
37+func (_m *MockFaviconInvalidators) EXPECT() *MockFaviconInvalidators_Expecter {
38+ return &MockFaviconInvalidators_Expecter{mock: &_m.Mock}
39+}
40+
41+// InvalidateAll provides a mock function for the type MockFaviconInvalidators
42+func (_mock *MockFaviconInvalidators) InvalidateAll(ctx context.Context, key favicon.Key) error {
43+ ret := _mock.Called(ctx, key)
44+
45+ if len(ret) == 0 {
46+ panic("no return value specified for InvalidateAll")
47+ }
48+
49+ var r0 error
50+ if returnFunc, ok := ret.Get(0).(func(context.Context, favicon.Key) error); ok {
51+ r0 = returnFunc(ctx, key)
52+ } else {
53+ r0 = ret.Error(0)
54+ }
55+ return r0
56+}
57+
58+// MockFaviconInvalidators_InvalidateAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InvalidateAll'
59+type MockFaviconInvalidators_InvalidateAll_Call struct {
60+ *mock.Call
61+}
62+
63+// InvalidateAll is a helper method to define mock.On call
64+// - ctx context.Context
65+// - key favicon.Key
66+func (_e *MockFaviconInvalidators_Expecter) InvalidateAll(ctx any, key any) *MockFaviconInvalidators_InvalidateAll_Call {
67+ return &MockFaviconInvalidators_InvalidateAll_Call{Call: _e.mock.On("InvalidateAll", ctx, key)}
68+}
69+
70+func (_c *MockFaviconInvalidators_InvalidateAll_Call) Run(run func(ctx context.Context, key favicon.Key)) *MockFaviconInvalidators_InvalidateAll_Call {
71+ _c.Call.Run(func(args mock.Arguments) {
72+ var arg0 context.Context
73+ if args[0] != nil {
74+ arg0 = args[0].(context.Context)
75+ }
76+ var arg1 favicon.Key
77+ if args[1] != nil {
78+ arg1 = args[1].(favicon.Key)
79+ }
80+ run(
81+ arg0,
82+ arg1,
83+ )
84+ })
85+ return _c
86+}
87+
88+func (_c *MockFaviconInvalidators_InvalidateAll_Call) Return(err error) *MockFaviconInvalidators_InvalidateAll_Call {
89+ _c.Call.Return(err)
90+ return _c
91+}
92+
93+func (_c *MockFaviconInvalidators_InvalidateAll_Call) RunAndReturn(run func(ctx context.Context, key favicon.Key) error) *MockFaviconInvalidators_InvalidateAll_Call {
94+ _c.Call.Return(run)
95+ return _c
96+}
A internal/application/port/mocks/favicon_repository.go +359 -0
@@ -0,0 +1,359 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+ "time"
10+
11+ "github.com/bnema/dumber/internal/domain/favicon"
12+ mock "github.com/stretchr/testify/mock"
13+)
14+
15+// NewMockFaviconRepository creates a new instance of MockFaviconRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
16+// The first argument is typically a *testing.T value.
17+func NewMockFaviconRepository(t interface {
18+ mock.TestingT
19+ Cleanup(func())
20+}) *MockFaviconRepository {
21+ mock := &MockFaviconRepository{}
22+ mock.Mock.Test(t)
23+
24+ t.Cleanup(func() { mock.AssertExpectations(t) })
25+
26+ return mock
27+}
28+
29+// MockFaviconRepository is an autogenerated mock type for the FaviconRepository type
30+type MockFaviconRepository struct {
31+ mock.Mock
32+}
33+
34+type MockFaviconRepository_Expecter struct {
35+ mock *mock.Mock
36+}
37+
38+func (_m *MockFaviconRepository) EXPECT() *MockFaviconRepository_Expecter {
39+ return &MockFaviconRepository_Expecter{mock: &_m.Mock}
40+}
41+
42+// Delete provides a mock function for the type MockFaviconRepository
43+func (_mock *MockFaviconRepository) Delete(ctx context.Context, key favicon.Key) error {
44+ ret := _mock.Called(ctx, key)
45+
46+ if len(ret) == 0 {
47+ panic("no return value specified for Delete")
48+ }
49+
50+ var r0 error
51+ if returnFunc, ok := ret.Get(0).(func(context.Context, favicon.Key) error); ok {
52+ r0 = returnFunc(ctx, key)
53+ } else {
54+ r0 = ret.Error(0)
55+ }
56+ return r0
57+}
58+
59+// MockFaviconRepository_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete'
60+type MockFaviconRepository_Delete_Call struct {
61+ *mock.Call
62+}
63+
64+// Delete is a helper method to define mock.On call
65+// - ctx context.Context
66+// - key favicon.Key
67+func (_e *MockFaviconRepository_Expecter) Delete(ctx any, key any) *MockFaviconRepository_Delete_Call {
68+ return &MockFaviconRepository_Delete_Call{Call: _e.mock.On("Delete", ctx, key)}
69+}
70+
71+func (_c *MockFaviconRepository_Delete_Call) Run(run func(ctx context.Context, key favicon.Key)) *MockFaviconRepository_Delete_Call {
72+ _c.Call.Run(func(args mock.Arguments) {
73+ var arg0 context.Context
74+ if args[0] != nil {
75+ arg0 = args[0].(context.Context)
76+ }
77+ var arg1 favicon.Key
78+ if args[1] != nil {
79+ arg1 = args[1].(favicon.Key)
80+ }
81+ run(
82+ arg0,
83+ arg1,
84+ )
85+ })
86+ return _c
87+}
88+
89+func (_c *MockFaviconRepository_Delete_Call) Return(err error) *MockFaviconRepository_Delete_Call {
90+ _c.Call.Return(err)
91+ return _c
92+}
93+
94+func (_c *MockFaviconRepository_Delete_Call) RunAndReturn(run func(ctx context.Context, key favicon.Key) error) *MockFaviconRepository_Delete_Call {
95+ _c.Call.Return(run)
96+ return _c
97+}
98+
99+// FindFirst provides a mock function for the type MockFaviconRepository
100+func (_mock *MockFaviconRepository) FindFirst(ctx context.Context, keys []favicon.Key) (*favicon.Metadata, error) {
101+ ret := _mock.Called(ctx, keys)
102+
103+ if len(ret) == 0 {
104+ panic("no return value specified for FindFirst")
105+ }
106+
107+ var r0 *favicon.Metadata
108+ var r1 error
109+ if returnFunc, ok := ret.Get(0).(func(context.Context, []favicon.Key) (*favicon.Metadata, error)); ok {
110+ return returnFunc(ctx, keys)
111+ }
112+ if returnFunc, ok := ret.Get(0).(func(context.Context, []favicon.Key) *favicon.Metadata); ok {
113+ r0 = returnFunc(ctx, keys)
114+ } else {
115+ if ret.Get(0) != nil {
116+ r0 = ret.Get(0).(*favicon.Metadata)
117+ }
118+ }
119+ if returnFunc, ok := ret.Get(1).(func(context.Context, []favicon.Key) error); ok {
120+ r1 = returnFunc(ctx, keys)
121+ } else {
122+ r1 = ret.Error(1)
123+ }
124+ return r0, r1
125+}
126+
127+// MockFaviconRepository_FindFirst_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindFirst'
128+type MockFaviconRepository_FindFirst_Call struct {
129+ *mock.Call
130+}
131+
132+// FindFirst is a helper method to define mock.On call
133+// - ctx context.Context
134+// - keys []favicon.Key
135+func (_e *MockFaviconRepository_Expecter) FindFirst(ctx any, keys any) *MockFaviconRepository_FindFirst_Call {
136+ return &MockFaviconRepository_FindFirst_Call{Call: _e.mock.On("FindFirst", ctx, keys)}
137+}
138+
139+func (_c *MockFaviconRepository_FindFirst_Call) Run(run func(ctx context.Context, keys []favicon.Key)) *MockFaviconRepository_FindFirst_Call {
140+ _c.Call.Run(func(args mock.Arguments) {
141+ var arg0 context.Context
142+ if args[0] != nil {
143+ arg0 = args[0].(context.Context)
144+ }
145+ var arg1 []favicon.Key
146+ if args[1] != nil {
147+ arg1 = args[1].([]favicon.Key)
148+ }
149+ run(
150+ arg0,
151+ arg1,
152+ )
153+ })
154+ return _c
155+}
156+
157+func (_c *MockFaviconRepository_FindFirst_Call) Return(metadata *favicon.Metadata, err error) *MockFaviconRepository_FindFirst_Call {
158+ _c.Call.Return(metadata, err)
159+ return _c
160+}
161+
162+func (_c *MockFaviconRepository_FindFirst_Call) RunAndReturn(run func(ctx context.Context, keys []favicon.Key) (*favicon.Metadata, error)) *MockFaviconRepository_FindFirst_Call {
163+ _c.Call.Return(run)
164+ return _c
165+}
166+
167+// Get provides a mock function for the type MockFaviconRepository
168+func (_mock *MockFaviconRepository) Get(ctx context.Context, key favicon.Key) (*favicon.Metadata, error) {
169+ ret := _mock.Called(ctx, key)
170+
171+ if len(ret) == 0 {
172+ panic("no return value specified for Get")
173+ }
174+
175+ var r0 *favicon.Metadata
176+ var r1 error
177+ if returnFunc, ok := ret.Get(0).(func(context.Context, favicon.Key) (*favicon.Metadata, error)); ok {
178+ return returnFunc(ctx, key)
179+ }
180+ if returnFunc, ok := ret.Get(0).(func(context.Context, favicon.Key) *favicon.Metadata); ok {
181+ r0 = returnFunc(ctx, key)
182+ } else {
183+ if ret.Get(0) != nil {
184+ r0 = ret.Get(0).(*favicon.Metadata)
185+ }
186+ }
187+ if returnFunc, ok := ret.Get(1).(func(context.Context, favicon.Key) error); ok {
188+ r1 = returnFunc(ctx, key)
189+ } else {
190+ r1 = ret.Error(1)
191+ }
192+ return r0, r1
193+}
194+
195+// MockFaviconRepository_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get'
196+type MockFaviconRepository_Get_Call struct {
197+ *mock.Call
198+}
199+
200+// Get is a helper method to define mock.On call
201+// - ctx context.Context
202+// - key favicon.Key
203+func (_e *MockFaviconRepository_Expecter) Get(ctx any, key any) *MockFaviconRepository_Get_Call {
204+ return &MockFaviconRepository_Get_Call{Call: _e.mock.On("Get", ctx, key)}
205+}
206+
207+func (_c *MockFaviconRepository_Get_Call) Run(run func(ctx context.Context, key favicon.Key)) *MockFaviconRepository_Get_Call {
208+ _c.Call.Run(func(args mock.Arguments) {
209+ var arg0 context.Context
210+ if args[0] != nil {
211+ arg0 = args[0].(context.Context)
212+ }
213+ var arg1 favicon.Key
214+ if args[1] != nil {
215+ arg1 = args[1].(favicon.Key)
216+ }
217+ run(
218+ arg0,
219+ arg1,
220+ )
221+ })
222+ return _c
223+}
224+
225+func (_c *MockFaviconRepository_Get_Call) Return(metadata *favicon.Metadata, err error) *MockFaviconRepository_Get_Call {
226+ _c.Call.Return(metadata, err)
227+ return _c
228+}
229+
230+func (_c *MockFaviconRepository_Get_Call) RunAndReturn(run func(ctx context.Context, key favicon.Key) (*favicon.Metadata, error)) *MockFaviconRepository_Get_Call {
231+ _c.Call.Return(run)
232+ return _c
233+}
234+
235+// UpdateLastChecked provides a mock function for the type MockFaviconRepository
236+func (_mock *MockFaviconRepository) UpdateLastChecked(ctx context.Context, key favicon.Key, contentHash string, checkedAt time.Time) error {
237+ ret := _mock.Called(ctx, key, contentHash, checkedAt)
238+
239+ if len(ret) == 0 {
240+ panic("no return value specified for UpdateLastChecked")
241+ }
242+
243+ var r0 error
244+ if returnFunc, ok := ret.Get(0).(func(context.Context, favicon.Key, string, time.Time) error); ok {
245+ r0 = returnFunc(ctx, key, contentHash, checkedAt)
246+ } else {
247+ r0 = ret.Error(0)
248+ }
249+ return r0
250+}
251+
252+// MockFaviconRepository_UpdateLastChecked_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateLastChecked'
253+type MockFaviconRepository_UpdateLastChecked_Call struct {
254+ *mock.Call
255+}
256+
257+// UpdateLastChecked is a helper method to define mock.On call
258+// - ctx context.Context
259+// - key favicon.Key
260+// - contentHash string
261+// - checkedAt time.Time
262+func (_e *MockFaviconRepository_Expecter) UpdateLastChecked(ctx any, key any, contentHash any, checkedAt any) *MockFaviconRepository_UpdateLastChecked_Call {
263+ return &MockFaviconRepository_UpdateLastChecked_Call{Call: _e.mock.On("UpdateLastChecked", ctx, key, contentHash, checkedAt)}
264+}
265+
266+func (_c *MockFaviconRepository_UpdateLastChecked_Call) Run(run func(ctx context.Context, key favicon.Key, contentHash string, checkedAt time.Time)) *MockFaviconRepository_UpdateLastChecked_Call {
267+ _c.Call.Run(func(args mock.Arguments) {
268+ var arg0 context.Context
269+ if args[0] != nil {
270+ arg0 = args[0].(context.Context)
271+ }
272+ var arg1 favicon.Key
273+ if args[1] != nil {
274+ arg1 = args[1].(favicon.Key)
275+ }
276+ var arg2 string
277+ if args[2] != nil {
278+ arg2 = args[2].(string)
279+ }
280+ var arg3 time.Time
281+ if args[3] != nil {
282+ arg3 = args[3].(time.Time)
283+ }
284+ run(
285+ arg0,
286+ arg1,
287+ arg2,
288+ arg3,
289+ )
290+ })
291+ return _c
292+}
293+
294+func (_c *MockFaviconRepository_UpdateLastChecked_Call) Return(err error) *MockFaviconRepository_UpdateLastChecked_Call {
295+ _c.Call.Return(err)
296+ return _c
297+}
298+
299+func (_c *MockFaviconRepository_UpdateLastChecked_Call) RunAndReturn(run func(ctx context.Context, key favicon.Key, contentHash string, checkedAt time.Time) error) *MockFaviconRepository_UpdateLastChecked_Call {
300+ _c.Call.Return(run)
301+ return _c
302+}
303+
304+// Upsert provides a mock function for the type MockFaviconRepository
305+func (_mock *MockFaviconRepository) Upsert(ctx context.Context, meta favicon.Metadata) error {
306+ ret := _mock.Called(ctx, meta)
307+
308+ if len(ret) == 0 {
309+ panic("no return value specified for Upsert")
310+ }
311+
312+ var r0 error
313+ if returnFunc, ok := ret.Get(0).(func(context.Context, favicon.Metadata) error); ok {
314+ r0 = returnFunc(ctx, meta)
315+ } else {
316+ r0 = ret.Error(0)
317+ }
318+ return r0
319+}
320+
321+// MockFaviconRepository_Upsert_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Upsert'
322+type MockFaviconRepository_Upsert_Call struct {
323+ *mock.Call
324+}
325+
326+// Upsert is a helper method to define mock.On call
327+// - ctx context.Context
328+// - meta favicon.Metadata
329+func (_e *MockFaviconRepository_Expecter) Upsert(ctx any, meta any) *MockFaviconRepository_Upsert_Call {
330+ return &MockFaviconRepository_Upsert_Call{Call: _e.mock.On("Upsert", ctx, meta)}
331+}
332+
333+func (_c *MockFaviconRepository_Upsert_Call) Run(run func(ctx context.Context, meta favicon.Metadata)) *MockFaviconRepository_Upsert_Call {
334+ _c.Call.Run(func(args mock.Arguments) {
335+ var arg0 context.Context
336+ if args[0] != nil {
337+ arg0 = args[0].(context.Context)
338+ }
339+ var arg1 favicon.Metadata
340+ if args[1] != nil {
341+ arg1 = args[1].(favicon.Metadata)
342+ }
343+ run(
344+ arg0,
345+ arg1,
346+ )
347+ })
348+ return _c
349+}
350+
351+func (_c *MockFaviconRepository_Upsert_Call) Return(err error) *MockFaviconRepository_Upsert_Call {
352+ _c.Call.Return(err)
353+ return _c
354+}
355+
356+func (_c *MockFaviconRepository_Upsert_Call) RunAndReturn(run func(ctx context.Context, meta favicon.Metadata) error) *MockFaviconRepository_Upsert_Call {
357+ _c.Call.Return(run)
358+ return _c
359+}
A internal/application/port/mocks/favicon_scheduler.go +102 -0
@@ -0,0 +1,102 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "github.com/bnema/dumber/internal/domain/favicon"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewMockFaviconRefreshScheduler creates a new instance of MockFaviconRefreshScheduler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewMockFaviconRefreshScheduler(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *MockFaviconRefreshScheduler {
20+ mock := &MockFaviconRefreshScheduler{}
21+ mock.Mock.Test(t)
22+
23+ t.Cleanup(func() { mock.AssertExpectations(t) })
24+
25+ return mock
26+}
27+
28+// MockFaviconRefreshScheduler is an autogenerated mock type for the FaviconRefreshScheduler type
29+type MockFaviconRefreshScheduler struct {
30+ mock.Mock
31+}
32+
33+type MockFaviconRefreshScheduler_Expecter struct {
34+ mock *mock.Mock
35+}
36+
37+func (_m *MockFaviconRefreshScheduler) EXPECT() *MockFaviconRefreshScheduler_Expecter {
38+ return &MockFaviconRefreshScheduler_Expecter{mock: &_m.Mock}
39+}
40+
41+// Schedule provides a mock function for the type MockFaviconRefreshScheduler
42+func (_mock *MockFaviconRefreshScheduler) Schedule(ctx context.Context, key favicon.Key, work func(context.Context)) bool {
43+ ret := _mock.Called(ctx, key, work)
44+
45+ if len(ret) == 0 {
46+ panic("no return value specified for Schedule")
47+ }
48+
49+ var r0 bool
50+ if returnFunc, ok := ret.Get(0).(func(context.Context, favicon.Key, func(context.Context)) bool); ok {
51+ r0 = returnFunc(ctx, key, work)
52+ } else {
53+ r0 = ret.Get(0).(bool)
54+ }
55+ return r0
56+}
57+
58+// MockFaviconRefreshScheduler_Schedule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Schedule'
59+type MockFaviconRefreshScheduler_Schedule_Call struct {
60+ *mock.Call
61+}
62+
63+// Schedule is a helper method to define mock.On call
64+// - ctx context.Context
65+// - key favicon.Key
66+// - work func(context.Context)
67+func (_e *MockFaviconRefreshScheduler_Expecter) Schedule(ctx any, key any, work any) *MockFaviconRefreshScheduler_Schedule_Call {
68+ return &MockFaviconRefreshScheduler_Schedule_Call{Call: _e.mock.On("Schedule", ctx, key, work)}
69+}
70+
71+func (_c *MockFaviconRefreshScheduler_Schedule_Call) Run(run func(ctx context.Context, key favicon.Key, work func(context.Context))) *MockFaviconRefreshScheduler_Schedule_Call {
72+ _c.Call.Run(func(args mock.Arguments) {
73+ var arg0 context.Context
74+ if args[0] != nil {
75+ arg0 = args[0].(context.Context)
76+ }
77+ var arg1 favicon.Key
78+ if args[1] != nil {
79+ arg1 = args[1].(favicon.Key)
80+ }
81+ var arg2 func(context.Context)
82+ if args[2] != nil {
83+ arg2 = args[2].(func(context.Context))
84+ }
85+ run(
86+ arg0,
87+ arg1,
88+ arg2,
89+ )
90+ })
91+ return _c
92+}
93+
94+func (_c *MockFaviconRefreshScheduler_Schedule_Call) Return(b bool) *MockFaviconRefreshScheduler_Schedule_Call {
95+ _c.Call.Return(b)
96+ return _c
97+}
98+
99+func (_c *MockFaviconRefreshScheduler_Schedule_Call) RunAndReturn(run func(ctx context.Context, key favicon.Key, work func(context.Context)) bool) *MockFaviconRefreshScheduler_Schedule_Call {
100+ _c.Call.Return(run)
101+ return _c
102+}
M internal/application/usecase/favicon_test.go +175 -79
@@ -7,11 +7,13 @@ import (
77 "time"
88
99 appport "github.com/bnema/dumber/internal/application/port"
10+ portmocks "github.com/bnema/dumber/internal/application/port/mocks"
1011 "github.com/bnema/dumber/internal/domain/favicon"
12+ "github.com/stretchr/testify/mock"
1113 )
1214
1315 func TestFaviconResolveExactHit(t *testing.T) {
14- fx := newFaviconFixture()
16+ fx := newFaviconFixture(t)
1517 fx.seed("docs.example.com", []byte("png32"), false)
1618 got, err := fx.uc.Resolve(context.Background(), "https://docs.example.com/a", 32, ResolveOptions{Purpose: ResolvePurposeUI})
1719 if err != nil {
@@ -23,7 +25,7 @@ func TestFaviconResolveExactHit(t *testing.T) {
2325 }
2426
2527 func TestFaviconResolveParentFallback(t *testing.T) {
26- fx := newFaviconFixture()
28+ fx := newFaviconFixture(t)
2729 fx.seed("example.com", []byte("parent"), false)
2830 got, err := fx.uc.Resolve(context.Background(), "https://docs.example.com/a", 32, ResolveOptions{Purpose: ResolvePurposeUI})
2931 if err != nil {
@@ -35,7 +37,7 @@ func TestFaviconResolveParentFallback(t *testing.T) {
3537 }
3638
3739 func TestFaviconResolvePrefersParentPathFallbackBeforeDomain(t *testing.T) {
38- fx := newFaviconFixture()
40+ fx := newFaviconFixture(t)
3941 fx.seed("github.com", []byte("github-default"), false)
4042 fx.seed("github.com/bnema/gordon", []byte("repo-default"), false)
4143
@@ -49,7 +51,7 @@ func TestFaviconResolvePrefersParentPathFallbackBeforeDomain(t *testing.T) {
4951 }
5052
5153 func TestFaviconResolvePathSpecificDoesNotLeakToParentOrSiblings(t *testing.T) {
52- fx := newFaviconFixture()
54+ fx := newFaviconFixture(t)
5355 fx.seed("github.com", []byte("github-default"), false)
5456
5557 if _, err := fx.uc.Observe(
@@ -91,7 +93,7 @@ func TestFaviconResolvePathSpecificDoesNotLeakToParentOrSiblings(t *testing.T) {
9193 }
9294
9395 func TestFaviconResolveRepairsOrphanCanonicalBlobMetadata(t *testing.T) {
94- fx := newFaviconFixture()
96+ fx := newFaviconFixture(t)
9597 fx.blobs.png["example.com"] = []byte("png")
9698 fx.blobs.sized["example.com"] = map[int][]byte{SystemviewIconSize: []byte("png32")}
9799
@@ -112,7 +114,7 @@ func TestFaviconResolveRepairsOrphanCanonicalBlobMetadata(t *testing.T) {
112114 }
113115
114116 func TestFaviconResolveRepairOrphanWithoutConverterReturnsMiss(t *testing.T) {
115- fx := newFaviconFixture()
117+ fx := newFaviconFixture(t)
116118 fx.uc.converter = nil
117119 fx.blobs.original["example.com"] = []byte("original")
118120 fx.blobs.contentTypes["example.com"] = "image/png"
@@ -124,7 +126,7 @@ func TestFaviconResolveRepairOrphanWithoutConverterReturnsMiss(t *testing.T) {
124126 }
125127
126128 func TestFaviconResolveStaleSchedulesDedupedBackgroundRefresh(t *testing.T) {
127- fx := newFaviconFixture()
129+ fx := newFaviconFixture(t)
128130 fx.seed("example.com", []byte("old"), true)
129131 for i := 0; i < 2; i++ {
130132 got, err := fx.uc.Resolve(context.Background(), "https://example.com", 32, ResolveOptions{ScheduleBackgroundRefresh: true})
@@ -144,7 +146,7 @@ func TestFaviconResolveStaleSchedulesDedupedBackgroundRefresh(t *testing.T) {
144146 }
145147
146148 func TestFaviconResolveStaleExactRefreshIgnoresFreshParent(t *testing.T) {
147- fx := newFaviconFixture()
149+ fx := newFaviconFixture(t)
148150 fx.seed("docs.example.com", []byte("old-exact"), true)
149151 fx.seed("example.com", []byte("fresh-parent"), false)
150152 got, err := fx.uc.Resolve(
@@ -166,7 +168,7 @@ func TestFaviconResolveStaleExactRefreshIgnoresFreshParent(t *testing.T) {
166168 }
167169
168170 func TestFaviconObserveSameHashUpdatesMetadataOnly(t *testing.T) {
169- fx := newFaviconFixture()
171+ fx := newFaviconFixture(t)
170172 fx.seedOriginal([]byte("same"))
171173 if _, err := fx.uc.Observe(context.Background(), "https://example.com", "https://cdn.test/icon.png", []byte("same"), favicon.SourceEngine, "image/png"); err != nil {
172174 t.Fatal(err)
@@ -180,7 +182,7 @@ func TestFaviconObserveSameHashUpdatesMetadataOnly(t *testing.T) {
180182 }
181183
182184 func TestFaviconObserveChangedHashRegeneratesAndInvalidates(t *testing.T) {
183- fx := newFaviconFixture()
185+ fx := newFaviconFixture(t)
184186 fx.seedOriginal([]byte("old"))
185187 if _, err := fx.uc.Observe(context.Background(), "https://example.com", "https://cdn.test/icon.png", []byte("new"), favicon.SourceEngine, "image/png"); err != nil {
186188 t.Fatal(err)
@@ -197,7 +199,7 @@ func TestFaviconObserveChangedHashRegeneratesAndInvalidates(t *testing.T) {
197199 }
198200
199201 func TestFaviconObserveWithoutConverterStoresOriginalOnly(t *testing.T) {
200- fx := newFaviconFixture()
202+ fx := newFaviconFixture(t)
201203 fx.uc.converter = nil
202204 if _, err := fx.uc.Observe(context.Background(), "https://example.com", "https://example.com/icon.png", []byte("raw"), favicon.SourceEngine, "image/png"); err != nil {
203205 t.Fatal(err)
@@ -211,7 +213,7 @@ func TestFaviconObserveWithoutConverterStoresOriginalOnly(t *testing.T) {
211213 }
212214
213215 func TestFaviconResolveMissingFetchesOnlyWhenBlockingAllowed(t *testing.T) {
214- fx := newFaviconFixture()
216+ fx := newFaviconFixture(t)
215217 if _, err := fx.uc.Resolve(context.Background(), "https://example.com", 32, ResolveOptions{}); !errors.Is(err, ErrFaviconMiss) {
216218 t.Fatalf("default resolve err=%v", err)
217219 }
@@ -228,7 +230,7 @@ func TestFaviconResolveMissingFetchesOnlyWhenBlockingAllowed(t *testing.T) {
228230 }
229231
230232 func TestFaviconRefreshIfStaleUsesParentFallbackBeforeFetching(t *testing.T) {
231- fx := newFaviconFixture()
233+ fx := newFaviconFixture(t)
232234 fx.seed("example.com", []byte("parent"), false)
233235 if err := fx.uc.RefreshIfStale(context.Background(), "https://docs.example.com/page"); err != nil {
234236 t.Fatal(err)
@@ -239,7 +241,7 @@ func TestFaviconRefreshIfStaleUsesParentFallbackBeforeFetching(t *testing.T) {
239241 }
240242
241243 func TestFaviconUseCasePropagatesRepositoryErrors(t *testing.T) {
242- fx := newFaviconFixture()
244+ fx := newFaviconFixture(t)
243245 fx.repo.err = errors.New("database unavailable")
244246 if _, err := fx.uc.Resolve(context.Background(), "https://example.com", 32, ResolveOptions{}); !errors.Is(err, fx.repo.err) {
245247 t.Fatalf("Resolve err=%v, want %v", err, fx.repo.err)
@@ -253,7 +255,7 @@ func TestFaviconUseCasePropagatesRepositoryErrors(t *testing.T) {
253255 }
254256
255257 func TestFaviconResolvePropagatesBlobErrors(t *testing.T) {
256- fx := newFaviconFixture()
258+ fx := newFaviconFixture(t)
257259 fx.seed("example.com", []byte("cached"), false)
258260 fx.blobs.err = errors.New("disk unavailable")
259261 if _, err := fx.uc.Resolve(context.Background(), "https://example.com", 32, ResolveOptions{}); !errors.Is(err, fx.blobs.err) {
@@ -262,7 +264,7 @@ func TestFaviconResolvePropagatesBlobErrors(t *testing.T) {
262264 }
263265
264266 func TestFaviconResolveBlockingRefreshesWhenMetadataFreshButBlobMissing(t *testing.T) {
265- fx := newFaviconFixture()
267+ fx := newFaviconFixture(t)
266268 fx.repo.byKey["example.com"] = &favicon.Metadata{Key: "example.com", PageURL: "https://example.com", ContentHash: favicon.Hash([]byte("old")), LastCheckedAt: fx.now}
267269 got, err := fx.uc.Resolve(context.Background(), "https://example.com", 32, ResolveOptions{AllowBlockingRefresh: true})
268270 if err != nil {
@@ -274,7 +276,7 @@ func TestFaviconResolveBlockingRefreshesWhenMetadataFreshButBlobMissing(t *testi
274276 }
275277
276278 func TestFaviconResolveBlockingPropagatesFetchErrors(t *testing.T) {
277- fx := newFaviconFixture()
279+ fx := newFaviconFixture(t)
278280 fx.fetcher.err = errors.New("network unavailable")
279281 if _, err := fx.uc.Resolve(context.Background(), "https://example.com", 32, ResolveOptions{AllowBlockingRefresh: true}); !errors.Is(err, fx.fetcher.err) {
280282 t.Fatalf("Resolve err=%v, want %v", err, fx.fetcher.err)
@@ -282,7 +284,7 @@ func TestFaviconResolveBlockingPropagatesFetchErrors(t *testing.T) {
282284 }
283285
284286 func TestFaviconRefreshFromIconURLsUsesPageURLKey(t *testing.T) {
285- fx := newFaviconFixture()
287+ fx := newFaviconFixture(t)
286288 if err := fx.uc.RefreshFromIconURLs(context.Background(), "https://docs.example.com/page", []string{"https://cdn.example.net/icon.png"}); err != nil {
287289 t.Fatal(err)
288290 }
@@ -295,8 +297,8 @@ func TestFaviconRefreshFromIconURLsUsesPageURLKey(t *testing.T) {
295297 }
296298
297299 func TestFaviconRefreshStoresDuckDuckGoFallbackUnderHostKey(t *testing.T) {
298- fx := newFaviconFixture()
299- fx.fetcher.responses = map[string]fakeFetchedIcon{
300+ fx := newFaviconFixture(t)
301+ fx.fetcher.responses = map[string]faviconFetchResponse{
300302 "https://example.com/favicon.ico": {
301303 bytes: []byte("host-fallback"),
302304 contentType: "image/png",
@@ -316,8 +318,8 @@ func TestFaviconRefreshStoresDuckDuckGoFallbackUnderHostKey(t *testing.T) {
316318 }
317319
318320 func TestFaviconRefreshRejectsResolvedKeyOutsideCandidateHierarchy(t *testing.T) {
319- fx := newFaviconFixture()
320- fx.fetcher.responses = map[string]fakeFetchedIcon{
321+ fx := newFaviconFixture(t)
322+ fx.fetcher.responses = map[string]faviconFetchResponse{
321323 "https://example.com/favicon.ico": {
322324 bytes: []byte("bad-fallback"),
323325 contentType: "image/png",
@@ -335,8 +337,8 @@ func TestFaviconRefreshRejectsResolvedKeyOutsideCandidateHierarchy(t *testing.T)
335337 }
336338
337339 func TestFaviconRefreshRejectsFetcherPageURLOutsideRequestedHierarchy(t *testing.T) {
338- fx := newFaviconFixture()
339- fx.fetcher.responses = map[string]fakeFetchedIcon{
340+ fx := newFaviconFixture(t)
341+ fx.fetcher.responses = map[string]faviconFetchResponse{
340342 "https://example.com/favicon.ico": {
341343 bytes: []byte("bad-page-url"),
342344 contentType: "image/png",
@@ -355,8 +357,8 @@ func TestFaviconRefreshRejectsFetcherPageURLOutsideRequestedHierarchy(t *testing
355357 }
356358
357359 func TestFaviconRefreshFromIconURLsSkipsUnsupportedCandidates(t *testing.T) {
358- fx := newFaviconFixture()
359- fx.fetcher.responses = map[string]fakeFetchedIcon{
360+ fx := newFaviconFixture(t)
361+ fx.fetcher.responses = map[string]faviconFetchResponse{
360362 "https://cdn.example.net/icon.svg": {bytes: []byte("svg"), contentType: "image/svg+xml"},
361363 "https://cdn.example.net/icon.png": {bytes: []byte("png"), contentType: "image/png"},
362364 }
@@ -385,7 +387,7 @@ func TestFaviconRefreshFromIconURLsSkipsUnsupportedCandidates(t *testing.T) {
385387 }
386388
387389 func TestFaviconObserveConversionErrorDoesNotMutateExistingAssets(t *testing.T) {
388- fx := newFaviconFixture()
390+ fx := newFaviconFixture(t)
389391 fx.seedOriginal([]byte("old"))
390392 fx.blobs.png["example.com"] = []byte("old-png")
391393 fx.blobs.sized["example.com"] = map[int][]byte{32: []byte("old-32")}
@@ -403,7 +405,7 @@ func TestFaviconObserveConversionErrorDoesNotMutateExistingAssets(t *testing.T)
403405 }
404406
405407 func TestFaviconEnsureSizedPropagatesReadSizedErrors(t *testing.T) {
406- fx := newFaviconFixture()
408+ fx := newFaviconFixture(t)
407409 fx.seedOriginal([]byte("old"))
408410 fx.blobs.readSizedErr = errors.New("sized store unavailable")
409411 if err := fx.uc.EnsureSized(context.Background(), "example.com", 32); !errors.Is(err, fx.blobs.readSizedErr) {
@@ -415,12 +417,12 @@ func TestFaviconEnsureSizedPropagatesReadSizedErrors(t *testing.T) {
415417 }
416418
417419 type faviconFixture struct {
418- repo *fakeFaviconRepo
419- blobs *fakeBlobStore
420- converter *fakeConverter
421- scheduler *fakeScheduler
422- invalidators *fakeInvalidators
423- fetcher *fakeFetcher
420+ repo *faviconRepoState
421+ blobs *faviconBlobStoreState
422+ converter *faviconConverterState
423+ scheduler *faviconSchedulerState
424+ invalidators *faviconInvalidatorsState
425+ fetcher *faviconFetcherState
424426 uc *FaviconUseCase
425427 now time.Time
426428 }
@@ -429,29 +431,98 @@ type faviconTestContextKey string
429431
430432 const faviconFixtureBackgroundKey faviconTestContextKey = "bg"
431433
432-func newFaviconFixture() *faviconFixture {
434+func newFaviconFixture(t *testing.T) *faviconFixture {
435+ t.Helper()
436+
433437 now := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
434438 fx := &faviconFixture{
435- repo: newFakeRepo(),
436- blobs: newFakeBlobStore(),
437- converter: &fakeConverter{},
438- scheduler: &fakeScheduler{seen: map[favicon.Key]bool{}},
439- invalidators: &fakeInvalidators{},
440- fetcher: &fakeFetcher{},
439+ repo: newFaviconRepoState(),
440+ blobs: newFaviconBlobStoreState(),
441+ converter: &faviconConverterState{},
442+ scheduler: &faviconSchedulerState{seen: map[favicon.Key]bool{}},
443+ invalidators: &faviconInvalidatorsState{},
444+ fetcher: &faviconFetcherState{},
441445 now: now,
442446 }
447+
448+ repo := mockFaviconRepository(t, fx.repo)
449+ blobs := mockFaviconBlobStore(t, fx.blobs)
450+ converter := mockFaviconConverter(t, fx.converter)
451+ scheduler := mockFaviconScheduler(t, fx.scheduler)
452+ invalidators := mockFaviconInvalidators(t, fx.invalidators)
453+ fetcher := mockFaviconFetcher(t, fx.fetcher)
454+
443455 fx.uc = NewFaviconUseCase(FaviconDeps{
444- Repository: fx.repo,
445- BlobStore: fx.blobs,
446- Converter: fx.converter,
447- Scheduler: fx.scheduler,
448- Invalidators: fx.invalidators,
449- Fetcher: fx.fetcher,
456+ Repository: repo,
457+ BlobStore: blobs,
458+ Converter: converter,
459+ Scheduler: scheduler,
460+ Invalidators: invalidators,
461+ Fetcher: fetcher,
450462 Now: func() time.Time { return fx.now },
451463 Background: context.WithValue(context.Background(), faviconFixtureBackgroundKey, true),
452464 })
453465 return fx
454466 }
467+
468+func mockFaviconRepository(t *testing.T, state *faviconRepoState) *portmocks.MockFaviconRepository {
469+ t.Helper()
470+
471+ repo := portmocks.NewMockFaviconRepository(t)
472+ repo.EXPECT().Get(mock.Anything, mock.Anything).RunAndReturn(state.get).Maybe()
473+ repo.EXPECT().FindFirst(mock.Anything, mock.Anything).RunAndReturn(state.findFirst).Maybe()
474+ repo.EXPECT().Upsert(mock.Anything, mock.Anything).RunAndReturn(state.upsert).Maybe()
475+ repo.EXPECT().UpdateLastChecked(mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn(state.updateLastChecked).Maybe()
476+ repo.EXPECT().Delete(mock.Anything, mock.Anything).RunAndReturn(state.delete).Maybe()
477+ return repo
478+}
479+
480+func mockFaviconBlobStore(t *testing.T, state *faviconBlobStoreState) *portmocks.MockFaviconBlobStore {
481+ t.Helper()
482+
483+ blobs := portmocks.NewMockFaviconBlobStore(t)
484+ blobs.EXPECT().ReadOriginal(mock.Anything, mock.Anything).RunAndReturn(state.readOriginal).Maybe()
485+ blobs.EXPECT().WriteOriginal(mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn(state.writeOriginal).Maybe()
486+ blobs.EXPECT().ReadPNG(mock.Anything, mock.Anything).RunAndReturn(state.readPNG).Maybe()
487+ blobs.EXPECT().WritePNG(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(state.writePNG).Maybe()
488+ blobs.EXPECT().ReadSizedPNG(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(state.readSizedPNG).Maybe()
489+ blobs.EXPECT().WriteSizedPNG(mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn(state.writeSizedPNG).Maybe()
490+ blobs.EXPECT().RemoveDerived(mock.Anything, mock.Anything).RunAndReturn(state.removeDerivedForKey).Maybe()
491+ return blobs
492+}
493+
494+func mockFaviconConverter(t *testing.T, state *faviconConverterState) *portmocks.MockFaviconImageConverter {
495+ t.Helper()
496+
497+ converter := portmocks.NewMockFaviconImageConverter(t)
498+ converter.EXPECT().Convert(mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn(state.convert).Maybe()
499+ return converter
500+}
501+
502+func mockFaviconScheduler(t *testing.T, state *faviconSchedulerState) *portmocks.MockFaviconRefreshScheduler {
503+ t.Helper()
504+
505+ scheduler := portmocks.NewMockFaviconRefreshScheduler(t)
506+ scheduler.EXPECT().Schedule(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(state.schedule).Maybe()
507+ return scheduler
508+}
509+
510+func mockFaviconInvalidators(t *testing.T, state *faviconInvalidatorsState) *portmocks.MockFaviconInvalidators {
511+ t.Helper()
512+
513+ invalidators := portmocks.NewMockFaviconInvalidators(t)
514+ invalidators.EXPECT().InvalidateAll(mock.Anything, mock.Anything).RunAndReturn(state.invalidateAll).Maybe()
515+ return invalidators
516+}
517+
518+func mockFaviconFetcher(t *testing.T, state *faviconFetcherState) *portmocks.MockFaviconFetcher {
519+ t.Helper()
520+
521+ fetcher := portmocks.NewMockFaviconFetcher(t)
522+ fetcher.EXPECT().Fetch(mock.Anything, mock.Anything).RunAndReturn(state.fetch).Maybe()
523+ return fetcher
524+}
525+
455526 func (f *faviconFixture) seed(key string, sized []byte, stale bool) {
456527 checked := f.now
457528 if stale {
@@ -460,6 +531,7 @@ func (f *faviconFixture) seed(key string, sized []byte, stale bool) {
460531 f.repo.byKey[favicon.Key(key)] = &favicon.Metadata{Key: favicon.Key(key), PageURL: "https://" + key, ContentHash: favicon.Hash([]byte("orig")), LastCheckedAt: checked}
461532 f.blobs.sized[favicon.Key(key)] = map[int][]byte{32: sized}
462533 }
534+
463535 func (f *faviconFixture) seedOriginal(b []byte) {
464536 const contentType = "image/png"
465537 key := favicon.Key("example.com")
@@ -474,15 +546,16 @@ func (f *faviconFixture) seedOriginal(b []byte) {
474546 f.blobs.contentTypes[key] = contentType
475547 }
476548
477-type fakeFaviconRepo struct {
549+type faviconRepoState struct {
478550 byKey map[favicon.Key]*favicon.Metadata
479551 err error
480552 }
481553
482-func newFakeRepo() *fakeFaviconRepo {
483- return &fakeFaviconRepo{byKey: map[favicon.Key]*favicon.Metadata{}}
554+func newFaviconRepoState() *faviconRepoState {
555+ return &faviconRepoState{byKey: map[favicon.Key]*favicon.Metadata{}}
484556 }
485-func (r *fakeFaviconRepo) Get(_ context.Context, key favicon.Key) (*favicon.Metadata, error) {
557+
558+func (r *faviconRepoState) get(_ context.Context, key favicon.Key) (*favicon.Metadata, error) {
486559 if r.err != nil {
487560 return nil, r.err
488561 }
@@ -492,18 +565,20 @@ func (r *fakeFaviconRepo) Get(_ context.Context, key favicon.Key) (*favicon.Meta
492565 }
493566 return nil, nil
494567 }
495-func (r *fakeFaviconRepo) FindFirst(ctx context.Context, keys []favicon.Key) (*favicon.Metadata, error) {
568+
569+func (r *faviconRepoState) findFirst(ctx context.Context, keys []favicon.Key) (*favicon.Metadata, error) {
496570 if r.err != nil {
497571 return nil, r.err
498572 }
499573 for _, k := range keys {
500- if m, _ := r.Get(ctx, k); m != nil {
574+ if m, _ := r.get(ctx, k); m != nil {
501575 return m, nil
502576 }
503577 }
504578 return nil, nil
505579 }
506-func (r *fakeFaviconRepo) Upsert(_ context.Context, meta favicon.Metadata) error {
580+
581+func (r *faviconRepoState) upsert(_ context.Context, meta favicon.Metadata) error {
507582 if r.err != nil {
508583 return r.err
509584 }
@@ -511,7 +586,8 @@ func (r *fakeFaviconRepo) Upsert(_ context.Context, meta favicon.Metadata) error
511586 r.byKey[meta.Key] = &cp
512587 return nil
513588 }
514-func (r *fakeFaviconRepo) UpdateLastChecked(_ context.Context, key favicon.Key, hash string, checkedAt time.Time) error {
589+
590+func (r *faviconRepoState) updateLastChecked(_ context.Context, key favicon.Key, hash string, checkedAt time.Time) error {
515591 if r.err != nil {
516592 return r.err
517593 }
@@ -519,7 +595,8 @@ func (r *fakeFaviconRepo) UpdateLastChecked(_ context.Context, key favicon.Key,
519595 r.byKey[key].LastCheckedAt = checkedAt
520596 return nil
521597 }
522-func (r *fakeFaviconRepo) Delete(_ context.Context, key favicon.Key) error {
598+
599+func (r *faviconRepoState) delete(_ context.Context, key favicon.Key) error {
523600 if r.err != nil {
524601 return r.err
525602 }
@@ -527,7 +604,7 @@ func (r *fakeFaviconRepo) Delete(_ context.Context, key favicon.Key) error {
527604 return nil
528605 }
529606
530-type fakeBlobStore struct {
607+type faviconBlobStoreState struct {
531608 original, png map[favicon.Key][]byte
532609 sized map[favicon.Key]map[int][]byte
533610 contentTypes map[favicon.Key]string
@@ -536,10 +613,16 @@ type fakeBlobStore struct {
536613 readSizedErr error
537614 }
538615
539-func newFakeBlobStore() *fakeBlobStore {
540- return &fakeBlobStore{original: map[favicon.Key][]byte{}, png: map[favicon.Key][]byte{}, sized: map[favicon.Key]map[int][]byte{}, contentTypes: map[favicon.Key]string{}}
616+func newFaviconBlobStoreState() *faviconBlobStoreState {
617+ return &faviconBlobStoreState{
618+ original: map[favicon.Key][]byte{},
619+ png: map[favicon.Key][]byte{},
620+ sized: map[favicon.Key]map[int][]byte{},
621+ contentTypes: map[favicon.Key]string{},
622+ }
541623 }
542-func (b *fakeBlobStore) ReadOriginal(_ context.Context, key favicon.Key) ([]byte, string, error) {
624+
625+func (b *faviconBlobStoreState) readOriginal(_ context.Context, key favicon.Key) ([]byte, string, error) {
543626 if b.err != nil {
544627 return nil, "", b.err
545628 }
@@ -549,7 +632,8 @@ func (b *fakeBlobStore) ReadOriginal(_ context.Context, key favicon.Key) ([]byte
549632 }
550633 return v, b.contentTypes[key], nil
551634 }
552-func (b *fakeBlobStore) WriteOriginal(_ context.Context, key favicon.Key, data []byte, ct string) error {
635+
636+func (b *faviconBlobStoreState) writeOriginal(_ context.Context, key favicon.Key, data []byte, ct string) error {
553637 if b.err != nil {
554638 return b.err
555639 }
@@ -558,7 +642,8 @@ func (b *fakeBlobStore) WriteOriginal(_ context.Context, key favicon.Key, data [
558642 b.contentTypes[key] = ct
559643 return nil
560644 }
561-func (b *fakeBlobStore) ReadPNG(_ context.Context, key favicon.Key) ([]byte, string, error) {
645+
646+func (b *faviconBlobStoreState) readPNG(_ context.Context, key favicon.Key) ([]byte, string, error) {
562647 if b.err != nil {
563648 return nil, "", b.err
564649 }
@@ -568,14 +653,16 @@ func (b *fakeBlobStore) ReadPNG(_ context.Context, key favicon.Key) ([]byte, str
568653 }
569654 return v, "image/png", nil
570655 }
571-func (b *fakeBlobStore) WritePNG(_ context.Context, key favicon.Key, data []byte) error {
656+
657+func (b *faviconBlobStoreState) writePNG(_ context.Context, key favicon.Key, data []byte) error {
572658 if b.err != nil {
573659 return b.err
574660 }
575661 b.png[key] = data
576662 return nil
577663 }
578-func (b *fakeBlobStore) ReadSizedPNG(_ context.Context, key favicon.Key, size int) ([]byte, string, error) {
664+
665+func (b *faviconBlobStoreState) readSizedPNG(_ context.Context, key favicon.Key, size int) ([]byte, string, error) {
579666 if b.err != nil {
580667 return nil, "", b.err
581668 }
@@ -587,7 +674,8 @@ func (b *fakeBlobStore) ReadSizedPNG(_ context.Context, key favicon.Key, size in
587674 }
588675 return b.sized[key][size], "image/png", nil
589676 }
590-func (b *fakeBlobStore) WriteSizedPNG(_ context.Context, key favicon.Key, size int, data []byte) error {
677+
678+func (b *faviconBlobStoreState) writeSizedPNG(_ context.Context, key favicon.Key, size int, data []byte) error {
591679 if b.err != nil {
592680 return b.err
593681 }
@@ -597,7 +685,8 @@ func (b *fakeBlobStore) WriteSizedPNG(_ context.Context, key favicon.Key, size i
597685 b.sized[key][size] = data
598686 return nil
599687 }
600-func (b *fakeBlobStore) RemoveDerived(_ context.Context, key favicon.Key) error {
688+
689+func (b *faviconBlobStoreState) removeDerivedForKey(_ context.Context, key favicon.Key) error {
601690 if b.err != nil {
602691 return b.err
603692 }
@@ -607,13 +696,13 @@ func (b *fakeBlobStore) RemoveDerived(_ context.Context, key favicon.Key) error
607696 return nil
608697 }
609698
610-type fakeConverter struct {
699+type faviconConverterState struct {
611700 calls int
612701 err error
613702 errByContentType map[string]error
614703 }
615704
616-func (c *fakeConverter) Convert(_ context.Context, original []byte, contentType string, sizes []int) (*appport.ConvertedFavicon, error) {
705+func (c *faviconConverterState) convert(_ context.Context, original []byte, contentType string, sizes []int) (*appport.ConvertedFavicon, error) {
617706 c.calls++
618707 if c.err != nil {
619708 return nil, c.err
@@ -628,14 +717,14 @@ func (c *fakeConverter) Convert(_ context.Context, original []byte, contentType
628717 return out, nil
629718 }
630719
631-type fakeScheduler struct {
720+type faviconSchedulerState struct {
632721 seen map[favicon.Key]bool
633722 works map[favicon.Key]func(context.Context)
634723 schedules int
635724 usedCallerContext bool
636725 }
637726
638-func (s *fakeScheduler) Schedule(ctx context.Context, key favicon.Key, work func(context.Context)) bool {
727+func (s *faviconSchedulerState) schedule(ctx context.Context, key favicon.Key, work func(context.Context)) bool {
639728 if ctx.Value(faviconFixtureBackgroundKey) != true {
640729 s.usedCallerContext = true
641730 }
@@ -651,33 +740,33 @@ func (s *fakeScheduler) Schedule(ctx context.Context, key favicon.Key, work func
651740 return true
652741 }
653742
654-func (s *fakeScheduler) run(key favicon.Key) {
743+func (s *faviconSchedulerState) run(key favicon.Key) {
655744 if work := s.works[key]; work != nil {
656745 work(context.Background())
657746 }
658747 }
659748
660-type fakeInvalidators struct{ calls int }
749+type faviconInvalidatorsState struct{ calls int }
661750
662-func (i *fakeInvalidators) InvalidateAll(_ context.Context, _ favicon.Key) error {
751+func (i *faviconInvalidatorsState) invalidateAll(_ context.Context, _ favicon.Key) error {
663752 i.calls++
664753 return nil
665754 }
666755
667-type fakeFetchedIcon struct {
756+type faviconFetchResponse struct {
668757 bytes []byte
669758 contentType string
670759 pageURL string
671760 resolvedKey favicon.Key
672761 }
673762
674-type fakeFetcher struct {
763+type faviconFetcherState struct {
675764 calls int
676765 err error
677- responses map[string]fakeFetchedIcon
766+ responses map[string]faviconFetchResponse
678767 }
679768
680-func (f *fakeFetcher) Fetch(_ context.Context, req appport.FaviconFetchRequest) (*appport.FaviconFetchedIcon, error) {
769+func (f *faviconFetcherState) fetch(_ context.Context, req appport.FaviconFetchRequest) (*appport.FaviconFetchedIcon, error) {
681770 f.calls++
682771 if f.err != nil {
683772 return nil, f.err
@@ -688,11 +777,18 @@ func (f *fakeFetcher) Fetch(_ context.Context, req appport.FaviconFetchRequest)
688777 }
689778 response := f.responses[iconURL]
690779 if response.bytes == nil {
691- response = fakeFetchedIcon{bytes: []byte("fetched"), contentType: "image/png"}
780+ response = faviconFetchResponse{bytes: []byte("fetched"), contentType: "image/png"}
692781 }
693782 pageURL := req.PageURL
694783 if response.pageURL != "" {
695784 pageURL = response.pageURL
696785 }
697- return &appport.FaviconFetchedIcon{PageURL: pageURL, IconURL: iconURL, ResolvedKey: response.resolvedKey, Bytes: response.bytes, Source: favicon.SourceDuckDuckGo, ContentType: response.contentType}, nil
786+ return &appport.FaviconFetchedIcon{
787+ PageURL: pageURL,
788+ IconURL: iconURL,
789+ ResolvedKey: response.resolvedKey,
790+ Bytes: response.bytes,
791+ Source: favicon.SourceDuckDuckGo,
792+ ContentType: response.contentType,
793+ }, nil
698794 }
A internal/bootstrap/mocks/runtime_config.go +167 -0
@@ -0,0 +1,167 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "github.com/bnema/dumber/internal/infrastructure/config"
9+ mock "github.com/stretchr/testify/mock"
10+)
11+
12+// NewMockConfigChangeManager creates a new instance of MockConfigChangeManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
13+// The first argument is typically a *testing.T value.
14+func NewMockConfigChangeManager(t interface {
15+ mock.TestingT
16+ Cleanup(func())
17+}) *MockConfigChangeManager {
18+ mock := &MockConfigChangeManager{}
19+ mock.Mock.Test(t)
20+
21+ t.Cleanup(func() { mock.AssertExpectations(t) })
22+
23+ return mock
24+}
25+
26+// MockConfigChangeManager is an autogenerated mock type for the configChangeManager type
27+type MockConfigChangeManager struct {
28+ mock.Mock
29+}
30+
31+type MockConfigChangeManager_Expecter struct {
32+ mock *mock.Mock
33+}
34+
35+func (_m *MockConfigChangeManager) EXPECT() *MockConfigChangeManager_Expecter {
36+ return &MockConfigChangeManager_Expecter{mock: &_m.Mock}
37+}
38+
39+// Get provides a mock function for the type MockConfigChangeManager
40+func (_mock *MockConfigChangeManager) Get() *config.Config {
41+ ret := _mock.Called()
42+
43+ if len(ret) == 0 {
44+ panic("no return value specified for Get")
45+ }
46+
47+ var r0 *config.Config
48+ if returnFunc, ok := ret.Get(0).(func() *config.Config); ok {
49+ r0 = returnFunc()
50+ } else {
51+ if ret.Get(0) != nil {
52+ r0 = ret.Get(0).(*config.Config)
53+ }
54+ }
55+ return r0
56+}
57+
58+// MockConfigChangeManager_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get'
59+type MockConfigChangeManager_Get_Call struct {
60+ *mock.Call
61+}
62+
63+// Get is a helper method to define mock.On call
64+func (_e *MockConfigChangeManager_Expecter) Get() *MockConfigChangeManager_Get_Call {
65+ return &MockConfigChangeManager_Get_Call{Call: _e.mock.On("Get")}
66+}
67+
68+func (_c *MockConfigChangeManager_Get_Call) Run(run func()) *MockConfigChangeManager_Get_Call {
69+ _c.Call.Run(func(args mock.Arguments) {
70+ run()
71+ })
72+ return _c
73+}
74+
75+func (_c *MockConfigChangeManager_Get_Call) Return(config1 *config.Config) *MockConfigChangeManager_Get_Call {
76+ _c.Call.Return(config1)
77+ return _c
78+}
79+
80+func (_c *MockConfigChangeManager_Get_Call) RunAndReturn(run func() *config.Config) *MockConfigChangeManager_Get_Call {
81+ _c.Call.Return(run)
82+ return _c
83+}
84+
85+// OnConfigChange provides a mock function for the type MockConfigChangeManager
86+func (_mock *MockConfigChangeManager) OnConfigChange(fn func(*config.Config)) {
87+ _mock.Called(fn)
88+ return
89+}
90+
91+// MockConfigChangeManager_OnConfigChange_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnConfigChange'
92+type MockConfigChangeManager_OnConfigChange_Call struct {
93+ *mock.Call
94+}
95+
96+// OnConfigChange is a helper method to define mock.On call
97+// - fn func(*config.Config)
98+func (_e *MockConfigChangeManager_Expecter) OnConfigChange(fn any) *MockConfigChangeManager_OnConfigChange_Call {
99+ return &MockConfigChangeManager_OnConfigChange_Call{Call: _e.mock.On("OnConfigChange", fn)}
100+}
101+
102+func (_c *MockConfigChangeManager_OnConfigChange_Call) Run(run func(fn func(*config.Config))) *MockConfigChangeManager_OnConfigChange_Call {
103+ _c.Call.Run(func(args mock.Arguments) {
104+ var arg0 func(*config.Config)
105+ if args[0] != nil {
106+ arg0 = args[0].(func(*config.Config))
107+ }
108+ run(
109+ arg0,
110+ )
111+ })
112+ return _c
113+}
114+
115+func (_c *MockConfigChangeManager_OnConfigChange_Call) Return() *MockConfigChangeManager_OnConfigChange_Call {
116+ _c.Call.Return()
117+ return _c
118+}
119+
120+func (_c *MockConfigChangeManager_OnConfigChange_Call) RunAndReturn(run func(fn func(*config.Config))) *MockConfigChangeManager_OnConfigChange_Call {
121+ _c.Run(run)
122+ return _c
123+}
124+
125+// Watch provides a mock function for the type MockConfigChangeManager
126+func (_mock *MockConfigChangeManager) Watch() error {
127+ ret := _mock.Called()
128+
129+ if len(ret) == 0 {
130+ panic("no return value specified for Watch")
131+ }
132+
133+ var r0 error
134+ if returnFunc, ok := ret.Get(0).(func() error); ok {
135+ r0 = returnFunc()
136+ } else {
137+ r0 = ret.Error(0)
138+ }
139+ return r0
140+}
141+
142+// MockConfigChangeManager_Watch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Watch'
143+type MockConfigChangeManager_Watch_Call struct {
144+ *mock.Call
145+}
146+
147+// Watch is a helper method to define mock.On call
148+func (_e *MockConfigChangeManager_Expecter) Watch() *MockConfigChangeManager_Watch_Call {
149+ return &MockConfigChangeManager_Watch_Call{Call: _e.mock.On("Watch")}
150+}
151+
152+func (_c *MockConfigChangeManager_Watch_Call) Run(run func()) *MockConfigChangeManager_Watch_Call {
153+ _c.Call.Run(func(args mock.Arguments) {
154+ run()
155+ })
156+ return _c
157+}
158+
159+func (_c *MockConfigChangeManager_Watch_Call) Return(err error) *MockConfigChangeManager_Watch_Call {
160+ _c.Call.Return(err)
161+ return _c
162+}
163+
164+func (_c *MockConfigChangeManager_Watch_Call) RunAndReturn(run func() error) *MockConfigChangeManager_Watch_Call {
165+ _c.Call.Return(run)
166+ return _c
167+}
M internal/bootstrap/runtime_config_test.go +38 -26
@@ -5,32 +5,44 @@ import (
55 "testing"
66
77 "github.com/bnema/dumber/internal/application/port"
8+ bootstrapmocks "github.com/bnema/dumber/internal/bootstrap/mocks"
89 "github.com/bnema/dumber/internal/domain/entity"
910 "github.com/bnema/dumber/internal/infrastructure/config"
11+ "github.com/stretchr/testify/mock"
1012 )
1113
12-type fakeConfigManager struct {
14+type configManagerState struct {
1315 current *config.Config
1416 watchCalls int
1517 callbacks []func(*config.Config)
1618 }
1719
18-func (f *fakeConfigManager) Watch() error {
19- f.watchCalls++
20+func newMockConfigManager(t *testing.T, state *configManagerState) *bootstrapmocks.MockConfigChangeManager {
21+ t.Helper()
22+
23+ manager := bootstrapmocks.NewMockConfigChangeManager(t)
24+ manager.EXPECT().Get().RunAndReturn(state.get).Maybe()
25+ manager.EXPECT().Watch().RunAndReturn(state.watch).Maybe()
26+ manager.EXPECT().OnConfigChange(mock.Anything).RunAndReturn(state.onConfigChange).Maybe()
27+ return manager
28+}
29+
30+func (s *configManagerState) watch() error {
31+ s.watchCalls++
2032 return nil
2133 }
2234
23-func (f *fakeConfigManager) OnConfigChange(callback func(*config.Config)) {
24- f.callbacks = append(f.callbacks, callback)
35+func (s *configManagerState) onConfigChange(callback func(*config.Config)) {
36+ s.callbacks = append(s.callbacks, callback)
2537 }
2638
27-func (f *fakeConfigManager) Get() *config.Config {
28- return f.current
39+func (s *configManagerState) get() *config.Config {
40+ return s.current
2941 }
3042
31-func (f *fakeConfigManager) emit(next *config.Config) {
32- f.current = next
33- for _, callback := range f.callbacks {
43+func (s *configManagerState) emit(next *config.Config) {
44+ s.current = next
45+ for _, callback := range s.callbacks {
3446 callback(next)
3547 }
3648 }
@@ -71,8 +83,8 @@ func TestEngineSettingsPayloadFromConfigMapsRuntimeFields(t *testing.T) {
7183 func TestRuntimeConfigProviderUpdatesSnapshotBeforeCallback(t *testing.T) {
7284 initial := config.DefaultConfig()
7385 initial.DefaultUIScale = 1
74- manager := &fakeConfigManager{}
75- provider := NewRuntimeConfigProvider(initial, manager)
86+ manager := &configManagerState{}
87+ provider := NewRuntimeConfigProvider(initial, newMockConfigManager(t, manager))
7688
7789 var seen port.RuntimeConfigSnapshot
7890 provider.OnChange(func(snapshot port.RuntimeConfigSnapshot) {
@@ -94,8 +106,8 @@ func TestRuntimeConfigProviderUpdatesSnapshotBeforeCallback(t *testing.T) {
94106 func TestRuntimeConfigProviderCurrentReflectsManagerGetWithoutSubscription(t *testing.T) {
95107 initial := config.DefaultConfig()
96108 initial.DefaultUIScale = 1
97- manager := &fakeConfigManager{current: initial}
98- provider := NewRuntimeConfigProvider(initial, manager)
109+ manager := &configManagerState{current: initial}
110+ provider := NewRuntimeConfigProvider(initial, newMockConfigManager(t, manager))
99111
100112 next := config.DefaultConfig()
101113 next.DefaultUIScale = 1.8
@@ -109,8 +121,8 @@ func TestRuntimeConfigProviderCurrentReflectsManagerGetWithoutSubscription(t *te
109121 func TestRuntimeConfigProviderOnChangeCallbackCanCallCurrent(t *testing.T) {
110122 initial := config.DefaultConfig()
111123 initial.DefaultUIScale = 1
112- manager := &fakeConfigManager{current: initial}
113- provider := NewRuntimeConfigProvider(initial, manager)
124+ manager := &configManagerState{current: initial}
125+ provider := NewRuntimeConfigProvider(initial, newMockConfigManager(t, manager))
114126
115127 completed := false
116128 var seen port.RuntimeConfigSnapshot
@@ -132,8 +144,8 @@ func TestRuntimeConfigProviderOnChangeCallbackCanCallCurrent(t *testing.T) {
132144 }
133145
134146 func TestRuntimeConfigProviderOnChangeNilCallbackDoesNotRegister(t *testing.T) {
135- manager := &fakeConfigManager{current: config.DefaultConfig()}
136- provider := NewRuntimeConfigProvider(config.DefaultConfig(), manager)
147+ manager := &configManagerState{current: config.DefaultConfig()}
148+ provider := NewRuntimeConfigProvider(config.DefaultConfig(), newMockConfigManager(t, manager))
137149
138150 provider.OnChange(nil)
139151
@@ -143,8 +155,8 @@ func TestRuntimeConfigProviderOnChangeNilCallbackDoesNotRegister(t *testing.T) {
143155 }
144156
145157 func TestRuntimeConfigProviderWatchDelegatesToManager(t *testing.T) {
146- manager := &fakeConfigManager{}
147- provider := NewRuntimeConfigProvider(config.DefaultConfig(), manager)
158+ manager := &configManagerState{}
159+ provider := NewRuntimeConfigProvider(config.DefaultConfig(), newMockConfigManager(t, manager))
148160
149161 if err := provider.Watch(); err != nil {
150162 t.Fatalf("Watch returned error: %v", err)
@@ -159,7 +171,7 @@ func TestRuntimeConfigProviderCurrentReturnsMapClone(t *testing.T) {
159171 cfg.SearchShortcuts = map[string]config.SearchShortcut{
160172 "gh": {URL: "https://github.com/search?q=%s", Description: "GitHub"},
161173 }
162- provider := NewRuntimeConfigProvider(cfg, &fakeConfigManager{current: cfg})
174+ provider := NewRuntimeConfigProvider(cfg, newMockConfigManager(t, &configManagerState{current: cfg}))
163175
164176 first := provider.Current()
165177 first.UI.SearchShortcuts["gh"] = port.RuntimeSearchShortcut{URL: "mutated"}
@@ -172,7 +184,7 @@ func TestRuntimeConfigProviderCurrentReturnsMapClone(t *testing.T) {
172184
173185 func TestRuntimeConfigProviderCurrentReturnsNestedConfigClone(t *testing.T) {
174186 cfg := runtimeConfigWithNestedMutableFields()
175- provider := NewRuntimeConfigProvider(cfg, &fakeConfigManager{current: cfg})
187+ provider := NewRuntimeConfigProvider(cfg, newMockConfigManager(t, &configManagerState{current: cfg}))
176188
177189 first := provider.Current()
178190 mutateNestedRuntimeConfigSnapshot(first)
@@ -182,8 +194,8 @@ func TestRuntimeConfigProviderCurrentReturnsNestedConfigClone(t *testing.T) {
182194 }
183195
184196 func TestRuntimeConfigProviderOnChangePassesNestedConfigClone(t *testing.T) {
185- manager := &fakeConfigManager{}
186- provider := NewRuntimeConfigProvider(config.DefaultConfig(), manager)
197+ manager := &configManagerState{}
198+ provider := NewRuntimeConfigProvider(config.DefaultConfig(), newMockConfigManager(t, manager))
187199
188200 provider.OnChange(func(snapshot port.RuntimeConfigSnapshot) {
189201 mutateNestedRuntimeConfigSnapshot(snapshot)
@@ -246,7 +258,7 @@ func mutateNestedRuntimeConfigSnapshot(snapshot port.RuntimeConfigSnapshot) {
246258 snapshot.UI.Session.SessionMode.Actions["added-session"] = entity.ActionBinding{Keys: []string{"added"}}
247259 }
248260
249-func mutateActionBinding(actions map[string]entity.ActionBinding, action string, key string) {
261+func mutateActionBinding(actions map[string]entity.ActionBinding, action, key string) {
250262 binding := actions[action]
251263 binding.Keys[0] = key
252264 actions[action] = binding
@@ -279,7 +291,7 @@ func assertNestedRuntimeConfigSnapshotUnchanged(t *testing.T, snapshot port.Runt
279291 assertMapEntryAbsent(t, snapshot.UI.Session.SessionMode.Actions, "added-session")
280292 }
281293
282-func assertActionBinding(t *testing.T, actions map[string]entity.ActionBinding, action string, wantKey string) {
294+func assertActionBinding(t *testing.T, actions map[string]entity.ActionBinding, action, wantKey string) {
283295 t.Helper()
284296
285297 binding, ok := actions[action]
M internal/cli/cmd/logs_test.go +17 -15
@@ -8,23 +8,13 @@ import (
88 "testing"
99 "time"
1010
11+ cmdmocks "github.com/bnema/dumber/internal/cli/cmd/mocks"
1112 "github.com/bnema/dumber/internal/domain/entity"
1213 "github.com/bnema/dumber/internal/logging"
14+ "github.com/stretchr/testify/mock"
1315 "github.com/stretchr/testify/require"
1416 )
1517
16-type fakeSessionMgr struct {
17- sessions []*entity.Session
18- err error
19-}
20-
21-func (f *fakeSessionMgr) GetRecentSessions(_ context.Context, _ int) ([]*entity.Session, error) {
22- if f.err != nil {
23- return nil, f.err
24- }
25- return f.sessions, nil
26-}
27-
2818 func TestGetSessionsMerged_DBAndFS(t *testing.T) {
2919 logDir := t.TempDir()
3020
@@ -34,7 +24,11 @@ func TestGetSessionsMerged_DBAndFS(t *testing.T) {
3424 require.NoError(t, os.WriteFile(filepath.Join(logDir, logging.SessionFilename(string(s1.ID))), []byte("hi\n"), 0o600))
3525 require.NoError(t, os.WriteFile(filepath.Join(logDir, logging.SessionFilename("legacy_only")), []byte("legacy\n"), 0o600))
3626
37- mgr := &fakeSessionMgr{sessions: []*entity.Session{s1, s2}}
27+ mgr := cmdmocks.NewMockSessionManager(t)
28+ mgr.EXPECT().
29+ GetRecentSessions(mock.Anything, recentSessionsLimit).
30+ Return([]*entity.Session{s1, s2}, nil).
31+ Once()
3832
3933 merged, err := getSessionsMerged(context.Background(), mgr, logDir)
4034 require.NoError(t, err)
@@ -54,7 +48,11 @@ func TestGetSessionsMerged_MissingTableFallsBack(t *testing.T) {
5448 logDir := t.TempDir()
5549 require.NoError(t, os.WriteFile(filepath.Join(logDir, logging.SessionFilename("legacy_only")), []byte("legacy\n"), 0o600))
5650
57- mgr := &fakeSessionMgr{err: fmt.Errorf("no such table: sessions")}
51+ mgr := cmdmocks.NewMockSessionManager(t)
52+ mgr.EXPECT().
53+ GetRecentSessions(mock.Anything, recentSessionsLimit).
54+ Return(nil, fmt.Errorf("no such table: sessions")).
55+ Once()
5856 merged, err := getSessionsMerged(context.Background(), mgr, logDir)
5957 require.NoError(t, err)
6058 require.Len(t, merged, 1)
@@ -65,7 +63,11 @@ func TestFindSession_DBShortIDMatch(t *testing.T) {
6563 logDir := t.TempDir()
6664
6765 s1 := &entity.Session{ID: entity.SessionID("20251217_205106_a7b3"), Type: entity.SessionTypeBrowser, StartedAt: time.Date(2025, 12, 17, 20, 51, 6, 0, time.UTC)}
68- mgr := &fakeSessionMgr{sessions: []*entity.Session{s1}}
66+ mgr := cmdmocks.NewMockSessionManager(t)
67+ mgr.EXPECT().
68+ GetRecentSessions(mock.Anything, recentSessionsLimit).
69+ Return([]*entity.Session{s1}, nil).
70+ Once()
6971
7072 info, err := findSession(context.Background(), mgr, logDir, "a7b3")
7173 require.NoError(t, err)
A internal/cli/cmd/mocks/logs.go +107 -0
@@ -0,0 +1,107 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "github.com/bnema/dumber/internal/domain/entity"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewMockSessionManager creates a new instance of MockSessionManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewMockSessionManager(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *MockSessionManager {
20+ mock := &MockSessionManager{}
21+ mock.Mock.Test(t)
22+
23+ t.Cleanup(func() { mock.AssertExpectations(t) })
24+
25+ return mock
26+}
27+
28+// MockSessionManager is an autogenerated mock type for the sessionManager type
29+type MockSessionManager struct {
30+ mock.Mock
31+}
32+
33+type MockSessionManager_Expecter struct {
34+ mock *mock.Mock
35+}
36+
37+func (_m *MockSessionManager) EXPECT() *MockSessionManager_Expecter {
38+ return &MockSessionManager_Expecter{mock: &_m.Mock}
39+}
40+
41+// GetRecentSessions provides a mock function for the type MockSessionManager
42+func (_mock *MockSessionManager) GetRecentSessions(ctx context.Context, limit int) ([]*entity.Session, error) {
43+ ret := _mock.Called(ctx, limit)
44+
45+ if len(ret) == 0 {
46+ panic("no return value specified for GetRecentSessions")
47+ }
48+
49+ var r0 []*entity.Session
50+ var r1 error
51+ if returnFunc, ok := ret.Get(0).(func(context.Context, int) ([]*entity.Session, error)); ok {
52+ return returnFunc(ctx, limit)
53+ }
54+ if returnFunc, ok := ret.Get(0).(func(context.Context, int) []*entity.Session); ok {
55+ r0 = returnFunc(ctx, limit)
56+ } else {
57+ if ret.Get(0) != nil {
58+ r0 = ret.Get(0).([]*entity.Session)
59+ }
60+ }
61+ if returnFunc, ok := ret.Get(1).(func(context.Context, int) error); ok {
62+ r1 = returnFunc(ctx, limit)
63+ } else {
64+ r1 = ret.Error(1)
65+ }
66+ return r0, r1
67+}
68+
69+// MockSessionManager_GetRecentSessions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRecentSessions'
70+type MockSessionManager_GetRecentSessions_Call struct {
71+ *mock.Call
72+}
73+
74+// GetRecentSessions is a helper method to define mock.On call
75+// - ctx context.Context
76+// - limit int
77+func (_e *MockSessionManager_Expecter) GetRecentSessions(ctx any, limit any) *MockSessionManager_GetRecentSessions_Call {
78+ return &MockSessionManager_GetRecentSessions_Call{Call: _e.mock.On("GetRecentSessions", ctx, limit)}
79+}
80+
81+func (_c *MockSessionManager_GetRecentSessions_Call) Run(run func(ctx context.Context, limit int)) *MockSessionManager_GetRecentSessions_Call {
82+ _c.Call.Run(func(args mock.Arguments) {
83+ var arg0 context.Context
84+ if args[0] != nil {
85+ arg0 = args[0].(context.Context)
86+ }
87+ var arg1 int
88+ if args[1] != nil {
89+ arg1 = args[1].(int)
90+ }
91+ run(
92+ arg0,
93+ arg1,
94+ )
95+ })
96+ return _c
97+}
98+
99+func (_c *MockSessionManager_GetRecentSessions_Call) Return(sessions []*entity.Session, err error) *MockSessionManager_GetRecentSessions_Call {
100+ _c.Call.Return(sessions, err)
101+ return _c
102+}
103+
104+func (_c *MockSessionManager_GetRecentSessions_Call) RunAndReturn(run func(ctx context.Context, limit int) ([]*entity.Session, error)) *MockSessionManager_GetRecentSessions_Call {
105+ _c.Call.Return(run)
106+ return _c
107+}
A internal/infrastructure/audio/pipewire/mocks/player.go +257 -0
@@ -0,0 +1,257 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "github.com/bnema/purego-pipewire/pipewire"
9+ mock "github.com/stretchr/testify/mock"
10+)
11+
12+// NewMockPlayer creates a new instance of MockPlayer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
13+// The first argument is typically a *testing.T value.
14+func NewMockPlayer(t interface {
15+ mock.TestingT
16+ Cleanup(func())
17+}) *MockPlayer {
18+ mock := &MockPlayer{}
19+ mock.Mock.Test(t)
20+
21+ t.Cleanup(func() { mock.AssertExpectations(t) })
22+
23+ return mock
24+}
25+
26+// MockPlayer is an autogenerated mock type for the Player type
27+type MockPlayer struct {
28+ mock.Mock
29+}
30+
31+type MockPlayer_Expecter struct {
32+ mock *mock.Mock
33+}
34+
35+func (_m *MockPlayer) EXPECT() *MockPlayer_Expecter {
36+ return &MockPlayer_Expecter{mock: &_m.Mock}
37+}
38+
39+// Close provides a mock function for the type MockPlayer
40+func (_mock *MockPlayer) Close() error {
41+ ret := _mock.Called()
42+
43+ if len(ret) == 0 {
44+ panic("no return value specified for Close")
45+ }
46+
47+ var r0 error
48+ if returnFunc, ok := ret.Get(0).(func() error); ok {
49+ r0 = returnFunc()
50+ } else {
51+ r0 = ret.Error(0)
52+ }
53+ return r0
54+}
55+
56+// MockPlayer_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
57+type MockPlayer_Close_Call struct {
58+ *mock.Call
59+}
60+
61+// Close is a helper method to define mock.On call
62+func (_e *MockPlayer_Expecter) Close() *MockPlayer_Close_Call {
63+ return &MockPlayer_Close_Call{Call: _e.mock.On("Close")}
64+}
65+
66+func (_c *MockPlayer_Close_Call) Run(run func()) *MockPlayer_Close_Call {
67+ _c.Call.Run(func(args mock.Arguments) {
68+ run()
69+ })
70+ return _c
71+}
72+
73+func (_c *MockPlayer_Close_Call) Return(err error) *MockPlayer_Close_Call {
74+ _c.Call.Return(err)
75+ return _c
76+}
77+
78+func (_c *MockPlayer_Close_Call) RunAndReturn(run func() error) *MockPlayer_Close_Call {
79+ _c.Call.Return(run)
80+ return _c
81+}
82+
83+// Pause provides a mock function for the type MockPlayer
84+func (_mock *MockPlayer) Pause() error {
85+ ret := _mock.Called()
86+
87+ if len(ret) == 0 {
88+ panic("no return value specified for Pause")
89+ }
90+
91+ var r0 error
92+ if returnFunc, ok := ret.Get(0).(func() error); ok {
93+ r0 = returnFunc()
94+ } else {
95+ r0 = ret.Error(0)
96+ }
97+ return r0
98+}
99+
100+// MockPlayer_Pause_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Pause'
101+type MockPlayer_Pause_Call struct {
102+ *mock.Call
103+}
104+
105+// Pause is a helper method to define mock.On call
106+func (_e *MockPlayer_Expecter) Pause() *MockPlayer_Pause_Call {
107+ return &MockPlayer_Pause_Call{Call: _e.mock.On("Pause")}
108+}
109+
110+func (_c *MockPlayer_Pause_Call) Run(run func()) *MockPlayer_Pause_Call {
111+ _c.Call.Run(func(args mock.Arguments) {
112+ run()
113+ })
114+ return _c
115+}
116+
117+func (_c *MockPlayer_Pause_Call) Return(err error) *MockPlayer_Pause_Call {
118+ _c.Call.Return(err)
119+ return _c
120+}
121+
122+func (_c *MockPlayer_Pause_Call) RunAndReturn(run func() error) *MockPlayer_Pause_Call {
123+ _c.Call.Return(run)
124+ return _c
125+}
126+
127+// Start provides a mock function for the type MockPlayer
128+func (_mock *MockPlayer) Start() error {
129+ ret := _mock.Called()
130+
131+ if len(ret) == 0 {
132+ panic("no return value specified for Start")
133+ }
134+
135+ var r0 error
136+ if returnFunc, ok := ret.Get(0).(func() error); ok {
137+ r0 = returnFunc()
138+ } else {
139+ r0 = ret.Error(0)
140+ }
141+ return r0
142+}
143+
144+// MockPlayer_Start_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Start'
145+type MockPlayer_Start_Call struct {
146+ *mock.Call
147+}
148+
149+// Start is a helper method to define mock.On call
150+func (_e *MockPlayer_Expecter) Start() *MockPlayer_Start_Call {
151+ return &MockPlayer_Start_Call{Call: _e.mock.On("Start")}
152+}
153+
154+func (_c *MockPlayer_Start_Call) Run(run func()) *MockPlayer_Start_Call {
155+ _c.Call.Run(func(args mock.Arguments) {
156+ run()
157+ })
158+ return _c
159+}
160+
161+func (_c *MockPlayer_Start_Call) Return(err error) *MockPlayer_Start_Call {
162+ _c.Call.Return(err)
163+ return _c
164+}
165+
166+func (_c *MockPlayer_Start_Call) RunAndReturn(run func() error) *MockPlayer_Start_Call {
167+ _c.Call.Return(run)
168+ return _c
169+}
170+
171+// State provides a mock function for the type MockPlayer
172+func (_mock *MockPlayer) State() pipewire.PlayerState {
173+ ret := _mock.Called()
174+
175+ if len(ret) == 0 {
176+ panic("no return value specified for State")
177+ }
178+
179+ var r0 pipewire.PlayerState
180+ if returnFunc, ok := ret.Get(0).(func() pipewire.PlayerState); ok {
181+ r0 = returnFunc()
182+ } else {
183+ r0 = ret.Get(0).(pipewire.PlayerState)
184+ }
185+ return r0
186+}
187+
188+// MockPlayer_State_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'State'
189+type MockPlayer_State_Call struct {
190+ *mock.Call
191+}
192+
193+// State is a helper method to define mock.On call
194+func (_e *MockPlayer_Expecter) State() *MockPlayer_State_Call {
195+ return &MockPlayer_State_Call{Call: _e.mock.On("State")}
196+}
197+
198+func (_c *MockPlayer_State_Call) Run(run func()) *MockPlayer_State_Call {
199+ _c.Call.Run(func(args mock.Arguments) {
200+ run()
201+ })
202+ return _c
203+}
204+
205+func (_c *MockPlayer_State_Call) Return(playerState pipewire.PlayerState) *MockPlayer_State_Call {
206+ _c.Call.Return(playerState)
207+ return _c
208+}
209+
210+func (_c *MockPlayer_State_Call) RunAndReturn(run func() pipewire.PlayerState) *MockPlayer_State_Call {
211+ _c.Call.Return(run)
212+ return _c
213+}
214+
215+// Stop provides a mock function for the type MockPlayer
216+func (_mock *MockPlayer) Stop() error {
217+ ret := _mock.Called()
218+
219+ if len(ret) == 0 {
220+ panic("no return value specified for Stop")
221+ }
222+
223+ var r0 error
224+ if returnFunc, ok := ret.Get(0).(func() error); ok {
225+ r0 = returnFunc()
226+ } else {
227+ r0 = ret.Error(0)
228+ }
229+ return r0
230+}
231+
232+// MockPlayer_Stop_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Stop'
233+type MockPlayer_Stop_Call struct {
234+ *mock.Call
235+}
236+
237+// Stop is a helper method to define mock.On call
238+func (_e *MockPlayer_Expecter) Stop() *MockPlayer_Stop_Call {
239+ return &MockPlayer_Stop_Call{Call: _e.mock.On("Stop")}
240+}
241+
242+func (_c *MockPlayer_Stop_Call) Run(run func()) *MockPlayer_Stop_Call {
243+ _c.Call.Run(func(args mock.Arguments) {
244+ run()
245+ })
246+ return _c
247+}
248+
249+func (_c *MockPlayer_Stop_Call) Return(err error) *MockPlayer_Stop_Call {
250+ _c.Call.Return(err)
251+ return _c
252+}
253+
254+func (_c *MockPlayer_Stop_Call) RunAndReturn(run func() error) *MockPlayer_Stop_Call {
255+ _c.Call.Return(run)
256+ return _c
257+}
M internal/infrastructure/audio/pipewire/output_test.go +36 -21
@@ -8,15 +8,15 @@ import (
88 "testing"
99
1010 "github.com/bnema/dumber/internal/application/port"
11+ pipewiremocks "github.com/bnema/dumber/internal/infrastructure/audio/pipewire/mocks"
1112 pwpipewire "github.com/bnema/purego-pipewire/pipewire"
1213 "github.com/stretchr/testify/assert"
1314 "github.com/stretchr/testify/require"
1415 )
1516
16-// --- Test doubles ---
17+type playerRecorder struct {
18+ *pipewiremocks.MockPlayer
1719
18-// fakePlayer is a test double for purego-pipewire Player.
19-type fakePlayer struct {
2020 started bool
2121 paused bool
2222 stopped bool
@@ -28,7 +28,19 @@ type fakePlayer struct {
2828 closeErr error
2929 }
3030
31-func (p *fakePlayer) Start() error {
31+func newPlayerRecorder(t *testing.T) *playerRecorder {
32+ t.Helper()
33+
34+ recorder := &playerRecorder{MockPlayer: pipewiremocks.NewMockPlayer(t)}
35+ recorder.EXPECT().Start().RunAndReturn(recorder.start).Maybe()
36+ recorder.EXPECT().Pause().RunAndReturn(recorder.pause).Maybe()
37+ recorder.EXPECT().Stop().RunAndReturn(recorder.stop).Maybe()
38+ recorder.EXPECT().Close().RunAndReturn(recorder.close).Maybe()
39+ recorder.EXPECT().State().RunAndReturn(recorder.currentState).Maybe()
40+ return recorder
41+}
42+
43+func (p *playerRecorder) start() error {
3244 if p.startErr != nil {
3345 return p.startErr
3446 }
@@ -37,13 +49,13 @@ func (p *fakePlayer) Start() error {
3749 return nil
3850 }
3951
40-func (p *fakePlayer) Pause() error {
52+func (p *playerRecorder) pause() error {
4153 p.paused = true
4254 p.state = pwpipewire.PlayerStatePaused
4355 return nil
4456 }
4557
46-func (p *fakePlayer) Stop() error {
58+func (p *playerRecorder) stop() error {
4759 if p.stopErr != nil {
4860 return p.stopErr
4961 }
@@ -52,7 +64,7 @@ func (p *fakePlayer) Stop() error {
5264 return nil
5365 }
5466
55-func (p *fakePlayer) Close() error {
67+func (p *playerRecorder) close() error {
5668 if p.closeErr != nil {
5769 return p.closeErr
5870 }
@@ -61,7 +73,7 @@ func (p *fakePlayer) Close() error {
6173 return nil
6274 }
6375
64-func (p *fakePlayer) State() pwpipewire.PlayerState {
76+func (p *playerRecorder) currentState() pwpipewire.PlayerState {
6577 return p.state
6678 }
6779
@@ -69,7 +81,7 @@ func (p *fakePlayer) State() pwpipewire.PlayerState {
6981
7082 func TestFactory_NewStream_ValidFormat_CreatesStream(t *testing.T) {
7183 var capturedConfig pwpipewire.PlayerConfig
72- player := &fakePlayer{}
84+ player := newPlayerRecorder(t)
7385
7486 factory := &Factory{
7587 createPlayer: func(config pwpipewire.PlayerConfig, _ pwpipewire.PlayerCallbacks) (pwpipewire.Player, error) {
@@ -175,7 +187,8 @@ func TestFactory_NewStream_PlayerCreationFails_ReturnsError(t *testing.T) {
175187 }
176188
177189 func TestFactory_NewStream_PlayerStartFails_ClosesAndReturnsError(t *testing.T) {
178- player := &fakePlayer{startErr: errors.New("start failed")}
190+ player := newPlayerRecorder(t)
191+ player.startErr = errors.New("start failed")
179192 factory := &Factory{
180193 createPlayer: func(_ pwpipewire.PlayerConfig, _ pwpipewire.PlayerCallbacks) (pwpipewire.Player, error) {
181194 return player, nil
@@ -224,7 +237,7 @@ func TestFactory_NewStream_MultipleFormats_Accepted(t *testing.T) {
224237 assert.Equal(t, fmt.SampleRate, config.SampleRate)
225238 assert.Equal(t, fmt.ChannelCount, config.Channels)
226239 assert.Equal(t, fmt.FramesPerBuffer, config.FramesPerBuffer)
227- return &fakePlayer{}, nil
240+ return newPlayerRecorder(t), nil
228241 },
229242 }
230243
@@ -244,7 +257,7 @@ func TestFactory_ImplementsPortInterface(_ *testing.T) {
244257 // --- Stream adapter tests ---
245258
246259 func TestPlayerStreamAdapter_Write_SendsToBuffer(t *testing.T) {
247- player := &fakePlayer{}
260+ player := newPlayerRecorder(t)
248261 adapter := &playerStreamAdapter{
249262 player: player,
250263 buf: make(chan [][]float32, 4),
@@ -265,7 +278,7 @@ func TestPlayerStreamAdapter_Write_SendsToBuffer(t *testing.T) {
265278 }
266279
267280 func TestPlayerStreamAdapter_Write_AfterClose_ReturnsError(t *testing.T) {
268- player := &fakePlayer{}
281+ player := newPlayerRecorder(t)
269282 adapter := &playerStreamAdapter{
270283 player: player,
271284 buf: make(chan [][]float32, 4),
@@ -279,7 +292,7 @@ func TestPlayerStreamAdapter_Write_AfterClose_ReturnsError(t *testing.T) {
279292 }
280293
281294 func TestPlayerStreamAdapter_Close_StopsAndClosesPlayer(t *testing.T) {
282- player := &fakePlayer{}
295+ player := newPlayerRecorder(t)
283296 adapter := &playerStreamAdapter{
284297 player: player,
285298 buf: make(chan [][]float32, 4),
@@ -294,7 +307,7 @@ func TestPlayerStreamAdapter_Close_StopsAndClosesPlayer(t *testing.T) {
294307 }
295308
296309 func TestPlayerStreamAdapter_Close_IsIdempotent(t *testing.T) {
297- player := &fakePlayer{}
310+ player := newPlayerRecorder(t)
298311 adapter := &playerStreamAdapter{
299312 player: player,
300313 buf: make(chan [][]float32, 4),
@@ -306,7 +319,8 @@ func TestPlayerStreamAdapter_Close_IsIdempotent(t *testing.T) {
306319 }
307320
308321 func TestPlayerStreamAdapter_Close_PropagatesStopError(t *testing.T) {
309- player := &fakePlayer{stopErr: errors.New("stop error")}
322+ player := newPlayerRecorder(t)
323+ player.stopErr = errors.New("stop error")
310324 adapter := &playerStreamAdapter{
311325 player: player,
312326 buf: make(chan [][]float32, 4),
@@ -319,7 +333,8 @@ func TestPlayerStreamAdapter_Close_PropagatesStopError(t *testing.T) {
319333 }
320334
321335 func TestPlayerStreamAdapter_Close_PropagatesCloseError(t *testing.T) {
322- player := &fakePlayer{closeErr: errors.New("close error")}
336+ player := newPlayerRecorder(t)
337+ player.closeErr = errors.New("close error")
323338 adapter := &playerStreamAdapter{
324339 player: player,
325340 buf: make(chan [][]float32, 4),
@@ -427,7 +442,7 @@ func TestCopyToPCM_EmptySamples_ReturnsZero(t *testing.T) {
427442
428443 func TestPlayerStreamAdapter_Write_DropsWhenBufferFull(t *testing.T) {
429444 adapter := &playerStreamAdapter{
430- player: &fakePlayer{},
445+ player: newPlayerRecorder(t),
431446 buf: make(chan [][]float32, 4),
432447 closed: make(chan struct{}),
433448 }
@@ -450,7 +465,7 @@ func TestPlayerStreamAdapter_Write_DropsWhenBufferFull(t *testing.T) {
450465
451466 func TestPlayerStreamAdapter_Write_DropsLogOnlyOnFirstAndPowerOfTwo(t *testing.T) {
452467 adapter := &playerStreamAdapter{
453- player: &fakePlayer{},
468+ player: newPlayerRecorder(t),
454469 buf: make(chan [][]float32, 1), // tiny buffer to force drops
455470 closed: make(chan struct{}),
456471 }
@@ -470,7 +485,7 @@ func TestPlayerStreamAdapter_Write_DropsLogOnlyOnFirstAndPowerOfTwo(t *testing.T
470485 // --- Concurrent Write / Close tests ---
471486
472487 func TestPlayerStreamAdapter_ConcurrentWriteClose_NoRace(t *testing.T) {
473- player := &fakePlayer{}
488+ player := newPlayerRecorder(t)
474489 adapter := &playerStreamAdapter{
475490 player: player,
476491 buf: make(chan [][]float32, 4),
@@ -507,7 +522,7 @@ func TestNewFactory_Succeeds(t *testing.T) {
507522
508523 func TestStream_WriteFlowsThroughToFill(t *testing.T) {
509524 var capturedCallbacks pwpipewire.PlayerCallbacks
510- player := &fakePlayer{}
525+ player := newPlayerRecorder(t)
511526
512527 factory := &Factory{
513528 createPlayer: func(_ pwpipewire.PlayerConfig, callbacks pwpipewire.PlayerCallbacks) (pwpipewire.Player, error) {
M internal/infrastructure/cef/popup_opener_bridge_test.go +1 -1
@@ -66,6 +66,6 @@ func TestHandlePopupOpenerPostMessage_UsesPopupCommittedURIForSourceMetadata(t *
6666 DataKind: "json",
6767 TargetOrigin: "https://app.example",
6868 SourceOrigin: "https://evil.example",
69- SourceHref: "https://evil.example/fake",
69+ SourceHref: "https://evil.example/decoy",
7070 })
7171 }
M internal/infrastructure/clipboard/clipboard_test.go +55 -37
@@ -9,10 +9,14 @@ import (
99 "testing"
1010
1111 "github.com/bnema/dumber/internal/domain/entity"
12+ clipboardmocks "github.com/bnema/dumber/internal/infrastructure/clipboard/mocks"
13+ "github.com/stretchr/testify/mock"
1214 "github.com/stretchr/testify/require"
1315 )
1416
15-type fakeToolkitClipboard struct {
17+type toolkitClipboardRecorder struct {
18+ *clipboardmocks.MockToolkitClipboard
19+
1620 writeTextCalls int
1721 readTextCalls int
1822 writeImageCalls int
@@ -23,67 +27,81 @@ type fakeToolkitClipboard struct {
2327 writeImageErr error
2428 }
2529
26-func (f *fakeToolkitClipboard) WriteText(_ context.Context, text string) error {
27- f.writeTextCalls++
28- f.text = text
29- return f.writeTextErr
30+func newToolkitClipboardRecorder(t *testing.T) *toolkitClipboardRecorder {
31+ t.Helper()
32+
33+ recorder := &toolkitClipboardRecorder{MockToolkitClipboard: clipboardmocks.NewMockToolkitClipboard(t)}
34+ recorder.EXPECT().WriteText(mock.Anything, mock.Anything).RunAndReturn(recorder.writeText).Maybe()
35+ recorder.EXPECT().ReadText(mock.Anything).RunAndReturn(recorder.readText).Maybe()
36+ recorder.EXPECT().WriteImage(mock.Anything, mock.Anything).RunAndReturn(recorder.writeImage).Maybe()
37+ return recorder
38+}
39+
40+func (r *toolkitClipboardRecorder) writeText(_ context.Context, text string) error {
41+ r.writeTextCalls++
42+ r.text = text
43+ return r.writeTextErr
3044 }
3145
32-func (f *fakeToolkitClipboard) ReadText(_ context.Context) (string, error) {
33- f.readTextCalls++
34- return f.text, f.readTextErr
46+func (r *toolkitClipboardRecorder) readText(_ context.Context) (string, error) {
47+ r.readTextCalls++
48+ return r.text, r.readTextErr
3549 }
3650
37-func (f *fakeToolkitClipboard) WriteImage(_ context.Context, image entity.ImageData) error {
38- f.writeImageCalls++
39- f.image = image
40- return f.writeImageErr
51+func (r *toolkitClipboardRecorder) writeImage(_ context.Context, image entity.ImageData) error {
52+ r.writeImageCalls++
53+ r.image = image
54+ return r.writeImageErr
4155 }
4256
4357 func TestAdapter_ReadTextFallsBackToToolkitClipboardWhenNoSystemTool(t *testing.T) {
44- fake := &fakeToolkitClipboard{text: "toolkit text"}
45- adapter := &Adapter{toolkit: fake}
58+ toolkit := newToolkitClipboardRecorder(t)
59+ toolkit.text = "toolkit text"
60+ adapter := &Adapter{toolkit: toolkit}
4661
4762 text, err := adapter.ReadText(context.Background())
4863
4964 require.NoError(t, err)
5065 require.Equal(t, "toolkit text", text)
51- require.Equal(t, 1, fake.readTextCalls)
66+ require.Equal(t, 1, toolkit.readTextCalls)
5267 }
5368
5469 func TestAdapter_HasTextReturnsTrueWhenToolkitFallbackHasText(t *testing.T) {
55- fake := &fakeToolkitClipboard{text: "toolkit text"}
56- adapter := &Adapter{toolkit: fake}
70+ toolkit := newToolkitClipboardRecorder(t)
71+ toolkit.text = "toolkit text"
72+ adapter := &Adapter{toolkit: toolkit}
5773
5874 hasText, err := adapter.HasText(context.Background())
5975
6076 require.NoError(t, err)
6177 require.True(t, hasText)
62- require.Equal(t, 1, fake.readTextCalls)
78+ require.Equal(t, 1, toolkit.readTextCalls)
6379 }
6480
6581 func TestAdapter_WriteTextReturnsToolkitErrorWhenNoSystemTool(t *testing.T) {
6682 sentinel := errors.New("toolkit text failed")
67- fake := &fakeToolkitClipboard{writeTextErr: sentinel}
68- adapter := &Adapter{toolkit: fake}
83+ toolkit := newToolkitClipboardRecorder(t)
84+ toolkit.writeTextErr = sentinel
85+ adapter := &Adapter{toolkit: toolkit}
6986
7087 err := adapter.WriteText(context.Background(), "hello")
7188
7289 require.Equal(t, sentinel, err)
73- require.Equal(t, 1, fake.writeTextCalls)
74- require.Equal(t, "hello", fake.text)
90+ require.Equal(t, 1, toolkit.writeTextCalls)
91+ require.Equal(t, "hello", toolkit.text)
7592 }
7693
7794 func TestAdapter_WriteImageReturnsToolkitErrorWhenNoSystemTool(t *testing.T) {
7895 sentinel := errors.New("toolkit image failed")
79- fake := &fakeToolkitClipboard{writeImageErr: sentinel}
80- adapter := &Adapter{toolkit: fake}
96+ toolkit := newToolkitClipboardRecorder(t)
97+ toolkit.writeImageErr = sentinel
98+ adapter := &Adapter{toolkit: toolkit}
8199
82100 err := adapter.WriteImage(context.Background(), entity.ImageData{Bytes: []byte{1}, MimeType: "image/png"})
83101
84102 require.Equal(t, sentinel, err)
85- require.Equal(t, 1, fake.writeImageCalls)
86- require.Equal(t, entity.ImageData{Bytes: []byte{1}, MimeType: "image/png"}, fake.image)
103+ require.Equal(t, 1, toolkit.writeImageCalls)
104+ require.Equal(t, entity.ImageData{Bytes: []byte{1}, MimeType: "image/png"}, toolkit.image)
87105 }
88106
89107 func TestNew_FallsBackToToolkitClipboardWhenSystemToolsUnavailable(t *testing.T) {
@@ -94,9 +112,9 @@ func TestNew_FallsBackToToolkitClipboardWhenSystemToolsUnavailable(t *testing.T)
94112 lookPath = oldLookPath
95113 })
96114
97- fake := &fakeToolkitClipboard{}
115+ toolkit := newToolkitClipboardRecorder(t)
98116 newToolkitClipboard = func() toolkitClipboard {
99- return fake
117+ return toolkit
100118 }
101119 lookPath = func(_ string) (string, error) {
102120 return "", errors.New("missing")
@@ -107,8 +125,8 @@ func TestNew_FallsBackToToolkitClipboardWhenSystemToolsUnavailable(t *testing.T)
107125
108126 adapter := New().(*Adapter)
109127 require.NoError(t, adapter.WriteImage(context.Background(), entity.ImageData{Bytes: []byte{1, 2, 3}}))
110- require.Equal(t, 1, fake.writeImageCalls)
111- require.Equal(t, []byte{1, 2, 3}, fake.image.Bytes)
128+ require.Equal(t, 1, toolkit.writeImageCalls)
129+ require.Equal(t, []byte{1, 2, 3}, toolkit.image.Bytes)
112130 }
113131
114132 func TestAdapter_WriteImagePrefersToolkitClipboardWhenAvailable(t *testing.T) {
@@ -123,14 +141,14 @@ func TestAdapter_WriteImagePrefersToolkitClipboardWhenAvailable(t *testing.T) {
123141 script := "#!/bin/sh\ncat > \"" + stdinPath + "\"\n"
124142 require.NoError(t, os.WriteFile(scriptPath, []byte(script), 0o755))
125143
126- fake := &fakeToolkitClipboard{}
127- newToolkitClipboard = func() toolkitClipboard { return fake }
144+ toolkit := newToolkitClipboardRecorder(t)
145+ newToolkitClipboard = func() toolkitClipboard { return toolkit }
128146 adapter := &Adapter{copyCmd: scriptPath}
129147 image := entity.ImageData{Bytes: []byte{1, 2, 3}, MimeType: "image/png"}
130148
131149 require.NoError(t, adapter.WriteImage(context.Background(), image))
132- require.Equal(t, 1, fake.writeImageCalls)
133- require.Equal(t, image, fake.image)
150+ require.Equal(t, 1, toolkit.writeImageCalls)
151+ require.Equal(t, image, toolkit.image)
134152
135153 _, err := os.Stat(stdinPath)
136154 require.Error(t, err)
@@ -193,14 +211,14 @@ func TestAdapter_WriteImageRejectsEmptyBytes(t *testing.T) {
193211 lookPath = oldLookPath
194212 })
195213
196- fake := &fakeToolkitClipboard{}
197- newToolkitClipboard = func() toolkitClipboard { return fake }
214+ toolkit := newToolkitClipboardRecorder(t)
215+ newToolkitClipboard = func() toolkitClipboard { return toolkit }
198216 lookPath = func(_ string) (string, error) { return "/usr/bin/clipboard", nil }
199217
200218 err := New().(*Adapter).WriteImage(context.Background(), entity.ImageData{})
201219 require.Error(t, err)
202220 require.Contains(t, err.Error(), "empty image data")
203- require.Zero(t, fake.writeImageCalls)
221+ require.Zero(t, toolkit.writeImageCalls)
204222 }
205223
206224 func TestAdapter_WriteImageWithCommandUsesImageMimeType(t *testing.T) {
A internal/infrastructure/clipboard/mocks/clipboard.go +213 -0
@@ -0,0 +1,213 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "github.com/bnema/dumber/internal/domain/entity"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewMockToolkitClipboard creates a new instance of MockToolkitClipboard. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewMockToolkitClipboard(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *MockToolkitClipboard {
20+ mock := &MockToolkitClipboard{}
21+ mock.Mock.Test(t)
22+
23+ t.Cleanup(func() { mock.AssertExpectations(t) })
24+
25+ return mock
26+}
27+
28+// MockToolkitClipboard is an autogenerated mock type for the toolkitClipboard type
29+type MockToolkitClipboard struct {
30+ mock.Mock
31+}
32+
33+type MockToolkitClipboard_Expecter struct {
34+ mock *mock.Mock
35+}
36+
37+func (_m *MockToolkitClipboard) EXPECT() *MockToolkitClipboard_Expecter {
38+ return &MockToolkitClipboard_Expecter{mock: &_m.Mock}
39+}
40+
41+// ReadText provides a mock function for the type MockToolkitClipboard
42+func (_mock *MockToolkitClipboard) ReadText(ctx context.Context) (string, error) {
43+ ret := _mock.Called(ctx)
44+
45+ if len(ret) == 0 {
46+ panic("no return value specified for ReadText")
47+ }
48+
49+ var r0 string
50+ var r1 error
51+ if returnFunc, ok := ret.Get(0).(func(context.Context) (string, error)); ok {
52+ return returnFunc(ctx)
53+ }
54+ if returnFunc, ok := ret.Get(0).(func(context.Context) string); ok {
55+ r0 = returnFunc(ctx)
56+ } else {
57+ r0 = ret.Get(0).(string)
58+ }
59+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
60+ r1 = returnFunc(ctx)
61+ } else {
62+ r1 = ret.Error(1)
63+ }
64+ return r0, r1
65+}
66+
67+// MockToolkitClipboard_ReadText_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadText'
68+type MockToolkitClipboard_ReadText_Call struct {
69+ *mock.Call
70+}
71+
72+// ReadText is a helper method to define mock.On call
73+// - ctx context.Context
74+func (_e *MockToolkitClipboard_Expecter) ReadText(ctx any) *MockToolkitClipboard_ReadText_Call {
75+ return &MockToolkitClipboard_ReadText_Call{Call: _e.mock.On("ReadText", ctx)}
76+}
77+
78+func (_c *MockToolkitClipboard_ReadText_Call) Run(run func(ctx context.Context)) *MockToolkitClipboard_ReadText_Call {
79+ _c.Call.Run(func(args mock.Arguments) {
80+ var arg0 context.Context
81+ if args[0] != nil {
82+ arg0 = args[0].(context.Context)
83+ }
84+ run(
85+ arg0,
86+ )
87+ })
88+ return _c
89+}
90+
91+func (_c *MockToolkitClipboard_ReadText_Call) Return(s string, err error) *MockToolkitClipboard_ReadText_Call {
92+ _c.Call.Return(s, err)
93+ return _c
94+}
95+
96+func (_c *MockToolkitClipboard_ReadText_Call) RunAndReturn(run func(ctx context.Context) (string, error)) *MockToolkitClipboard_ReadText_Call {
97+ _c.Call.Return(run)
98+ return _c
99+}
100+
101+// WriteImage provides a mock function for the type MockToolkitClipboard
102+func (_mock *MockToolkitClipboard) WriteImage(ctx context.Context, image entity.ImageData) error {
103+ ret := _mock.Called(ctx, image)
104+
105+ if len(ret) == 0 {
106+ panic("no return value specified for WriteImage")
107+ }
108+
109+ var r0 error
110+ if returnFunc, ok := ret.Get(0).(func(context.Context, entity.ImageData) error); ok {
111+ r0 = returnFunc(ctx, image)
112+ } else {
113+ r0 = ret.Error(0)
114+ }
115+ return r0
116+}
117+
118+// MockToolkitClipboard_WriteImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteImage'
119+type MockToolkitClipboard_WriteImage_Call struct {
120+ *mock.Call
121+}
122+
123+// WriteImage is a helper method to define mock.On call
124+// - ctx context.Context
125+// - image entity.ImageData
126+func (_e *MockToolkitClipboard_Expecter) WriteImage(ctx any, image any) *MockToolkitClipboard_WriteImage_Call {
127+ return &MockToolkitClipboard_WriteImage_Call{Call: _e.mock.On("WriteImage", ctx, image)}
128+}
129+
130+func (_c *MockToolkitClipboard_WriteImage_Call) Run(run func(ctx context.Context, image entity.ImageData)) *MockToolkitClipboard_WriteImage_Call {
131+ _c.Call.Run(func(args mock.Arguments) {
132+ var arg0 context.Context
133+ if args[0] != nil {
134+ arg0 = args[0].(context.Context)
135+ }
136+ var arg1 entity.ImageData
137+ if args[1] != nil {
138+ arg1 = args[1].(entity.ImageData)
139+ }
140+ run(
141+ arg0,
142+ arg1,
143+ )
144+ })
145+ return _c
146+}
147+
148+func (_c *MockToolkitClipboard_WriteImage_Call) Return(err error) *MockToolkitClipboard_WriteImage_Call {
149+ _c.Call.Return(err)
150+ return _c
151+}
152+
153+func (_c *MockToolkitClipboard_WriteImage_Call) RunAndReturn(run func(ctx context.Context, image entity.ImageData) error) *MockToolkitClipboard_WriteImage_Call {
154+ _c.Call.Return(run)
155+ return _c
156+}
157+
158+// WriteText provides a mock function for the type MockToolkitClipboard
159+func (_mock *MockToolkitClipboard) WriteText(ctx context.Context, text string) error {
160+ ret := _mock.Called(ctx, text)
161+
162+ if len(ret) == 0 {
163+ panic("no return value specified for WriteText")
164+ }
165+
166+ var r0 error
167+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
168+ r0 = returnFunc(ctx, text)
169+ } else {
170+ r0 = ret.Error(0)
171+ }
172+ return r0
173+}
174+
175+// MockToolkitClipboard_WriteText_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteText'
176+type MockToolkitClipboard_WriteText_Call struct {
177+ *mock.Call
178+}
179+
180+// WriteText is a helper method to define mock.On call
181+// - ctx context.Context
182+// - text string
183+func (_e *MockToolkitClipboard_Expecter) WriteText(ctx any, text any) *MockToolkitClipboard_WriteText_Call {
184+ return &MockToolkitClipboard_WriteText_Call{Call: _e.mock.On("WriteText", ctx, text)}
185+}
186+
187+func (_c *MockToolkitClipboard_WriteText_Call) Run(run func(ctx context.Context, text string)) *MockToolkitClipboard_WriteText_Call {
188+ _c.Call.Run(func(args mock.Arguments) {
189+ var arg0 context.Context
190+ if args[0] != nil {
191+ arg0 = args[0].(context.Context)
192+ }
193+ var arg1 string
194+ if args[1] != nil {
195+ arg1 = args[1].(string)
196+ }
197+ run(
198+ arg0,
199+ arg1,
200+ )
201+ })
202+ return _c
203+}
204+
205+func (_c *MockToolkitClipboard_WriteText_Call) Return(err error) *MockToolkitClipboard_WriteText_Call {
206+ _c.Call.Return(err)
207+ return _c
208+}
209+
210+func (_c *MockToolkitClipboard_WriteText_Call) RunAndReturn(run func(ctx context.Context, text string) error) *MockToolkitClipboard_WriteText_Call {
211+ _c.Call.Return(run)
212+ return _c
213+}
M internal/infrastructure/systemviewsbridge/client_test.go +46 -29
@@ -9,6 +9,8 @@ import (
99
1010 "github.com/bnema/dumber/internal/application/dto"
1111 "github.com/bnema/dumber/internal/application/port"
12+ systemviewsbridgemocks "github.com/bnema/dumber/internal/infrastructure/systemviewsbridge/mocks"
13+ "github.com/stretchr/testify/mock"
1214 )
1315
1416 func TestBuildMessageEnvelope(t *testing.T) {
@@ -41,8 +43,8 @@ func TestBuildMessageEnvelope(t *testing.T) {
4143 func TestClientSendPrefersNativeTransport(t *testing.T) {
4244 t.Parallel()
4345
44- native := &fakeTransport{available: true, response: []byte(`{"transport":"native"}`)}
45- fetch := &fakeTransport{available: true, response: []byte(`{"transport":"fetch"}`)}
46+ native := newTransportRecorder(t, true, []byte(`{"transport":"native"}`))
47+ fetch := newTransportRecorder(t, true, []byte(`{"transport":"fetch"}`))
4648
4749 client := NewClient(native, fetch)
4850 got, err := client.Send(context.Background(), "favorite_list", struct {
@@ -75,8 +77,8 @@ func TestClientSendPrefersNativeTransport(t *testing.T) {
7577 func TestClientSendFallsBackToFetchTransport(t *testing.T) {
7678 t.Parallel()
7779
78- native := &fakeTransport{available: false, response: []byte(`{"transport":"native"}`)}
79- fetch := &fakeTransport{available: true, response: []byte(`{"transport":"fetch"}`)}
80+ native := newTransportRecorder(t, false, []byte(`{"transport":"native"}`))
81+ fetch := newTransportRecorder(t, true, []byte(`{"transport":"fetch"}`))
8082
8183 client := NewClient(native, fetch)
8284 got, err := client.Send(context.Background(), "favorite_list", struct {
@@ -101,7 +103,7 @@ func TestClientSendFallsBackToFetchTransport(t *testing.T) {
101103 func TestClientTimelineDecodesEntries(t *testing.T) {
102104 t.Parallel()
103105
104- native := &fakeTransport{available: true, response: []byte(`{"requestId":"req-9","success":true,"data":[{"id":1,"url":"https://example.com","title":"Example"}]}`)}
106+ native := newTransportRecorder(t, true, []byte(`{"requestId":"req-9","success":true,"data":[{"id":1,"url":"https://example.com","title":"Example"}]}`))
105107 client := NewClient(native, nil)
106108
107109 entries, err := client.Timeline(context.Background(), 25, 0)
@@ -130,7 +132,7 @@ func TestClientTimelineDecodesEntries(t *testing.T) {
130132 func TestClientTimelineWindowOmitsZeroCursor(t *testing.T) {
131133 t.Parallel()
132134
133- native := &fakeTransport{available: true, response: []byte(`{"requestId":"req-window","success":true}`)}
135+ native := newTransportRecorder(t, true, []byte(`{"requestId":"req-window","success":true}`))
134136 client := NewClient(native, nil)
135137
136138 _, err := client.TimelineWindow(context.Background(), time.Time{}, "")
@@ -157,7 +159,7 @@ func TestClientTimelineWindowOmitsZeroCursor(t *testing.T) {
157159 func TestClientDeleteRangeRejectsEmptyRange(t *testing.T) {
158160 t.Parallel()
159161
160- transport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-range-empty","success":true}`)}
162+ transport := newTransportRecorder(t, true, []byte(`{"requestId":"req-range-empty","success":true}`))
161163 client := NewClient(transport, nil)
162164
163165 err := client.DeleteRange(context.Background(), " ")
@@ -172,7 +174,7 @@ func TestClientDeleteRangeRejectsEmptyRange(t *testing.T) {
172174 func TestClientDeleteRangeSendsRange(t *testing.T) {
173175 t.Parallel()
174176
175- native := &fakeTransport{available: true, response: []byte(`{"requestId":"req-10","success":true}`)}
177+ native := newTransportRecorder(t, true, []byte(`{"requestId":"req-10","success":true}`))
176178 client := NewClient(native, nil)
177179
178180 if err := client.DeleteRange(context.Background(), "week"); err != nil {
@@ -191,7 +193,7 @@ func TestClientDeleteRangeSendsRange(t *testing.T) {
191193 func TestClientListDecodesFavorites(t *testing.T) {
192194 t.Parallel()
193195
194- native := &fakeTransport{available: true, response: []byte(`{"requestId":"req-11","success":true,"data":[{"id":1,"url":"https://example.com","title":"Example"}]}`)}
196+ native := newTransportRecorder(t, true, []byte(`{"requestId":"req-11","success":true,"data":[{"id":1,"url":"https://example.com","title":"Example"}]}`))
195197 client := NewClient(native, nil)
196198
197199 favorites, err := client.List(context.Background())
@@ -217,7 +219,7 @@ func TestClientListDecodesFavorites(t *testing.T) {
217219 func TestClientListFoldersDecodesFolders(t *testing.T) {
218220 t.Parallel()
219221
220- native := &fakeTransport{available: true, response: []byte(`{"requestId":"req-12","success":true,"data":[{"id":9,"name":"Read Later"}]}`)}
222+ native := newTransportRecorder(t, true, []byte(`{"requestId":"req-12","success":true,"data":[{"id":9,"name":"Read Later"}]}`))
221223 client := NewClient(native, nil)
222224
223225 folders, err := client.ListFolders(context.Background())
@@ -243,7 +245,7 @@ func TestClientListFoldersDecodesFolders(t *testing.T) {
243245 func TestClientListTagsDecodesTags(t *testing.T) {
244246 t.Parallel()
245247
246- native := &fakeTransport{available: true, response: []byte(`{"requestId":"req-13","success":true,"data":[{"id":5,"name":"Go","color":"#00ff00"}]}`)}
248+ native := newTransportRecorder(t, true, []byte(`{"requestId":"req-13","success":true,"data":[{"id":5,"name":"Go","color":"#00ff00"}]}`))
247249 client := NewClient(native, nil)
248250
249251 tags, err := client.ListTags(context.Background())
@@ -271,7 +273,8 @@ func TestClientCurrentAndDefaultDecodeConfigPayload(t *testing.T) {
271273
272274 resp := []byte(`{"requestId":"req-14","success":true,"data":{"engine_type":"webkit","default_search_engine":"DuckDuckGo"}}`)
273275
274- currentClient := NewClient(&fakeTransport{available: true, response: resp}, nil)
276+ currentTransport := newTransportRecorder(t, true, resp)
277+ currentClient := NewClient(currentTransport, nil)
275278 current, err := currentClient.Current(context.Background())
276279 if err != nil {
277280 t.Fatalf("Current() error = %v", err)
@@ -281,14 +284,15 @@ func TestClientCurrentAndDefaultDecodeConfigPayload(t *testing.T) {
281284 }
282285
283286 var msg port.WebUIMessage
284- if unmarshalErr := json.Unmarshal(currentClient.native.(*fakeTransport).last, &msg); unmarshalErr != nil {
287+ if unmarshalErr := json.Unmarshal(currentTransport.last, &msg); unmarshalErr != nil {
285288 t.Fatalf("unmarshal sent envelope: %v", unmarshalErr)
286289 }
287290 if msg.Type != "/api/config" {
288291 t.Fatalf("sent type = %q, want %q", msg.Type, "/api/config")
289292 }
290293
291- defaultClient := NewClient(&fakeTransport{available: true, response: resp}, nil)
294+ defaultTransport := newTransportRecorder(t, true, resp)
295+ defaultClient := NewClient(defaultTransport, nil)
292296 defaultCfg, err := defaultClient.Default(context.Background())
293297 if err != nil {
294298 t.Fatalf("Default() error = %v", err)
@@ -297,7 +301,7 @@ func TestClientCurrentAndDefaultDecodeConfigPayload(t *testing.T) {
297301 t.Fatalf("Default() = %+v", defaultCfg)
298302 }
299303
300- if err := json.Unmarshal(defaultClient.native.(*fakeTransport).last, &msg); err != nil {
304+ if err := json.Unmarshal(defaultTransport.last, &msg); err != nil {
301305 t.Fatalf("unmarshal sent envelope: %v", err)
302306 }
303307 if msg.Type != "/api/config/default" {
@@ -372,7 +376,7 @@ func TestClientConfigActionsUseExpectedMessageTypes(t *testing.T) {
372376 t.Run(tt.name, func(t *testing.T) {
373377 t.Parallel()
374378
375- transport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-15","success":true,"data":{"status":"success","conflicts":[]}}`)}
379+ transport := newTransportRecorder(t, true, []byte(`{"requestId":"req-15","success":true,"data":{"status":"success","conflicts":[]}}`))
376380 if tt.name == "save config" || tt.name == "reset keybinding" || tt.name == "reset all keybindings" {
377381 transport.response = []byte(`{"requestId":"req-15","success":true}`)
378382 }
@@ -418,7 +422,7 @@ func TestClientCreateFolderIncludesParentID(t *testing.T) {
418422 t.Parallel()
419423
420424 parentID := int64(42)
421- transport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-16","success":true,"data":{"id":9,"name":"Nested","icon":"📁","parent_id":42}}`)}
425+ transport := newTransportRecorder(t, true, []byte(`{"requestId":"req-16","success":true,"data":{"id":9,"name":"Nested","icon":"📁","parent_id":42}}`))
422426 client := NewClient(transport, nil)
423427
424428 folder, err := client.CreateFolder(context.Background(), "Nested", " 📁 ", &parentID)
@@ -472,7 +476,7 @@ func TestClientCreateFolderIncludesParentID(t *testing.T) {
472476 func TestClientDomainStatsRejectsNegativeLimit(t *testing.T) {
473477 t.Parallel()
474478
475- transport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-17","success":true,"data":[]}`)}
479+ transport := newTransportRecorder(t, true, []byte(`{"requestId":"req-17","success":true,"data":[]}`))
476480 client := NewClient(transport, nil)
477481
478482 _, err := client.DomainStats(context.Background(), -1)
@@ -487,7 +491,7 @@ func TestClientDomainStatsRejectsNegativeLimit(t *testing.T) {
487491 func TestClientDeleteDomainRejectsEmptyDomain(t *testing.T) {
488492 t.Parallel()
489493
490- transport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-domain-empty","success":true}`)}
494+ transport := newTransportRecorder(t, true, []byte(`{"requestId":"req-domain-empty","success":true}`))
491495 client := NewClient(transport, nil)
492496
493497 err := client.DeleteDomain(context.Background(), " ")
@@ -502,7 +506,7 @@ func TestClientDeleteDomainRejectsEmptyDomain(t *testing.T) {
502506 func TestClientUpdateFolderTrimsIcon(t *testing.T) {
503507 t.Parallel()
504508
505- transport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-18","success":true}`)}
509+ transport := newTransportRecorder(t, true, []byte(`{"requestId":"req-18","success":true}`))
506510 client := NewClient(transport, nil)
507511
508512 if err := client.UpdateFolder(context.Background(), 9, "Read", " 📚 "); err != nil {
@@ -530,7 +534,7 @@ func TestClientUpdateFolderTrimsIcon(t *testing.T) {
530534 func TestClientUpdateFolderOmitsBlankIcon(t *testing.T) {
531535 t.Parallel()
532536
533- transport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-19","success":true}`)}
537+ transport := newTransportRecorder(t, true, []byte(`{"requestId":"req-19","success":true}`))
534538 client := NewClient(transport, nil)
535539
536540 if err := client.UpdateFolder(context.Background(), 9, "Read", " "); err != nil {
@@ -555,7 +559,7 @@ func TestClientUpdateFolderOmitsBlankIcon(t *testing.T) {
555559 func TestClientDeleteEntryRejectsInvalidID(t *testing.T) {
556560 t.Parallel()
557561
558- transport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-20","success":true}`)}
562+ transport := newTransportRecorder(t, true, []byte(`{"requestId":"req-20","success":true}`))
559563 client := NewClient(transport, nil)
560564
561565 err := client.DeleteEntry(context.Background(), 0)
@@ -570,7 +574,7 @@ func TestClientDeleteEntryRejectsInvalidID(t *testing.T) {
570574 func TestClientTagActionsTrimFields(t *testing.T) {
571575 t.Parallel()
572576
573- createTransport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-21","success":true,"data":{"id":5,"name":"Go","color":"#00add8"}}`)}
577+ createTransport := newTransportRecorder(t, true, []byte(`{"requestId":"req-21","success":true,"data":{"id":5,"name":"Go","color":"#00add8"}}`))
574578 client := NewClient(createTransport, nil)
575579 if _, err := client.CreateTag(context.Background(), " Go ", " #00add8 "); err != nil {
576580 t.Fatalf("CreateTag() error = %v", err)
@@ -590,7 +594,7 @@ func TestClientTagActionsTrimFields(t *testing.T) {
590594 t.Fatalf("CreateTag payload = %#v", createPayload)
591595 }
592596
593- updateTransport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-22","success":true}`)}
597+ updateTransport := newTransportRecorder(t, true, []byte(`{"requestId":"req-22","success":true}`))
594598 client = NewClient(updateTransport, nil)
595599 if err := client.UpdateTag(context.Background(), 5, " Go ", " "); err != nil {
596600 t.Fatalf("UpdateTag() error = %v", err)
@@ -610,16 +614,29 @@ func TestClientTagActionsTrimFields(t *testing.T) {
610614 }
611615 }
612616
613-type fakeTransport struct {
617+type transportRecorder struct {
618+ *systemviewsbridgemocks.MockTransport
619+
614620 available bool
615621 called bool
616622 last []byte
617623 response []byte
618624 }
619625
620-func (t *fakeTransport) Available() bool { return t.available }
626+func newTransportRecorder(t *testing.T, available bool, response []byte) *transportRecorder {
627+ t.Helper()
628+
629+ recorder := &transportRecorder{
630+ MockTransport: systemviewsbridgemocks.NewMockTransport(t),
631+ available: available,
632+ response: response,
633+ }
634+ recorder.EXPECT().Available().RunAndReturn(func() bool { return recorder.available }).Maybe()
635+ recorder.EXPECT().Send(mock.Anything, mock.Anything).RunAndReturn(recorder.send).Maybe()
636+ return recorder
637+}
621638
622-func (t *fakeTransport) Send(_ context.Context, body []byte) ([]byte, error) {
639+func (t *transportRecorder) send(_ context.Context, body []byte) ([]byte, error) {
623640 t.called = true
624641 t.last = append(t.last[:0], body...)
625642 return t.response, nil
@@ -641,7 +658,7 @@ func (t *blockingTransport) Send(ctx context.Context, _ []byte) ([]byte, error)
641658 func TestClientRejectsInvalidFavoriteMutationIDs(t *testing.T) {
642659 t.Parallel()
643660
644- transport := &fakeTransport{available: true, response: []byte(`{"requestId":"req","success":true}`)}
661+ transport := newTransportRecorder(t, true, []byte(`{"requestId":"req","success":true}`))
645662 client := NewClient(transport, nil)
646663
647664 checks := []struct {
@@ -679,7 +696,7 @@ func TestClientRejectsInvalidFavoriteMutationIDs(t *testing.T) {
679696 func TestClientRejectsNegativeHistoryLimits(t *testing.T) {
680697 t.Parallel()
681698
682- client := NewClient(&fakeTransport{available: true}, nil)
699+ client := NewClient(newTransportRecorder(t, true, nil), nil)
683700
684701 if _, err := client.Timeline(context.Background(), -1, 0); err == nil {
685702 t.Fatal("Timeline() error = nil, want error")
A internal/infrastructure/systemviewsbridge/mocks/client.go +150 -0
@@ -0,0 +1,150 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ mock "github.com/stretchr/testify/mock"
11+)
12+
13+// NewMockTransport creates a new instance of MockTransport. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
14+// The first argument is typically a *testing.T value.
15+func NewMockTransport(t interface {
16+ mock.TestingT
17+ Cleanup(func())
18+}) *MockTransport {
19+ mock := &MockTransport{}
20+ mock.Mock.Test(t)
21+
22+ t.Cleanup(func() { mock.AssertExpectations(t) })
23+
24+ return mock
25+}
26+
27+// MockTransport is an autogenerated mock type for the Transport type
28+type MockTransport struct {
29+ mock.Mock
30+}
31+
32+type MockTransport_Expecter struct {
33+ mock *mock.Mock
34+}
35+
36+func (_m *MockTransport) EXPECT() *MockTransport_Expecter {
37+ return &MockTransport_Expecter{mock: &_m.Mock}
38+}
39+
40+// Available provides a mock function for the type MockTransport
41+func (_mock *MockTransport) Available() bool {
42+ ret := _mock.Called()
43+
44+ if len(ret) == 0 {
45+ panic("no return value specified for Available")
46+ }
47+
48+ var r0 bool
49+ if returnFunc, ok := ret.Get(0).(func() bool); ok {
50+ r0 = returnFunc()
51+ } else {
52+ r0 = ret.Get(0).(bool)
53+ }
54+ return r0
55+}
56+
57+// MockTransport_Available_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Available'
58+type MockTransport_Available_Call struct {
59+ *mock.Call
60+}
61+
62+// Available is a helper method to define mock.On call
63+func (_e *MockTransport_Expecter) Available() *MockTransport_Available_Call {
64+ return &MockTransport_Available_Call{Call: _e.mock.On("Available")}
65+}
66+
67+func (_c *MockTransport_Available_Call) Run(run func()) *MockTransport_Available_Call {
68+ _c.Call.Run(func(args mock.Arguments) {
69+ run()
70+ })
71+ return _c
72+}
73+
74+func (_c *MockTransport_Available_Call) Return(b bool) *MockTransport_Available_Call {
75+ _c.Call.Return(b)
76+ return _c
77+}
78+
79+func (_c *MockTransport_Available_Call) RunAndReturn(run func() bool) *MockTransport_Available_Call {
80+ _c.Call.Return(run)
81+ return _c
82+}
83+
84+// Send provides a mock function for the type MockTransport
85+func (_mock *MockTransport) Send(ctx context.Context, body []byte) ([]byte, error) {
86+ ret := _mock.Called(ctx, body)
87+
88+ if len(ret) == 0 {
89+ panic("no return value specified for Send")
90+ }
91+
92+ var r0 []byte
93+ var r1 error
94+ if returnFunc, ok := ret.Get(0).(func(context.Context, []byte) ([]byte, error)); ok {
95+ return returnFunc(ctx, body)
96+ }
97+ if returnFunc, ok := ret.Get(0).(func(context.Context, []byte) []byte); ok {
98+ r0 = returnFunc(ctx, body)
99+ } else {
100+ if ret.Get(0) != nil {
101+ r0 = ret.Get(0).([]byte)
102+ }
103+ }
104+ if returnFunc, ok := ret.Get(1).(func(context.Context, []byte) error); ok {
105+ r1 = returnFunc(ctx, body)
106+ } else {
107+ r1 = ret.Error(1)
108+ }
109+ return r0, r1
110+}
111+
112+// MockTransport_Send_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Send'
113+type MockTransport_Send_Call struct {
114+ *mock.Call
115+}
116+
117+// Send is a helper method to define mock.On call
118+// - ctx context.Context
119+// - body []byte
120+func (_e *MockTransport_Expecter) Send(ctx any, body any) *MockTransport_Send_Call {
121+ return &MockTransport_Send_Call{Call: _e.mock.On("Send", ctx, body)}
122+}
123+
124+func (_c *MockTransport_Send_Call) Run(run func(ctx context.Context, body []byte)) *MockTransport_Send_Call {
125+ _c.Call.Run(func(args mock.Arguments) {
126+ var arg0 context.Context
127+ if args[0] != nil {
128+ arg0 = args[0].(context.Context)
129+ }
130+ var arg1 []byte
131+ if args[1] != nil {
132+ arg1 = args[1].([]byte)
133+ }
134+ run(
135+ arg0,
136+ arg1,
137+ )
138+ })
139+ return _c
140+}
141+
142+func (_c *MockTransport_Send_Call) Return(bytes []byte, err error) *MockTransport_Send_Call {
143+ _c.Call.Return(bytes, err)
144+ return _c
145+}
146+
147+func (_c *MockTransport_Send_Call) RunAndReturn(run func(ctx context.Context, body []byte) ([]byte, error)) *MockTransport_Send_Call {
148+ _c.Call.Return(run)
149+ return _c
150+}
M internal/infrastructure/updater/applier_test.go +1 -1
@@ -122,7 +122,7 @@ func TestApplier_StageUpdate(t *testing.T) {
122122 applier := NewApplier(tmpDir)
123123 ctx := context.Background()
124124
125- // Create a fake binary to stage
125+ // Create a dummy binary to stage
126126 srcBinary := filepath.Join(tmpDir, "new-binary")
127127 binaryContent := []byte("#!/bin/sh\necho 'new version'\n")
128128 require.NoError(t, os.WriteFile(srcBinary, binaryContent, 0o755))
M internal/ui/app_browser_launch_test.go +71 -71
@@ -2441,9 +2441,9 @@ func TestApp_ActiveWorkspaceUsesLastFocusedWindowTabsNotGlobalActiveTab(t *testi
24412441 }
24422442 }
24432443
2444-// fakeRecordingWebView is a complete port.WebView that records which navigation
2444+// recordingWebView is a complete port.WebView that records which navigation
24452445 // methods were called and with what arguments. It does not use reflect or unsafe.
2446-type fakeRecordingWebView struct {
2446+type recordingWebView struct {
24472447 id port.WebViewID
24482448
24492449 loadURICalled bool
@@ -2461,79 +2461,79 @@ type fakeRecordingWebView struct {
24612461 destroyCalls int
24622462 }
24632463
2464-func (f *fakeRecordingWebView) ID() port.WebViewID { return f.id }
2464+func (f *recordingWebView) ID() port.WebViewID { return f.id }
24652465
2466-func (f *fakeRecordingWebView) LoadURI(_ context.Context, uri string) error {
2466+func (f *recordingWebView) LoadURI(_ context.Context, uri string) error {
24672467 f.loadURICalled = true
24682468 f.loadURILastURI = uri
24692469 return nil
24702470 }
24712471
2472-func (f *fakeRecordingWebView) LoadHTML(_ context.Context, _, _ string) error {
2472+func (f *recordingWebView) LoadHTML(_ context.Context, _, _ string) error {
24732473 f.loadHTMLCalls++
24742474 return nil
24752475 }
24762476
2477-func (f *fakeRecordingWebView) Reload(_ context.Context) error {
2477+func (f *recordingWebView) Reload(_ context.Context) error {
24782478 f.reloadCalls++
24792479 return nil
24802480 }
24812481
2482-func (f *fakeRecordingWebView) ReloadBypassCache(_ context.Context) error {
2482+func (f *recordingWebView) ReloadBypassCache(_ context.Context) error {
24832483 f.reloadBypassCacheCalls++
24842484 return nil
24852485 }
24862486
2487-func (f *fakeRecordingWebView) Stop(_ context.Context) error {
2487+func (f *recordingWebView) Stop(_ context.Context) error {
24882488 f.stopCalls++
24892489 return nil
24902490 }
24912491
2492-func (f *fakeRecordingWebView) GoBack(_ context.Context) error {
2492+func (f *recordingWebView) GoBack(_ context.Context) error {
24932493 f.goBackCalls++
24942494 return nil
24952495 }
24962496
2497-func (f *fakeRecordingWebView) GoForward(_ context.Context) error {
2497+func (f *recordingWebView) GoForward(_ context.Context) error {
24982498 f.goForwardCalls++
24992499 return nil
25002500 }
25012501
2502-func (f *fakeRecordingWebView) State() port.WebViewState { return port.WebViewState{} }
2502+func (f *recordingWebView) State() port.WebViewState { return port.WebViewState{} }
25032503
2504-func (f *fakeRecordingWebView) URI() string { return f.loadURILastURI }
2505-func (f *fakeRecordingWebView) Title() string { return "" }
2504+func (f *recordingWebView) URI() string { return f.loadURILastURI }
2505+func (f *recordingWebView) Title() string { return "" }
25062506
2507-func (f *fakeRecordingWebView) IsLoading() bool { return false }
2508-func (f *fakeRecordingWebView) EstimatedProgress() float64 { return 0 }
2509-func (f *fakeRecordingWebView) CanGoBack() bool { return false }
2510-func (f *fakeRecordingWebView) CanGoForward() bool { return false }
2507+func (f *recordingWebView) IsLoading() bool { return false }
2508+func (f *recordingWebView) EstimatedProgress() float64 { return 0 }
2509+func (f *recordingWebView) CanGoBack() bool { return false }
2510+func (f *recordingWebView) CanGoForward() bool { return false }
25112511
2512-func (f *fakeRecordingWebView) SetZoomLevel(_ context.Context, level float64) error {
2512+func (f *recordingWebView) SetZoomLevel(_ context.Context, level float64) error {
25132513 f.setZoomLevelCalls++
25142514 f.setZoomLevelLastLevel = level
25152515 return nil
25162516 }
25172517
2518-func (f *fakeRecordingWebView) OpenDevTools() { f.openDevToolsCalls++ }
2519-func (f *fakeRecordingWebView) PrintPage() { f.printPageCalls++ }
2518+func (f *recordingWebView) OpenDevTools() { f.openDevToolsCalls++ }
2519+func (f *recordingWebView) PrintPage() { f.printPageCalls++ }
25202520
2521-func (f *fakeRecordingWebView) GetZoomLevel() float64 { return 1.0 }
2522-func (f *fakeRecordingWebView) GetFindController() port.FindController { return nil }
2523-func (f *fakeRecordingWebView) SetCallbacks(_ *port.WebViewCallbacks) {}
2524-func (f *fakeRecordingWebView) RunJavaScript(_ context.Context, _ string) {}
2525-func (f *fakeRecordingWebView) SetBackgroundColor(_, _, _, _ float64) {}
2526-func (f *fakeRecordingWebView) ResetBackgroundToDefault() {}
2527-func (f *fakeRecordingWebView) Favicon() port.Texture { return nil }
2528-func (f *fakeRecordingWebView) Generation() uint64 { return 0 }
2529-func (f *fakeRecordingWebView) IsFullscreen() bool { return false }
2530-func (f *fakeRecordingWebView) IsPlayingAudio() bool { return false }
2531-func (f *fakeRecordingWebView) IsDestroyed() bool { return f.destroyCalls > 0 }
2532-func (f *fakeRecordingWebView) Destroy() { f.destroyCalls++ }
2521+func (f *recordingWebView) GetZoomLevel() float64 { return 1.0 }
2522+func (f *recordingWebView) GetFindController() port.FindController { return nil }
2523+func (f *recordingWebView) SetCallbacks(_ *port.WebViewCallbacks) {}
2524+func (f *recordingWebView) RunJavaScript(_ context.Context, _ string) {}
2525+func (f *recordingWebView) SetBackgroundColor(_, _, _, _ float64) {}
2526+func (f *recordingWebView) ResetBackgroundToDefault() {}
2527+func (f *recordingWebView) Favicon() port.Texture { return nil }
2528+func (f *recordingWebView) Generation() uint64 { return 0 }
2529+func (f *recordingWebView) IsFullscreen() bool { return false }
2530+func (f *recordingWebView) IsPlayingAudio() bool { return false }
2531+func (f *recordingWebView) IsDestroyed() bool { return f.destroyCalls > 0 }
2532+func (f *recordingWebView) Destroy() { f.destroyCalls++ }
25332533
25342534 func TestApp_AttachPopupToTabDestroysPopupWhenWorkspaceViewMissing(t *testing.T) {
25352535 ctx := context.Background()
2536- popupWV := &fakeRecordingWebView{id: 1}
2536+ popupWV := &recordingWebView{id: 1}
25372537 app := &App{workspaceViews: map[entity.TabID]*component.WorkspaceView{}}
25382538
25392539 app.attachPopupToTab(ctx, entity.TabID("missing-tab"), entity.NewPane(entity.PaneID("popup-pane")), popupWV)
@@ -2545,7 +2545,7 @@ func TestApp_AttachPopupToTabDestroysPopupWhenWorkspaceViewMissing(t *testing.T)
25452545
25462546 func TestApp_AttachPopupToTabDestroysPopupWhenPaneNil(t *testing.T) {
25472547 ctx := context.Background()
2548- popupWV := &fakeRecordingWebView{id: 1}
2548+ popupWV := &recordingWebView{id: 1}
25492549 tabID := entity.TabID("tab-1")
25502550 app := &App{workspaceViews: map[entity.TabID]*component.WorkspaceView{tabID: &component.WorkspaceView{}}}
25512551
@@ -2568,7 +2568,7 @@ func TestApp_AttachPopupToTabSkipsRegistrationWhenPaneViewMissing(t *testing.T)
25682568 },
25692569 }
25702570
2571- popupWV := &fakeRecordingWebView{id: 1}
2571+ popupWV := &recordingWebView{id: 1}
25722572 app.attachPopupToTab(ctx, tabID, pane, popupWV)
25732573
25742574 if got := contentCoord.GetWebView(pane.ID); got != nil {
@@ -2608,7 +2608,7 @@ func TestApp_AttachPopupToTabReleasesRegistrationWhenWrapFails(t *testing.T) {
26082608 tabID: wsView,
26092609 },
26102610 }
2611- popupWV := &fakeRecordingWebView{id: 1}
2611+ popupWV := &recordingWebView{id: 1}
26122612
26132613 app.attachPopupToTab(ctx, tabID, pane, popupWV)
26142614
@@ -2637,15 +2637,15 @@ func TestApp_BrowserWindowWebViewActionsIgnoreStaleFocusedWindow(t *testing.T) {
26372637 secondTabs.SetActive(tab2.ID)
26382638 second := &browserWindow{id: "window-2", tabs: secondTabs}
26392639
2640- // Create fake recording webviews.
2641- fakeWv1 := &fakeRecordingWebView{id: 1}
2642- fakeWv2 := &fakeRecordingWebView{id: 2}
2640+ // Create recording webviews.
2641+ recordingWv1 := &recordingWebView{id: 1}
2642+ recordingWv2 := &recordingWebView{id: 2}
26432643
26442644 // Create content coordinator with initialized internal maps to avoid
26452645 // nil-map panics in SetNavigationOrigin during NavigateWebView calls.
26462646 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)
2647- contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), fakeWv1)
2648- contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), fakeWv2)
2647+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), recordingWv1)
2648+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), recordingWv2)
26492649
26502650 // Create NavigationCoordinator (no navigateUC needed for direct webview calls).
26512651 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)
@@ -2684,11 +2684,11 @@ func TestApp_BrowserWindowWebViewActionsIgnoreStaleFocusedWindow(t *testing.T) {
26842684 t.Fatalf("goForwardBrowserWindow returned error: %v", err)
26852685 }
26862686
2687- assertRecordingWebViewIdle(t, "first", fakeWv1)
2688- assertRecordingWebViewActions(t, "second", fakeWv2, "https://google.com")
2687+ assertRecordingWebViewIdle(t, "first", recordingWv1)
2688+ assertRecordingWebViewActions(t, "second", recordingWv2, "https://google.com")
26892689 }
26902690
2691-func assertRecordingWebViewIdle(t *testing.T, name string, wv *fakeRecordingWebView) {
2691+func assertRecordingWebViewIdle(t *testing.T, name string, wv *recordingWebView) {
26922692 t.Helper()
26932693 if wv.loadURICalled || wv.loadURILastURI != "" {
26942694 t.Errorf("%s webview LoadURI was called, expected zero calls", name)
@@ -2710,7 +2710,7 @@ func assertRecordingWebViewIdle(t *testing.T, name string, wv *fakeRecordingWebV
27102710 }
27112711 }
27122712
2713-func assertRecordingWebViewActions(t *testing.T, name string, wv *fakeRecordingWebView, wantURI string) {
2713+func assertRecordingWebViewActions(t *testing.T, name string, wv *recordingWebView, wantURI string) {
27142714 t.Helper()
27152715 if !wv.loadURICalled {
27162716 t.Errorf("%s webview LoadURI was not called", name)
@@ -2735,7 +2735,7 @@ func assertRecordingWebViewActions(t *testing.T, name string, wv *fakeRecordingW
27352735 }
27362736 }
27372737
2738-func assertRecordingWebViewBrowserActionsIdle(t *testing.T, name string, wv *fakeRecordingWebView) {
2738+func assertRecordingWebViewBrowserActionsIdle(t *testing.T, name string, wv *recordingWebView) {
27392739 t.Helper()
27402740 if wv.reloadCalls > 0 {
27412741 t.Errorf("%s webview Reload calls = %d, want 0", name, wv.reloadCalls)
@@ -2763,7 +2763,7 @@ func assertRecordingWebViewBrowserActionsIdle(t *testing.T, name string, wv *fak
27632763 }
27642764 }
27652765
2766-func assertRecordingWebViewBrowserActionsCalledOnce(t *testing.T, name string, wv *fakeRecordingWebView) {
2766+func assertRecordingWebViewBrowserActionsCalledOnce(t *testing.T, name string, wv *recordingWebView) {
27672767 t.Helper()
27682768 if wv.reloadCalls != 1 {
27692769 t.Errorf("%s webview Reload calls = %d, want 1", name, wv.reloadCalls)
@@ -2837,14 +2837,14 @@ func TestApp_DispatchBrowserWindowActionUsesSourceWindow(t *testing.T) {
28372837 secondTabs.SetActive(tab2.ID)
28382838 second := &browserWindow{id: "window-2", tabs: secondTabs}
28392839
2840- // Create fake recording webviews.
2841- fakeWv1 := &fakeRecordingWebView{id: 1, loadURILastURI: "https://first.example"}
2842- fakeWv2 := &fakeRecordingWebView{id: 2, loadURILastURI: "https://second.example"}
2840+ // Create recording webviews.
2841+ recordingWv1 := &recordingWebView{id: 1, loadURILastURI: "https://first.example"}
2842+ recordingWv2 := &recordingWebView{id: 2, loadURILastURI: "https://second.example"}
28432843
2844- // Register fake webviews in content coordinator.
2844+ // Register recording webviews in content coordinator.
28452845 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)
2846- contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), fakeWv1)
2847- contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), fakeWv2)
2846+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), recordingWv1)
2847+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), recordingWv2)
28482848
28492849 // Create navCoord (no navigateUC needed for direct webview calls).
28502850 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)
@@ -2880,11 +2880,11 @@ func TestApp_DispatchBrowserWindowActionUsesSourceWindow(t *testing.T) {
28802880 }
28812881 }
28822882
2883- // Assert first fake webview receives zero calls (stale focus is ignored).
2884- assertRecordingWebViewBrowserActionsIdle(t, "first", fakeWv1)
2883+ // Assert first recording webview receives zero calls (stale focus is ignored).
2884+ assertRecordingWebViewBrowserActionsIdle(t, "first", recordingWv1)
28852885
2886- // Assert second fake webview receives exactly one call per action.
2887- assertRecordingWebViewBrowserActionsCalledOnce(t, "second", fakeWv2)
2886+ // Assert second recording webview receives exactly one call per action.
2887+ assertRecordingWebViewBrowserActionsCalledOnce(t, "second", recordingWv2)
28882888 }
28892889
28902890 func TestApp_DispatchBrowserWindowActionZoomInSupportsFileURLs(t *testing.T) {
@@ -2896,9 +2896,9 @@ func TestApp_DispatchBrowserWindowActionZoomInSupportsFileURLs(t *testing.T) {
28962896 tabs.SetActive(tab.ID)
28972897 bw := &browserWindow{id: "window-1", tabs: tabs}
28982898
2899- fakeWv := &fakeRecordingWebView{id: 1, loadURILastURI: "file:///tmp/demo.html"}
2899+ recordingWv := &recordingWebView{id: 1, loadURILastURI: "file:///tmp/demo.html"}
29002900 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)
2901- contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), fakeWv)
2901+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), recordingWv)
29022902
29032903 app := &App{
29042904 browserWindows: map[string]*browserWindow{bw.id: bw},
@@ -2916,11 +2916,11 @@ func TestApp_DispatchBrowserWindowActionZoomInSupportsFileURLs(t *testing.T) {
29162916 if err := app.dispatchBrowserWindowAction(ctx, bw, input.ActionZoomIn); err != nil {
29172917 t.Fatalf("dispatchBrowserWindowAction returned error: %v", err)
29182918 }
2919- if fakeWv.setZoomLevelCalls != 1 {
2920- t.Fatalf("webview SetZoomLevel calls = %d, want 1", fakeWv.setZoomLevelCalls)
2919+ if recordingWv.setZoomLevelCalls != 1 {
2920+ t.Fatalf("webview SetZoomLevel calls = %d, want 1", recordingWv.setZoomLevelCalls)
29212921 }
2922- if diff := math.Abs(fakeWv.setZoomLevelLastLevel - 1.1); diff > 0.0001 {
2923- t.Fatalf("webview SetZoomLevel level = %f, want 1.1", fakeWv.setZoomLevelLastLevel)
2922+ if diff := math.Abs(recordingWv.setZoomLevelLastLevel - 1.1); diff > 0.0001 {
2923+ t.Fatalf("webview SetZoomLevel level = %f, want 1.1", recordingWv.setZoomLevelLastLevel)
29242924 }
29252925 }
29262926
@@ -3161,11 +3161,11 @@ func TestApp_WorkspaceOmniboxNavigateUsesOwnerWindow(t *testing.T) {
31613161 first := &browserWindow{id: "window-1", tabs: firstTabs}
31623162 second := &browserWindow{id: "window-2", tabs: secondTabs}
31633163
3164- fakeWv1 := &fakeRecordingWebView{id: 1}
3165- fakeWv2 := &fakeRecordingWebView{id: 2}
3164+ recordingWv1 := &recordingWebView{id: 1}
3165+ recordingWv2 := &recordingWebView{id: 2}
31663166 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)
3167- contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), fakeWv1)
3168- contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), fakeWv2)
3167+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), recordingWv1)
3168+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), recordingWv2)
31693169
31703170 app := &App{
31713171 deps: &Dependencies{Config: &config.Config{}},
@@ -3189,11 +3189,11 @@ func TestApp_WorkspaceOmniboxNavigateUsesOwnerWindow(t *testing.T) {
31893189 t.Fatalf("omnibox navigate callback returned error: %v", err)
31903190 }
31913191
3192- if fakeWv1.loadURICalled {
3193- t.Errorf("first window webview navigated to %q, want no navigation", fakeWv1.loadURILastURI)
3192+ if recordingWv1.loadURICalled {
3193+ t.Errorf("first window webview navigated to %q, want no navigation", recordingWv1.loadURILastURI)
31943194 }
3195- if !fakeWv2.loadURICalled || fakeWv2.loadURILastURI != "https://example.com" {
3196- t.Errorf("second window webview navigation = called %v uri %q, want https://example.com", fakeWv2.loadURICalled, fakeWv2.loadURILastURI)
3195+ if !recordingWv2.loadURICalled || recordingWv2.loadURILastURI != "https://example.com" {
3196+ t.Errorf("second window webview navigation = called %v uri %q, want https://example.com", recordingWv2.loadURICalled, recordingWv2.loadURILastURI)
31973197 }
31983198 }
31993199
M internal/ui/browser_window_history_sidebar_test.go +38 -38
@@ -41,13 +41,13 @@ func TestHistorySidebarConfig_OnNavigateNavigatesActivePaneAndKeepsSidebar(t *te
4141 secondTabs.SetActive(tab2.ID)
4242 second := &browserWindow{id: "window-2", tabs: secondTabs}
4343
44- // Create fake webviews, one per pane.
45- fakeWv1 := &fakeRecordingWebView{id: 1}
46- fakeWv2 := &fakeRecordingWebView{id: 2}
44+ // Create recording webviews, one per pane.
45+ recordingWv1 := &recordingWebView{id: 1}
46+ recordingWv2 := &recordingWebView{id: 2}
4747
4848 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)
49- contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), fakeWv1)
50- contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), fakeWv2)
49+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), recordingWv1)
50+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), recordingWv2)
5151
5252 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)
5353
@@ -75,12 +75,12 @@ func TestHistorySidebarConfig_OnNavigateNavigatesActivePaneAndKeepsSidebar(t *te
7575 require.NoError(t, err, "OnNavigate should succeed")
7676
7777 // The second window's webview must have received the navigation.
78- assert.True(t, fakeWv2.loadURICalled, "second window webview should receive navigation")
79- assert.Equal(t, navigateURL, fakeWv2.loadURILastURI)
78+ assert.True(t, recordingWv2.loadURICalled, "second window webview should receive navigation")
79+ assert.Equal(t, navigateURL, recordingWv2.loadURILastURI)
8080 assert.True(t, second.sidebarVisible, "default history activation should keep the sidebar visible")
8181
8282 // The first window's webview must NOT have been touched (stale-focus guard).
83- assert.False(t, fakeWv1.loadURICalled, "first window webview must not receive navigation from second window callback")
83+ assert.False(t, recordingWv1.loadURICalled, "first window webview must not receive navigation from second window callback")
8484 }
8585
8686 // TestHistorySidebarConfig_OnNavigateKeepOpenNavigatesWithoutClosing verifies
@@ -103,8 +103,8 @@ func TestHistorySidebarConfig_OnNavigateKeepOpenNavigatesWithoutClosing(t *testi
103103 }
104104
105105 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)
106- fakeWv := &fakeRecordingWebView{id: 1}
107- contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), fakeWv)
106+ recordingWv := &recordingWebView{id: 1}
107+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), recordingWv)
108108 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)
109109
110110 app := &App{
@@ -122,8 +122,8 @@ func TestHistorySidebarConfig_OnNavigateKeepOpenNavigatesWithoutClosing(t *testi
122122 navigateURL := "https://keep-open.com"
123123 err := cfg.OnNavigateKeepOpen(ctx, navigateURL)
124124 require.NoError(t, err)
125- assert.True(t, fakeWv.loadURICalled)
126- assert.Equal(t, navigateURL, fakeWv.loadURILastURI, "Ctrl+Enter navigation should go to the URL")
125+ assert.True(t, recordingWv.loadURICalled)
126+ assert.Equal(t, navigateURL, recordingWv.loadURILastURI, "Ctrl+Enter navigation should go to the URL")
127127 assert.True(t, bw.sidebarVisible, "Ctrl+Enter navigation should keep the sidebar visible")
128128 }
129129
@@ -148,12 +148,12 @@ func TestHistorySidebar_OwnershipOnMultiWindowNavigation(t *testing.T) {
148148 secondTabs.SetActive(tab2.ID)
149149 second := &browserWindow{id: "window-2", tabs: secondTabs}
150150
151- fakeWv1 := &fakeRecordingWebView{id: 1}
152- fakeWv2 := &fakeRecordingWebView{id: 2}
151+ recordingWv1 := &recordingWebView{id: 1}
152+ recordingWv2 := &recordingWebView{id: 2}
153153
154154 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)
155- contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), fakeWv1)
156- contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), fakeWv2)
155+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), recordingWv1)
156+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), recordingWv2)
157157
158158 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)
159159
@@ -177,11 +177,11 @@ func TestHistorySidebar_OwnershipOnMultiWindowNavigation(t *testing.T) {
177177 require.NoError(t, err)
178178
179179 // Second window's webview must receive the navigation.
180- assert.True(t, fakeWv2.loadURICalled, "second window webview should receive navigation")
181- assert.Equal(t, "https://second-window.com", fakeWv2.loadURILastURI)
180+ assert.True(t, recordingWv2.loadURICalled, "second window webview should receive navigation")
181+ assert.Equal(t, "https://second-window.com", recordingWv2.loadURILastURI)
182182
183183 // First window's webview must NOT have been touched.
184- assert.False(t, fakeWv1.loadURICalled, "first window webview should NOT receive navigation when second was targeted")
184+ assert.False(t, recordingWv1.loadURICalled, "first window webview should NOT receive navigation when second was targeted")
185185 }
186186
187187 // =============================================================================
@@ -509,9 +509,9 @@ func TestApp_HistorySidebarConfig_NavigateCallbackNavigates(t *testing.T) {
509509 sidebarVisible: true,
510510 }
511511
512- fakeWv := &fakeRecordingWebView{id: 1}
512+ recordingWv := &recordingWebView{id: 1}
513513 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)
514- contentCoord.RegisterPopupWebView(paneID, fakeWv)
514+ contentCoord.RegisterPopupWebView(paneID, recordingWv)
515515 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)
516516
517517 app := &App{
@@ -535,8 +535,8 @@ func TestApp_HistorySidebarConfig_NavigateCallbackNavigates(t *testing.T) {
535535 require.NoError(t, err)
536536
537537 // Verify the navigation reached the correct webview.
538- assert.True(t, fakeWv.loadURICalled, "webview must receive navigation")
539- assert.Equal(t, navigateURL, fakeWv.loadURILastURI)
538+ assert.True(t, recordingWv.loadURICalled, "webview must receive navigation")
539+ assert.Equal(t, navigateURL, recordingWv.loadURILastURI)
540540 assert.True(t, bw.sidebarVisible, "default history navigation should keep the sidebar open")
541541 }
542542
@@ -557,9 +557,9 @@ func TestApp_NavigateHistorySidebarSelection_KeepsSidebarVisible(t *testing.T) {
557557 sidebarVisible: true,
558558 }
559559
560- fakeWv := &fakeRecordingWebView{id: 1}
560+ recordingWv := &recordingWebView{id: 1}
561561 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)
562- contentCoord.RegisterPopupWebView(paneID, fakeWv)
562+ contentCoord.RegisterPopupWebView(paneID, recordingWv)
563563 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)
564564
565565 app := &App{
@@ -575,8 +575,8 @@ func TestApp_NavigateHistorySidebarSelection_KeepsSidebarVisible(t *testing.T) {
575575
576576 err := app.navigateHistorySidebarSelection(ctx, bw, "https://open.com")
577577 require.NoError(t, err)
578- assert.True(t, fakeWv.loadURICalled)
579- assert.Equal(t, "https://open.com", fakeWv.loadURILastURI)
578+ assert.True(t, recordingWv.loadURICalled)
579+ assert.Equal(t, "https://open.com", recordingWv.loadURILastURI)
580580 assert.True(t, bw.sidebarVisible, "history selection navigation should not hide the sidebar")
581581 }
582582
@@ -599,12 +599,12 @@ func TestApp_HistorySidebarConfig_NavigateCallbackOwnership(t *testing.T) {
599599 secondTabs.SetActive(tab2.ID)
600600 second := &browserWindow{id: "window-2", tabs: secondTabs}
601601
602- fakeWv1 := &fakeRecordingWebView{id: 1}
603- fakeWv2 := &fakeRecordingWebView{id: 2}
602+ recordingWv1 := &recordingWebView{id: 1}
603+ recordingWv2 := &recordingWebView{id: 2}
604604
605605 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)
606- contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), fakeWv1)
607- contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), fakeWv2)
606+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), recordingWv1)
607+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), recordingWv2)
608608 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)
609609
610610 app := &App{
@@ -629,11 +629,11 @@ func TestApp_HistorySidebarConfig_NavigateCallbackOwnership(t *testing.T) {
629629 require.NoError(t, err)
630630
631631 // Second window's webview must receive navigation.
632- assert.True(t, fakeWv2.loadURICalled, "second window webview must receive navigation")
633- assert.Equal(t, "https://ownership.com", fakeWv2.loadURILastURI)
632+ assert.True(t, recordingWv2.loadURICalled, "second window webview must receive navigation")
633+ assert.Equal(t, "https://ownership.com", recordingWv2.loadURILastURI)
634634
635635 // First window's webview must NOT be touched.
636- assert.False(t, fakeWv1.loadURICalled, "first window webview must not receive navigation")
636+ assert.False(t, recordingWv1.loadURICalled, "first window webview must not receive navigation")
637637 }
638638
639639 // TestApp_HistorySidebarConfig_KeepOpenCallback verifies that
@@ -655,9 +655,9 @@ func TestApp_HistorySidebarConfig_KeepOpenCallback(t *testing.T) {
655655 sidebarVisible: true,
656656 }
657657
658- fakeWv := &fakeRecordingWebView{id: 1}
658+ recordingWv := &recordingWebView{id: 1}
659659 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)
660- contentCoord.RegisterPopupWebView(paneID, fakeWv)
660+ contentCoord.RegisterPopupWebView(paneID, recordingWv)
661661 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)
662662
663663 app := &App{
@@ -678,8 +678,8 @@ func TestApp_HistorySidebarConfig_KeepOpenCallback(t *testing.T) {
678678 err := cfg.OnNavigateKeepOpen(ctx, navigateURL)
679679 require.NoError(t, err)
680680
681- assert.True(t, fakeWv.loadURICalled, "webview must receive navigation")
682- assert.Equal(t, navigateURL, fakeWv.loadURILastURI)
681+ assert.True(t, recordingWv.loadURICalled, "webview must receive navigation")
682+ assert.Equal(t, navigateURL, recordingWv.loadURILastURI)
683683
684684 // Sidebar must remain visible (keep-open contract).
685685 assert.True(t, bw.sidebarVisible, "sidebar must stay visible after keep-open navigation")
M internal/ui/browser_window_test.go +7 -7
@@ -130,8 +130,8 @@ func TestTabCoordinator_CloseReleasesClosedTabWorkspaceWebViews(t *testing.T) {
130130
131131 pool := &recordingWebViewPool{}
132132 contentCoord := contentcoord.NewCoordinator(ctx, pool, nil, nil, nil, nil, nil, nil)
133- closedWV := &fakeRecordingWebView{id: 101}
134- survivingWV := &fakeRecordingWebView{id: 102}
133+ closedWV := &recordingWebView{id: 101}
134+ survivingWV := &recordingWebView{id: 102}
135135 contentCoord.RegisterPopupWebView(closedTab.Workspace.ActivePaneID, closedWV)
136136 contentCoord.RegisterPopupWebView(survivingTab.Workspace.ActivePaneID, survivingWV)
137137
@@ -173,8 +173,8 @@ func TestTabCoordinator_SwitchDoesNotReleaseWorkspaceWebViews(t *testing.T) {
173173
174174 pool := &recordingWebViewPool{}
175175 contentCoord := contentcoord.NewCoordinator(ctx, pool, nil, nil, nil, nil, nil, nil)
176- firstWV := &fakeRecordingWebView{id: 111}
177- secondWV := &fakeRecordingWebView{id: 112}
176+ firstWV := &recordingWebView{id: 111}
177+ secondWV := &recordingWebView{id: 112}
178178 contentCoord.RegisterPopupWebView(firstTab.Workspace.ActivePaneID, firstWV)
179179 contentCoord.RegisterPopupWebView(secondTab.Workspace.ActivePaneID, secondWV)
180180
@@ -209,7 +209,7 @@ func TestTabCoordinator_CloseLastTabReleasesWorkspaceBeforeWindowRemoval(t *test
209209
210210 pool := &recordingWebViewPool{}
211211 contentCoord := contentcoord.NewCoordinator(ctx, pool, nil, nil, nil, nil, nil, nil)
212- closedWV := &fakeRecordingWebView{id: 121}
212+ closedWV := &recordingWebView{id: 121}
213213 contentCoord.RegisterPopupWebView(closedTab.Workspace.ActivePaneID, closedWV)
214214
215215 bw := &browserWindow{id: "window-1", tabs: tabs}
@@ -247,8 +247,8 @@ func TestBrowserWindow_RemoveBrowserWindowReleasesOwnedTabWorkspaceWebViews(t *t
247247
248248 pool := &recordingWebViewPool{}
249249 contentCoord := contentcoord.NewCoordinator(ctx, pool, nil, nil, nil, nil, nil, nil)
250- ownedWV := &fakeRecordingWebView{id: 201}
251- otherWV := &fakeRecordingWebView{id: 202}
250+ ownedWV := &recordingWebView{id: 201}
251+ otherWV := &recordingWebView{id: 202}
252252 contentCoord.RegisterPopupWebView(ownedTab.Workspace.ActivePaneID, ownedWV)
253253 contentCoord.RegisterPopupWebView(otherTab.Workspace.ActivePaneID, otherWV)
254254
A internal/ui/coordinator/mocks/navigation.go +118 -0
@@ -0,0 +1,118 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ mock "github.com/stretchr/testify/mock"
11+)
12+
13+// NewMockOmniboxProvider creates a new instance of MockOmniboxProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
14+// The first argument is typically a *testing.T value.
15+func NewMockOmniboxProvider(t interface {
16+ mock.TestingT
17+ Cleanup(func())
18+}) *MockOmniboxProvider {
19+ mock := &MockOmniboxProvider{}
20+ mock.Mock.Test(t)
21+
22+ t.Cleanup(func() { mock.AssertExpectations(t) })
23+
24+ return mock
25+}
26+
27+// MockOmniboxProvider is an autogenerated mock type for the OmniboxProvider type
28+type MockOmniboxProvider struct {
29+ mock.Mock
30+}
31+
32+type MockOmniboxProvider_Expecter struct {
33+ mock *mock.Mock
34+}
35+
36+func (_m *MockOmniboxProvider) EXPECT() *MockOmniboxProvider_Expecter {
37+ return &MockOmniboxProvider_Expecter{mock: &_m.Mock}
38+}
39+
40+// ToggleOmnibox provides a mock function for the type MockOmniboxProvider
41+func (_mock *MockOmniboxProvider) ToggleOmnibox(ctx context.Context) {
42+ _mock.Called(ctx)
43+ return
44+}
45+
46+// MockOmniboxProvider_ToggleOmnibox_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ToggleOmnibox'
47+type MockOmniboxProvider_ToggleOmnibox_Call struct {
48+ *mock.Call
49+}
50+
51+// ToggleOmnibox is a helper method to define mock.On call
52+// - ctx context.Context
53+func (_e *MockOmniboxProvider_Expecter) ToggleOmnibox(ctx any) *MockOmniboxProvider_ToggleOmnibox_Call {
54+ return &MockOmniboxProvider_ToggleOmnibox_Call{Call: _e.mock.On("ToggleOmnibox", ctx)}
55+}
56+
57+func (_c *MockOmniboxProvider_ToggleOmnibox_Call) Run(run func(ctx context.Context)) *MockOmniboxProvider_ToggleOmnibox_Call {
58+ _c.Call.Run(func(args mock.Arguments) {
59+ var arg0 context.Context
60+ if args[0] != nil {
61+ arg0 = args[0].(context.Context)
62+ }
63+ run(
64+ arg0,
65+ )
66+ })
67+ return _c
68+}
69+
70+func (_c *MockOmniboxProvider_ToggleOmnibox_Call) Return() *MockOmniboxProvider_ToggleOmnibox_Call {
71+ _c.Call.Return()
72+ return _c
73+}
74+
75+func (_c *MockOmniboxProvider_ToggleOmnibox_Call) RunAndReturn(run func(ctx context.Context)) *MockOmniboxProvider_ToggleOmnibox_Call {
76+ _c.Run(run)
77+ return _c
78+}
79+
80+// UpdateOmniboxZoom provides a mock function for the type MockOmniboxProvider
81+func (_mock *MockOmniboxProvider) UpdateOmniboxZoom(factor float64) {
82+ _mock.Called(factor)
83+ return
84+}
85+
86+// MockOmniboxProvider_UpdateOmniboxZoom_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateOmniboxZoom'
87+type MockOmniboxProvider_UpdateOmniboxZoom_Call struct {
88+ *mock.Call
89+}
90+
91+// UpdateOmniboxZoom is a helper method to define mock.On call
92+// - factor float64
93+func (_e *MockOmniboxProvider_Expecter) UpdateOmniboxZoom(factor any) *MockOmniboxProvider_UpdateOmniboxZoom_Call {
94+ return &MockOmniboxProvider_UpdateOmniboxZoom_Call{Call: _e.mock.On("UpdateOmniboxZoom", factor)}
95+}
96+
97+func (_c *MockOmniboxProvider_UpdateOmniboxZoom_Call) Run(run func(factor float64)) *MockOmniboxProvider_UpdateOmniboxZoom_Call {
98+ _c.Call.Run(func(args mock.Arguments) {
99+ var arg0 float64
100+ if args[0] != nil {
101+ arg0 = args[0].(float64)
102+ }
103+ run(
104+ arg0,
105+ )
106+ })
107+ return _c
108+}
109+
110+func (_c *MockOmniboxProvider_UpdateOmniboxZoom_Call) Return() *MockOmniboxProvider_UpdateOmniboxZoom_Call {
111+ _c.Call.Return()
112+ return _c
113+}
114+
115+func (_c *MockOmniboxProvider_UpdateOmniboxZoom_Call) RunAndReturn(run func(factor float64)) *MockOmniboxProvider_UpdateOmniboxZoom_Call {
116+ _c.Run(run)
117+ return _c
118+}
M internal/ui/coordinator/navigation_test.go +8 -17
@@ -4,28 +4,19 @@ import (
44 "context"
55 "testing"
66
7- "github.com/stretchr/testify/require"
7+ coordinatormocks "github.com/bnema/dumber/internal/ui/coordinator/mocks"
8+ "github.com/stretchr/testify/mock"
89 )
910
10-type fakeOmniboxProvider struct {
11- toggled bool
12- zoomUpdates []float64
13-}
14-
15-func (f *fakeOmniboxProvider) ToggleOmnibox(context.Context) { f.toggled = true }
16-
17-func (f *fakeOmniboxProvider) UpdateOmniboxZoom(factor float64) {
18- f.zoomUpdates = append(f.zoomUpdates, factor)
19-}
20-
2111 func TestNavigationCoordinator_OmniboxProviderOpenAndZoom(t *testing.T) {
2212 c := &NavigationCoordinator{}
23- provider := &fakeOmniboxProvider{}
13+ provider := coordinatormocks.NewMockOmniboxProvider(t)
14+ provider.EXPECT().ToggleOmnibox(mock.Anything).Once()
15+ provider.EXPECT().UpdateOmniboxZoom(1.25).Once()
2416
2517 c.SetOmniboxProvider(provider)
26- require.NoError(t, c.OpenOmnibox(context.Background()))
18+ if err := c.OpenOmnibox(context.Background()); err != nil {
19+ t.Fatal(err)
20+ }
2721 c.NotifyZoomChanged(context.Background(), 1.25)
28-
29- require.True(t, provider.toggled)
30- require.Equal(t, []float64{1.25}, provider.zoomUpdates)
3122 }
A internal/ui/dialog/mocks/permission.go +96 -0
@@ -0,0 +1,96 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ mock "github.com/stretchr/testify/mock"
11+)
12+
13+// NewMockPermissionPopup creates a new instance of MockPermissionPopup. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
14+// The first argument is typically a *testing.T value.
15+func NewMockPermissionPopup(t interface {
16+ mock.TestingT
17+ Cleanup(func())
18+}) *MockPermissionPopup {
19+ mock := &MockPermissionPopup{}
20+ mock.Mock.Test(t)
21+
22+ t.Cleanup(func() { mock.AssertExpectations(t) })
23+
24+ return mock
25+}
26+
27+// MockPermissionPopup is an autogenerated mock type for the permissionPopup type
28+type MockPermissionPopup struct {
29+ mock.Mock
30+}
31+
32+type MockPermissionPopup_Expecter struct {
33+ mock *mock.Mock
34+}
35+
36+func (_m *MockPermissionPopup) EXPECT() *MockPermissionPopup_Expecter {
37+ return &MockPermissionPopup_Expecter{mock: &_m.Mock}
38+}
39+
40+// Show provides a mock function for the type MockPermissionPopup
41+func (_mock *MockPermissionPopup) Show(ctx context.Context, heading string, body string, callback func(allowed bool, persistent bool)) {
42+ _mock.Called(ctx, heading, body, callback)
43+ return
44+}
45+
46+// MockPermissionPopup_Show_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Show'
47+type MockPermissionPopup_Show_Call struct {
48+ *mock.Call
49+}
50+
51+// Show is a helper method to define mock.On call
52+// - ctx context.Context
53+// - heading string
54+// - body string
55+// - callback func(allowed bool, persistent bool)
56+func (_e *MockPermissionPopup_Expecter) Show(ctx any, heading any, body any, callback any) *MockPermissionPopup_Show_Call {
57+ return &MockPermissionPopup_Show_Call{Call: _e.mock.On("Show", ctx, heading, body, callback)}
58+}
59+
60+func (_c *MockPermissionPopup_Show_Call) Run(run func(ctx context.Context, heading string, body string, callback func(allowed bool, persistent bool))) *MockPermissionPopup_Show_Call {
61+ _c.Call.Run(func(args mock.Arguments) {
62+ var arg0 context.Context
63+ if args[0] != nil {
64+ arg0 = args[0].(context.Context)
65+ }
66+ var arg1 string
67+ if args[1] != nil {
68+ arg1 = args[1].(string)
69+ }
70+ var arg2 string
71+ if args[2] != nil {
72+ arg2 = args[2].(string)
73+ }
74+ var arg3 func(allowed bool, persistent bool)
75+ if args[3] != nil {
76+ arg3 = args[3].(func(allowed bool, persistent bool))
77+ }
78+ run(
79+ arg0,
80+ arg1,
81+ arg2,
82+ arg3,
83+ )
84+ })
85+ return _c
86+}
87+
88+func (_c *MockPermissionPopup_Show_Call) Return() *MockPermissionPopup_Show_Call {
89+ _c.Call.Return()
90+ return _c
91+}
92+
93+func (_c *MockPermissionPopup_Show_Call) RunAndReturn(run func(ctx context.Context, heading string, body string, callback func(allowed bool, persistent bool))) *MockPermissionPopup_Show_Call {
94+ _c.Run(run)
95+ return _c
96+}
M internal/ui/dialog/permission_test.go +31 -34
@@ -6,43 +6,40 @@ import (
66
77 "github.com/bnema/dumber/internal/application/port"
88 "github.com/bnema/dumber/internal/domain/entity"
9+ dialogmocks "github.com/bnema/dumber/internal/ui/dialog/mocks"
910 "github.com/stretchr/testify/assert"
11+ "github.com/stretchr/testify/mock"
1012 )
1113
12-type fakePermissionPopup struct {
13- showCalls []fakePopupShowCall
14- callback func(allowed, persistent bool)
15-}
16-
17-type fakePopupShowCall struct {
14+type permissionPopupShowCall struct {
1815 heading string
1916 body string
2017 }
2118
22-func (f *fakePermissionPopup) Show(
23- _ context.Context,
24- heading string,
25- body string,
26- callback func(allowed, persistent bool),
27-) {
28- f.showCalls = append(f.showCalls, fakePopupShowCall{
29- heading: heading,
30- body: body,
31- })
32- f.callback = callback
33-}
34-
35-func (f *fakePermissionPopup) Respond(allowed, persistent bool) {
36- if f.callback == nil {
37- return
19+func TestPermissionDialog_QueuesRequestsWhilePopupVisible(t *testing.T) {
20+ popup := dialogmocks.NewMockPermissionPopup(t)
21+ var showCalls []permissionPopupShowCall
22+ var callback func(allowed, persistent bool)
23+ popup.EXPECT().
24+ Show(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
25+ Run(func(_ context.Context, heading string, body string, cb func(allowed, persistent bool)) {
26+ showCalls = append(showCalls, permissionPopupShowCall{
27+ heading: heading,
28+ body: body,
29+ })
30+ callback = cb
31+ }).
32+ Twice()
33+
34+ respond := func(allowed, persistent bool) {
35+ if callback == nil {
36+ return
37+ }
38+ cb := callback
39+ callback = nil
40+ cb(allowed, persistent)
3841 }
39- cb := f.callback
40- f.callback = nil
41- cb(allowed, persistent)
42-}
4342
44-func TestPermissionDialog_QueuesRequestsWhilePopupVisible(t *testing.T) {
45- popup := &fakePermissionPopup{}
4643 d := &PermissionDialog{popup: popup}
4744
4845 var firstResult port.PermissionDialogResult
@@ -71,22 +68,22 @@ func TestPermissionDialog_QueuesRequestsWhilePopupVisible(t *testing.T) {
7168 },
7269 )
7370
74- if assert.Len(t, popup.showCalls, 1) {
75- assert.Equal(t, "Allow Microphone Access?", popup.showCalls[0].heading)
71+ if assert.Len(t, showCalls, 1) {
72+ assert.Equal(t, "Allow Microphone Access?", showCalls[0].heading)
7673 }
7774 assert.False(t, firstCalled)
7875 assert.False(t, secondCalled)
7976
80- popup.Respond(true, false)
77+ respond(true, false)
8178 assert.True(t, firstCalled)
8279 assert.Equal(t, port.PermissionDialogResult{Allowed: true, Persistent: false}, firstResult)
8380
84- if assert.Len(t, popup.showCalls, 2) {
85- assert.Equal(t, "Allow Camera Access?", popup.showCalls[1].heading)
81+ if assert.Len(t, showCalls, 2) {
82+ assert.Equal(t, "Allow Camera Access?", showCalls[1].heading)
8683 }
8784 assert.False(t, secondCalled)
8885
89- popup.Respond(false, true)
86+ respond(false, true)
9087 assert.True(t, secondCalled)
9188 assert.Equal(t, port.PermissionDialogResult{Allowed: false, Persistent: true}, secondResult)
9289 }
M internal/ui/systemviews/app_test.go +98 -98
@@ -48,7 +48,7 @@ func TestParseRoute(t *testing.T) {
4848 func TestAppRunMountsPlaceholderAndRecordsRoute(t *testing.T) {
4949 t.Parallel()
5050
51- dom := &fakeDOM{}
51+ dom := &recordingDOM{}
5252 app := NewApp(Dependencies{
5353 DOM: dom,
5454 LocationURI: "dumb://history",
@@ -64,7 +64,7 @@ func TestAppRunMountsPlaceholderAndRecordsRoute(t *testing.T) {
6464 func TestAppLoadInitialHistoryRouteUsesStyledSections(t *testing.T) {
6565 t.Parallel()
6666
67- history := &fakeHistoryService{entries: []*entity.HistoryEntry{{
67+ history := &recordingHistoryService{entries: []*entity.HistoryEntry{{
6868 URL: "https://example.com",
6969 Title: "Example",
7070 }}}
@@ -98,7 +98,7 @@ func TestAppLoadInitialHistoryRouteUsesStyledSections(t *testing.T) {
9898 func TestAppLoadInitialHistoryRouteShowsTotalStatsWithWindowedTimeline(t *testing.T) {
9999 t.Parallel()
100100
101- history := &fakeHistoryService{
101+ history := &recordingHistoryService{
102102 entries: []*entity.HistoryEntry{
103103 {ID: 1, URL: "https://older.example", Title: "Older", VisitCount: 2},
104104 {ID: 2, URL: "https://newer.example", Title: "Newer", VisitCount: 3},
@@ -119,8 +119,8 @@ func TestAppLoadInitialHistoryRouteShowsTotalStatsWithWindowedTimeline(t *testin
119119 func TestAppRunMountsHistoryLoadingBeforeAsyncHydration(t *testing.T) {
120120 t.Parallel()
121121
122- dom := &fakeActionDOM{fakeDOM: fakeDOM{mounts: make(chan string, 4)}}
123- history := &fakeHistoryService{
122+ dom := &recordingActionDOM{recordingDOM: recordingDOM{mounts: make(chan string, 4)}}
123+ history := &recordingHistoryService{
124124 entries: []*entity.HistoryEntry{{ID: 1, URL: "https://example.com", Title: "Loaded entry"}},
125125 timelineStarted: make(chan struct{}),
126126 releaseTimeline: make(chan struct{}),
@@ -147,7 +147,7 @@ func TestAppRunMountsHistoryLoadingBeforeAsyncHydration(t *testing.T) {
147147 func TestSurfaceActionErrorPreventsStaleAsyncHydrationMount(t *testing.T) {
148148 t.Parallel()
149149
150- dom := &fakeDOM{mounts: make(chan string, 4)}
150+ dom := &recordingDOM{mounts: make(chan string, 4)}
151151 app := NewApp(Dependencies{DOM: dom, LocationURI: "dumb://history"})
152152 app.currentRoute = RouteHistory
153153 staleHTML := "<div>Stale hydrated entry</div>"
@@ -168,8 +168,8 @@ func TestSurfaceActionErrorPreventsStaleAsyncHydrationMount(t *testing.T) {
168168 func TestSurfaceActionErrorWhileAsyncHydrationBlockedPreventsStaleMount(t *testing.T) {
169169 t.Parallel()
170170
171- dom := &fakeActionDOM{fakeDOM: fakeDOM{mounts: make(chan string, 4)}}
172- history := &fakeHistoryService{
171+ dom := &recordingActionDOM{recordingDOM: recordingDOM{mounts: make(chan string, 4)}}
172+ history := &recordingHistoryService{
173173 entries: []*entity.HistoryEntry{{ID: 1, URL: "https://example.com", Title: "Stale hydrated entry"}},
174174 timelineStarted: make(chan struct{}),
175175 releaseTimeline: make(chan struct{}),
@@ -200,7 +200,7 @@ func TestSurfaceActionErrorWhileAsyncHydrationBlockedPreventsStaleMount(t *testi
200200
201201 func TestCurrentHistoryRouteSnapshotIncludesWindowAfter(t *testing.T) {
202202 cursor := time.Date(2026, 4, 25, 9, 0, 0, 0, time.UTC)
203- app := NewApp(Dependencies{History: &fakeHistoryService{}, LocationURI: "dumb://history"})
203+ app := NewApp(Dependencies{History: &recordingHistoryService{}, LocationURI: "dumb://history"})
204204 app.currentRoute = RouteHistory
205205 app.historyWindowAfter = cursor
206206
@@ -212,13 +212,13 @@ func TestAppHandleHistoryLoadMoreAppendsOlderWindow(t *testing.T) {
212212 t.Parallel()
213213
214214 cursor := time.Date(2026, 4, 25, 9, 0, 0, 0, time.UTC)
215- history := &fakeHistoryService{window: &entity.HistoryWindow{
215+ history := &recordingHistoryService{window: &entity.HistoryWindow{
216216 Entries: []*entity.HistoryEntry{{ID: 2, URL: "https://older.example", Title: "Older entry", LastVisited: cursor.Add(-time.Hour)}},
217217 Before: cursor,
218218 After: cursor.Add(-24 * time.Hour),
219219 HasMore: true,
220220 }}
221- dom := &fakeActionDOM{}
221+ dom := &recordingActionDOM{}
222222 app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"})
223223 app.currentRoute = RouteHistory
224224 app.historyEntries = []*entity.HistoryEntry{{ID: 1, URL: "https://newer.example", Title: "Newer entry", LastVisited: cursor}}
@@ -240,13 +240,13 @@ func TestAppHandleHistoryLoadMoreRemountsWhenAppendUnavailable(t *testing.T) {
240240 t.Parallel()
241241
242242 cursor := time.Date(2026, 4, 25, 9, 0, 0, 0, time.UTC)
243- history := &fakeHistoryService{window: &entity.HistoryWindow{
243+ history := &recordingHistoryService{window: &entity.HistoryWindow{
244244 Entries: []*entity.HistoryEntry{{ID: 2, URL: "https://older.example", Title: "Older entry", LastVisited: cursor.Add(-time.Hour)}},
245245 Before: cursor,
246246 After: cursor.Add(-24 * time.Hour),
247247 HasMore: true,
248248 }}
249- dom := &fakeDOM{}
249+ dom := &recordingDOM{}
250250 app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"})
251251 app.currentRoute = RouteHistory
252252 app.historyEntries = []*entity.HistoryEntry{{ID: 1, URL: "https://newer.example", Title: "Newer entry", LastVisited: cursor}}
@@ -267,8 +267,8 @@ func TestAppHandleHistoryLoadMoreIgnoresStaleCursor(t *testing.T) {
267267 t.Parallel()
268268
269269 cursor := time.Date(2026, 4, 25, 9, 0, 0, 0, time.UTC)
270- history := &fakeHistoryService{window: &entity.HistoryWindow{Entries: []*entity.HistoryEntry{{ID: 2, URL: "https://older.example"}}}}
271- dom := &fakeActionDOM{}
270+ history := &recordingHistoryService{window: &entity.HistoryWindow{Entries: []*entity.HistoryEntry{{ID: 2, URL: "https://older.example"}}}}
271+ dom := &recordingActionDOM{}
272272 app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"})
273273 app.currentRoute = RouteHistory
274274 app.historyWindowAfter = cursor
@@ -285,7 +285,7 @@ func TestAppHandleHistoryLoadMoreIgnoresStaleCursor(t *testing.T) {
285285 func TestAppLoadInitialHistoryRouteRendersManagementActions(t *testing.T) {
286286 t.Parallel()
287287
288- history := &fakeHistoryService{
288+ history := &recordingHistoryService{
289289 entries: []*entity.HistoryEntry{{
290290 ID: 42,
291291 URL: "https://example.com/page",
@@ -314,8 +314,8 @@ func TestAppLoadInitialHistoryRouteRendersManagementActions(t *testing.T) {
314314 }
315315
316316 func TestAppHandleHistoryActionsRefreshesDOM(t *testing.T) {
317- dom := &fakeDOM{}
318- history := &fakeHistoryService{
317+ dom := &recordingDOM{}
318+ history := &recordingHistoryService{
319319 entries: []*entity.HistoryEntry{{ID: 42, URL: "https://example.com", Title: "Example"}},
320320 searchEntries: []*entity.HistoryEntry{{ID: 7, URL: "https://search.example", Title: "Search result"}},
321321 }
@@ -364,8 +364,8 @@ func TestAppHandleHistoryActionsRefreshesDOM(t *testing.T) {
364364 }
365365
366366 func TestAppCloseStopsActionWorkerAndReleasesDOM(t *testing.T) {
367- dom := &fakeActionDOM{}
368- history := &fakeHistoryService{entries: []*entity.HistoryEntry{{ID: 1, URL: "https://example.com"}}}
367+ dom := &recordingActionDOM{}
368+ history := &recordingHistoryService{entries: []*entity.HistoryEntry{{ID: 1, URL: "https://example.com"}}}
369369 app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"})
370370
371371 ctx, cancel := context.WithCancel(context.Background())
@@ -396,8 +396,8 @@ func TestAppRunCleansActionWorkerWhenBindingFails(t *testing.T) {
396396 }
397397
398398 func TestAppRunReleasesDOMWhenActionWorkerCannotStart(t *testing.T) {
399- dom := &fakeActionDOM{}
400- history := &fakeHistoryService{entries: []*entity.HistoryEntry{{ID: 1, URL: "https://example.com"}}}
399+ dom := &recordingActionDOM{}
400+ history := &recordingHistoryService{entries: []*entity.HistoryEntry{{ID: 1, URL: "https://example.com"}}}
401401 app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"})
402402
403403 ctx, cancel := context.WithCancel(context.Background())
@@ -415,7 +415,7 @@ func TestAppRunReleasesDOMWhenActionWorkerCannotStart(t *testing.T) {
415415 func TestAppLoadInitialHistoryRouteRendersErrorState(t *testing.T) {
416416 t.Parallel()
417417
418- history := &fakeHistoryService{err: errors.New("database unavailable")}
418+ history := &recordingHistoryService{err: errors.New("database unavailable")}
419419 app := NewApp(Dependencies{
420420 History: history,
421421 LocationURI: "dumb://history",
@@ -432,11 +432,11 @@ func TestAppLoadInitialHistoryRouteRendersErrorState(t *testing.T) {
432432 func TestAppLoadInitialHistoryRouteAppliesThemeTokens(t *testing.T) {
433433 t.Parallel()
434434
435- history := &fakeHistoryService{entries: []*entity.HistoryEntry{{
435+ history := &recordingHistoryService{entries: []*entity.HistoryEntry{{
436436 URL: "https://example.com",
437437 Title: "Example",
438438 }}}
439- config := &fakeConfigService{current: dto.SystemviewConfigPayload{
439+ config := &recordingConfigService{current: dto.SystemviewConfigPayload{
440440 Appearance: dto.WebUIAppearanceConfig{
441441 ColorScheme: "prefer-light",
442442 LightPalette: dto.ColorPalette{
@@ -479,7 +479,7 @@ func TestAppLoadInitialHistoryRouteAppliesThemeTokens(t *testing.T) {
479479 func TestAppLoadInitialFavoritesRouteRendersData(t *testing.T) {
480480 t.Parallel()
481481
482- favorites := &fakeFavoritesService{
482+ favorites := &recordingFavoritesService{
483483 favorites: []*entity.Favorite{{URL: "https://example.com", Title: "Example"}},
484484 folders: []*entity.Folder{{Name: "Read Later"}},
485485 tags: []*entity.Tag{{Name: "Go"}},
@@ -519,7 +519,7 @@ func TestAppLoadInitialFavoritesRouteRendersCRUDControls(t *testing.T) {
519519
520520 folderID := entity.FolderID(1)
521521 shortcut := 3
522- favorites := &fakeFavoritesService{
522+ favorites := &recordingFavoritesService{
523523 favorites: []*entity.Favorite{{ID: 42, URL: "https://example.com", Title: "Example", FolderID: &folderID, ShortcutKey: &shortcut}},
524524 folders: []*entity.Folder{{ID: folderID, Name: "Read Later", Icon: "📚"}},
525525 tags: []*entity.Tag{{ID: 7, Name: "Go", Color: "#00add8"}},
@@ -546,8 +546,8 @@ func TestAppLoadInitialFavoritesRouteRendersCRUDControls(t *testing.T) {
546546 }
547547
548548 func TestAppHandleFavoriteActionsRefreshesDOM(t *testing.T) {
549- dom := &fakeDOM{}
550- favorites := &fakeFavoritesService{
549+ dom := &recordingDOM{}
550+ favorites := &recordingFavoritesService{
551551 favorites: []*entity.Favorite{{ID: 42, URL: "https://example.com", Title: "Example"}},
552552 folders: []*entity.Folder{{ID: 5, Name: "Read Later"}},
553553 tags: []*entity.Tag{{ID: 7, Name: "Go", Color: "#00add8"}},
@@ -675,7 +675,7 @@ func TestAppLoadInitialConfigRouteRendersData(t *testing.T) {
675675 },
676676 },
677677 }
678- service := &fakeConfigService{
678+ service := &recordingConfigService{
679679 current: config,
680680 keybindings: port.KeybindingsConfig{Groups: []port.KeybindingGroup{
681681 {
@@ -756,7 +756,7 @@ func TestAppLoadInitialConfigRouteRendersData(t *testing.T) {
756756 func TestAppLoadInitialConfigRouteRendersEditControls(t *testing.T) {
757757 t.Parallel()
758758
759- service := &fakeConfigService{
759+ service := &recordingConfigService{
760760 current: testConfigPayload(),
761761 keybindings: port.KeybindingsConfig{Groups: []port.KeybindingGroup{{
762762 Mode: "default",
@@ -793,8 +793,8 @@ func TestAppLoadInitialConfigRouteRendersEditControls(t *testing.T) {
793793 }
794794
795795 func TestAppHandleConfigActionsRefreshesDOM(t *testing.T) {
796- dom := &fakeDOM{}
797- service := &fakeConfigService{
796+ dom := &recordingDOM{}
797+ service := &recordingConfigService{
798798 current: testConfigPayload(),
799799 defaultCfg: testDefaultConfigPayload(),
800800 keybindings: port.KeybindingsConfig{Groups: []port.KeybindingGroup{{
@@ -883,7 +883,7 @@ func TestAppHandleConfigActionsRefreshesDOM(t *testing.T) {
883883 func TestAppRejectsSearchURLsWithoutPlaceholder(t *testing.T) {
884884 t.Parallel()
885885
886- service := &fakeConfigService{current: testConfigPayload(), defaultCfg: testDefaultConfigPayload()}
886+ service := &recordingConfigService{current: testConfigPayload(), defaultCfg: testDefaultConfigPayload()}
887887 app := NewApp(Dependencies{Config: service, LocationURI: "dumb://config"})
888888
889889 err := app.handleConfigAction(context.Background(), DOMAction{
@@ -909,7 +909,7 @@ func TestAppLoadInitialConfigRouteUsesResolvedAppearanceOnlyForShellTheme(t *tes
909909 resolved.DarkPalette.Background = "#222222"
910910 resolved.DarkPalette.SurfaceVariant = "#444444"
911911 cfg.ResolvedAppearance = &resolved
912- service := &fakeConfigService{
912+ service := &recordingConfigService{
913913 current: cfg,
914914 keybindings: port.KeybindingsConfig{},
915915 }
@@ -956,8 +956,8 @@ func testDefaultConfigPayload() dto.SystemviewConfigPayload {
956956 return cfg
957957 }
958958
959-// Handwritten fake to capture DOM mounts for stateful render assertions.
960-type fakeDOM struct {
959+// Recording fixture to capture DOM mounts for stateful render assertions.
960+type recordingDOM struct {
961961 mu sync.Mutex
962962 mounted bool
963963 html string
@@ -965,7 +965,7 @@ type fakeDOM struct {
965965 mounts chan string
966966 }
967967
968-func (d *fakeDOM) Mount(markup string) error {
968+func (d *recordingDOM) Mount(markup string) error {
969969 d.mu.Lock()
970970 d.mounted = true
971971 d.html = markup
@@ -980,19 +980,19 @@ func (d *fakeDOM) Mount(markup string) error {
980980 return nil
981981 }
982982
983-func (d *fakeDOM) Mounted() bool {
983+func (d *recordingDOM) Mounted() bool {
984984 d.mu.Lock()
985985 defer d.mu.Unlock()
986986 return d.mounted
987987 }
988988
989-func (d *fakeDOM) HTML() string {
989+func (d *recordingDOM) HTML() string {
990990 d.mu.Lock()
991991 defer d.mu.Unlock()
992992 return d.html
993993 }
994994
995-func (d *fakeDOM) AppendedHTML() string {
995+func (d *recordingDOM) AppendedHTML() string {
996996 d.mu.Lock()
997997 defer d.mu.Unlock()
998998 return d.appendedHTML
@@ -1018,30 +1018,30 @@ func assertNoMount(t *testing.T, mounts <-chan string, wait time.Duration) {
10181018 }
10191019 }
10201020
1021-type fakeActionDOM struct {
1022- fakeDOM
1021+type recordingActionDOM struct {
1022+ recordingDOM
10231023 handler DOMActionHandler
10241024 released bool
10251025 }
10261026
1027-func (d *fakeActionDOM) BindActions(handler DOMActionHandler) error {
1027+func (d *recordingActionDOM) BindActions(handler DOMActionHandler) error {
10281028 d.handler = handler
10291029 return nil
10301030 }
10311031
1032-func (d *fakeActionDOM) AppendHistoryTimeline(markup string) error {
1032+func (d *recordingActionDOM) AppendHistoryTimeline(markup string) error {
10331033 d.mu.Lock()
10341034 d.appendedHTML += markup
10351035 d.mu.Unlock()
10361036 return nil
10371037 }
10381038
1039-func (d *fakeActionDOM) Release() {
1039+func (d *recordingActionDOM) Release() {
10401040 d.released = true
10411041 }
10421042
10431043 type failingActionDOM struct {
1044- fakeDOM
1044+ recordingDOM
10451045 bindErr error
10461046 }
10471047
@@ -1049,8 +1049,8 @@ func (d *failingActionDOM) BindActions(DOMActionHandler) error {
10491049 return d.bindErr
10501050 }
10511051
1052-// Handwritten fake to capture history state for stateful render assertions.
1053-type fakeHistoryService struct {
1052+// Recording fixture to capture history state for stateful render assertions.
1053+type recordingHistoryService struct {
10541054 called bool
10551055 limit int
10561056 offset int
@@ -1080,7 +1080,7 @@ type fakeHistoryService struct {
10801080 startOnce sync.Once
10811081 }
10821082
1083-func (s *fakeHistoryService) Timeline(_ context.Context, limit, offset int) ([]*entity.HistoryEntry, error) {
1083+func (s *recordingHistoryService) Timeline(_ context.Context, limit, offset int) ([]*entity.HistoryEntry, error) {
10841084 s.called = true
10851085 s.limit = limit
10861086 s.offset = offset
@@ -1093,7 +1093,7 @@ func (s *fakeHistoryService) Timeline(_ context.Context, limit, offset int) ([]*
10931093 return s.entries, s.err
10941094 }
10951095
1096-func (s *fakeHistoryService) TimelineByDomain(_ context.Context, domain string, limit, offset int) ([]*entity.HistoryEntry, error) {
1096+func (s *recordingHistoryService) TimelineByDomain(_ context.Context, domain string, limit, offset int) ([]*entity.HistoryEntry, error) {
10971097 s.domainCalled = true
10981098 s.domain = domain
10991099 s.limit = limit
@@ -1101,7 +1101,7 @@ func (s *fakeHistoryService) TimelineByDomain(_ context.Context, domain string,
11011101 return s.entries, s.err
11021102 }
11031103
1104-func (s *fakeHistoryService) TimelineWindow(_ context.Context, before time.Time, domain string) (*entity.HistoryWindow, error) {
1104+func (s *recordingHistoryService) TimelineWindow(_ context.Context, before time.Time, domain string) (*entity.HistoryWindow, error) {
11051105 s.called = true
11061106 s.windowBefore = before
11071107 s.windowDomain = domain
@@ -1120,7 +1120,7 @@ func (s *fakeHistoryService) TimelineWindow(_ context.Context, before time.Time,
11201120 return &entity.HistoryWindow{Entries: s.entries, Before: before, After: before.Add(-24 * time.Hour)}, nil
11211121 }
11221122
1123-func (s *fakeHistoryService) Search(_ context.Context, query string, limit int) ([]*entity.HistoryEntry, error) {
1123+func (s *recordingHistoryService) Search(_ context.Context, query string, limit int) ([]*entity.HistoryEntry, error) {
11241124 s.searchCalled = true
11251125 s.query = query
11261126 s.searchLimit = limit
@@ -1130,37 +1130,37 @@ func (s *fakeHistoryService) Search(_ context.Context, query string, limit int)
11301130 return s.entries, nil
11311131 }
11321132
1133-func (s *fakeHistoryService) DeleteEntry(_ context.Context, id int64) error {
1133+func (s *recordingHistoryService) DeleteEntry(_ context.Context, id int64) error {
11341134 s.deletedEntryID = id
11351135 return nil
11361136 }
11371137
1138-func (s *fakeHistoryService) DeleteRange(_ context.Context, rangeID string) error {
1138+func (s *recordingHistoryService) DeleteRange(_ context.Context, rangeID string) error {
11391139 s.deletedRangeID = rangeID
11401140 return nil
11411141 }
11421142
1143-func (s *fakeHistoryService) Stats(context.Context) (*entity.HistoryStats, error) {
1143+func (s *recordingHistoryService) Stats(context.Context) (*entity.HistoryStats, error) {
11441144 s.statsCalled = true
11451145 return s.stats, nil
11461146 }
11471147
1148-func (s *fakeHistoryService) Analytics(context.Context) (*entity.HistoryAnalytics, error) {
1148+func (s *recordingHistoryService) Analytics(context.Context) (*entity.HistoryAnalytics, error) {
11491149 s.analyticsCalled = true
11501150 return nil, nil
11511151 }
11521152
1153-func (s *fakeHistoryService) DomainStats(context.Context, int) ([]*entity.DomainStat, error) {
1153+func (s *recordingHistoryService) DomainStats(context.Context, int) ([]*entity.DomainStat, error) {
11541154 return s.domainStats, nil
11551155 }
11561156
1157-func (s *fakeHistoryService) DeleteDomain(_ context.Context, domain string) error {
1157+func (s *recordingHistoryService) DeleteDomain(_ context.Context, domain string) error {
11581158 s.deletedDomain = domain
11591159 return nil
11601160 }
11611161
1162-// Handwritten fake to capture favorites state for stateful render assertions.
1163-type fakeFavoritesService struct {
1162+// Recording fixture to capture favorites state for stateful render assertions.
1163+type recordingFavoritesService struct {
11641164 calledList bool
11651165 calledFolders bool
11661166 calledTags bool
@@ -1184,85 +1184,85 @@ type fakeFavoritesService struct {
11841184 removedTagID int64
11851185 }
11861186
1187-func (s *fakeFavoritesService) List(context.Context) ([]*entity.Favorite, error) {
1187+func (s *recordingFavoritesService) List(context.Context) ([]*entity.Favorite, error) {
11881188 s.calledList = true
11891189 return s.favorites, nil
11901190 }
11911191
1192-func (s *fakeFavoritesService) ListFolders(context.Context) ([]*entity.Folder, error) {
1192+func (s *recordingFavoritesService) ListFolders(context.Context) ([]*entity.Folder, error) {
11931193 s.calledFolders = true
11941194 return s.folders, nil
11951195 }
11961196
1197-func (s *fakeFavoritesService) ListTags(context.Context) ([]*entity.Tag, error) {
1197+func (s *recordingFavoritesService) ListTags(context.Context) ([]*entity.Tag, error) {
11981198 s.calledTags = true
11991199 return s.tags, nil
12001200 }
12011201
1202-func (s *fakeFavoritesService) CreateFavorite(_ context.Context, input dto.FavoriteCreateInput) (*entity.Favorite, error) {
1202+func (s *recordingFavoritesService) CreateFavorite(_ context.Context, input dto.FavoriteCreateInput) (*entity.Favorite, error) {
12031203 s.createdFavorite = input
12041204 return &entity.Favorite{ID: 99, URL: input.URL, Title: input.Title, FolderID: input.FolderID}, nil
12051205 }
12061206
1207-func (s *fakeFavoritesService) UpdateFavorite(_ context.Context, input dto.FavoriteUpdateInput) (*entity.Favorite, error) {
1207+func (s *recordingFavoritesService) UpdateFavorite(_ context.Context, input dto.FavoriteUpdateInput) (*entity.Favorite, error) {
12081208 s.updatedFavorite = input
12091209 return &entity.Favorite{ID: input.ID, URL: "https://example.com", Title: input.Title, FaviconURL: input.FaviconURL, FolderID: input.FolderID, ShortcutKey: input.ShortcutKey}, nil
12101210 }
12111211
1212-func (s *fakeFavoritesService) DeleteFavorite(_ context.Context, id int64) error {
1212+func (s *recordingFavoritesService) DeleteFavorite(_ context.Context, id int64) error {
12131213 s.deletedFavorite = id
12141214 return nil
12151215 }
12161216
1217-func (s *fakeFavoritesService) SetShortcut(context.Context, int64, *int) error { return nil }
1217+func (s *recordingFavoritesService) SetShortcut(context.Context, int64, *int) error { return nil }
12181218
1219-func (s *fakeFavoritesService) SetFolder(context.Context, int64, *int64) error { return nil }
1219+func (s *recordingFavoritesService) SetFolder(context.Context, int64, *int64) error { return nil }
12201220
1221-func (s *fakeFavoritesService) CreateFolder(_ context.Context, name, icon string, _ *int64) (*entity.Folder, error) {
1221+func (s *recordingFavoritesService) CreateFolder(_ context.Context, name, icon string, _ *int64) (*entity.Folder, error) {
12221222 s.createdFolder = name
12231223 s.createdFolderIcon = icon
12241224 return &entity.Folder{ID: 77, Name: name, Icon: icon}, nil
12251225 }
12261226
1227-func (s *fakeFavoritesService) UpdateFolder(_ context.Context, id int64, _, _ string) error {
1227+func (s *recordingFavoritesService) UpdateFolder(_ context.Context, id int64, _, _ string) error {
12281228 s.updatedFolderID = id
12291229 return nil
12301230 }
12311231
1232-func (s *fakeFavoritesService) DeleteFolder(_ context.Context, id int64) error {
1232+func (s *recordingFavoritesService) DeleteFolder(_ context.Context, id int64) error {
12331233 s.deletedFolderID = id
12341234 return nil
12351235 }
12361236
1237-func (s *fakeFavoritesService) CreateTag(_ context.Context, name, color string) (*entity.Tag, error) {
1237+func (s *recordingFavoritesService) CreateTag(_ context.Context, name, color string) (*entity.Tag, error) {
12381238 s.createdTag = name
12391239 return &entity.Tag{ID: 55, Name: name, Color: color}, nil
12401240 }
12411241
1242-func (s *fakeFavoritesService) UpdateTag(_ context.Context, id int64, _, _ string) error {
1242+func (s *recordingFavoritesService) UpdateTag(_ context.Context, id int64, _, _ string) error {
12431243 s.updatedTagID = id
12441244 return nil
12451245 }
12461246
1247-func (s *fakeFavoritesService) DeleteTag(_ context.Context, id int64) error {
1247+func (s *recordingFavoritesService) DeleteTag(_ context.Context, id int64) error {
12481248 s.deletedTagID = id
12491249 return nil
12501250 }
12511251
1252-func (s *fakeFavoritesService) AssignTag(_ context.Context, favoriteID, tagID int64) error {
1252+func (s *recordingFavoritesService) AssignTag(_ context.Context, favoriteID, tagID int64) error {
12531253 s.assignedFavoriteID = favoriteID
12541254 s.assignedTagID = tagID
12551255 return nil
12561256 }
12571257
1258-func (s *fakeFavoritesService) RemoveTag(_ context.Context, favoriteID, tagID int64) error {
1258+func (s *recordingFavoritesService) RemoveTag(_ context.Context, favoriteID, tagID int64) error {
12591259 s.removedFavoriteID = favoriteID
12601260 s.removedTagID = tagID
12611261 return nil
12621262 }
12631263
1264-// Handwritten fake to capture config state for stateful render assertions.
1265-type fakeConfigService struct {
1264+// Recording fixture to capture config state for stateful render assertions.
1265+type recordingConfigService struct {
12661266 calledCurrent bool
12671267 calledDefault bool
12681268 calledSave bool
@@ -1280,17 +1280,17 @@ type fakeConfigService struct {
12801280 resetReq port.ResetKeybindingRequest
12811281 }
12821282
1283-func (s *fakeConfigService) Current(context.Context) (dto.SystemviewConfigPayload, error) {
1283+func (s *recordingConfigService) Current(context.Context) (dto.SystemviewConfigPayload, error) {
12841284 s.calledCurrent = true
12851285 return s.current, nil
12861286 }
12871287
1288-func (s *fakeConfigService) Default(context.Context) (dto.SystemviewConfigPayload, error) {
1288+func (s *recordingConfigService) Default(context.Context) (dto.SystemviewConfigPayload, error) {
12891289 s.calledDefault = true
12901290 return s.defaultCfg, nil
12911291 }
12921292
1293-func (s *fakeConfigService) Save(_ context.Context, cfg dto.WebUIConfig) error {
1293+func (s *recordingConfigService) Save(_ context.Context, cfg dto.WebUIConfig) error {
12941294 s.calledSave = true
12951295 s.savedConfig = cfg
12961296 s.current.Appearance = cfg.Appearance
@@ -1308,30 +1308,30 @@ func (s *fakeConfigService) Save(_ context.Context, cfg dto.WebUIConfig) error {
13081308 return nil
13091309 }
13101310
1311-func (s *fakeConfigService) GetKeybindings(context.Context) (port.KeybindingsConfig, error) {
1311+func (s *recordingConfigService) GetKeybindings(context.Context) (port.KeybindingsConfig, error) {
13121312 s.calledKeybindings = true
13131313 return s.keybindings, nil
13141314 }
13151315
1316-func (s *fakeConfigService) SetKeybinding(_ context.Context, req port.SetKeybindingRequest) (port.SetKeybindingResponse, error) {
1316+func (s *recordingConfigService) SetKeybinding(_ context.Context, req port.SetKeybindingRequest) (port.SetKeybindingResponse, error) {
13171317 s.calledSet = true
13181318 s.setReq = req
13191319 return s.setResp, nil
13201320 }
13211321
1322-func (s *fakeConfigService) ResetKeybinding(_ context.Context, req port.ResetKeybindingRequest) error {
1322+func (s *recordingConfigService) ResetKeybinding(_ context.Context, req port.ResetKeybindingRequest) error {
13231323 s.calledReset = true
13241324 s.resetReq = req
13251325 return nil
13261326 }
13271327
1328-func (s *fakeConfigService) ResetAllKeybindings(context.Context) error {
1328+func (s *recordingConfigService) ResetAllKeybindings(context.Context) error {
13291329 s.calledResetAll = true
13301330 return nil
13311331 }
13321332
13331333 func TestHandleFavoriteActionValidatesCreateURL(t *testing.T) {
1334- app := NewApp(Dependencies{Favorites: &fakeFavoritesService{}})
1334+ app := NewApp(Dependencies{Favorites: &recordingFavoritesService{}})
13351335
13361336 err := app.handleFavoriteAction(context.Background(), DOMAction{
13371337 Action: favoriteActionCreate,
@@ -1356,7 +1356,7 @@ func TestHandleFavoriteActionValidatesCreateURL(t *testing.T) {
13561356 }
13571357
13581358 func TestHandleFavoriteCreateAcceptsInternalDumbRoutes(t *testing.T) {
1359- favorites := &fakeFavoritesService{}
1359+ favorites := &recordingFavoritesService{}
13601360 app := NewApp(Dependencies{Favorites: favorites})
13611361
13621362 require.NoError(t, app.handleFavoriteAction(context.Background(), DOMAction{
@@ -1373,7 +1373,7 @@ func TestHandleFavoriteCreateAcceptsInternalDumbRoutes(t *testing.T) {
13731373 }
13741374
13751375 func TestHandleFavoriteCreateRejectsUnknownDumbRoutes(t *testing.T) {
1376- favorites := &fakeFavoritesService{}
1376+ favorites := &recordingFavoritesService{}
13771377 app := NewApp(Dependencies{Favorites: favorites})
13781378
13791379 err := app.handleFavoriteAction(context.Background(), DOMAction{
@@ -1397,7 +1397,7 @@ func TestParsePositiveInt64DistinguishesParseAndRangeErrors(t *testing.T) {
13971397 }
13981398
13991399 func TestHandleFavoriteTagActionsAcceptSnakeCaseIDs(t *testing.T) {
1400- favorites := &fakeFavoritesService{}
1400+ favorites := &recordingFavoritesService{}
14011401 app := NewApp(Dependencies{Favorites: favorites})
14021402
14031403 require.NoError(t, app.handleFavoriteAction(context.Background(), DOMAction{
@@ -1416,8 +1416,8 @@ func TestHandleFavoriteTagActionsAcceptSnakeCaseIDs(t *testing.T) {
14161416 }
14171417
14181418 func TestHandleHistorySearchClearsDomainFilter(t *testing.T) {
1419- dom := &fakeDOM{}
1420- history := &fakeHistoryService{searchEntries: []*entity.HistoryEntry{{ID: 7, URL: "https://search.example", Title: "Search result"}}}
1419+ dom := &recordingDOM{}
1420+ history := &recordingHistoryService{searchEntries: []*entity.HistoryEntry{{ID: 7, URL: "https://search.example", Title: "Search result"}}}
14211421 app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"})
14221422 app.historyDomainFilter = "example.com"
14231423
@@ -1433,7 +1433,7 @@ func TestHandleHistorySearchClearsDomainFilter(t *testing.T) {
14331433 }
14341434
14351435 func TestAppRunWithContextRejectsNilContext(t *testing.T) {
1436- app := NewApp(Dependencies{DOM: &fakeDOM{}, LocationURI: "dumb://history"})
1436+ app := NewApp(Dependencies{DOM: &recordingDOM{}, LocationURI: "dumb://history"})
14371437
14381438 var ctx context.Context
14391439 err := app.RunWithContext(ctx)
@@ -1442,7 +1442,7 @@ func TestAppRunWithContextRejectsNilContext(t *testing.T) {
14421442 }
14431443
14441444 func TestAppRunWithContextRequiresDOMBeforeLoading(t *testing.T) {
1445- history := &fakeHistoryService{entries: []*entity.HistoryEntry{{ID: 1, URL: "https://example.com"}}}
1445+ history := &recordingHistoryService{entries: []*entity.HistoryEntry{{ID: 1, URL: "https://example.com"}}}
14461446 app := NewApp(Dependencies{History: history, LocationURI: "dumb://history"})
14471447
14481448 err := app.RunWithContext(context.Background())
@@ -1453,9 +1453,9 @@ func TestAppRunWithContextRequiresDOMBeforeLoading(t *testing.T) {
14531453
14541454 func TestHandleActionsRejectUnknownAction(t *testing.T) {
14551455 app := NewApp(Dependencies{
1456- History: &fakeHistoryService{},
1457- Favorites: &fakeFavoritesService{},
1458- Config: &fakeConfigService{current: testConfigPayload()},
1456+ History: &recordingHistoryService{},
1457+ Favorites: &recordingFavoritesService{},
1458+ Config: &recordingConfigService{current: testConfigPayload()},
14591459 })
14601460
14611461 require.ErrorContains(t, app.handleHistoryAction(context.Background(), DOMAction{Action: "history.unknown"}), "unknown history action")
@@ -1464,7 +1464,7 @@ func TestHandleActionsRejectUnknownAction(t *testing.T) {
14641464 }
14651465
14661466 func TestSavePerformanceConfigRejectsOutOfRangeValues(t *testing.T) {
1467- service := &fakeConfigService{current: testConfigPayload()}
1467+ service := &recordingConfigService{current: testConfigPayload()}
14681468 app := NewApp(Dependencies{Config: service})
14691469
14701470 err := app.handleConfigAction(context.Background(), DOMAction{

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close