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
2 template-data:2 template-data:
3 unroll-variadic: true3 unroll-variadic: true
4 packages:4 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: {}
5 github.com/bnema/dumber/internal/application/port:46 github.com/bnema/dumber/internal/application/port:
6 config:47 config:
7 dir: '{{.InterfaceDir}}/mocks'48 dir: '{{.InterfaceDir}}/mocks'
@@ -58,6 +99,12 @@ packages:
58 DesktopIntegration: {}99 DesktopIntegration: {}
59 DownloadEventHandler: {}100 DownloadEventHandler: {}
60 DownloadPreparer: {}101 DownloadPreparer: {}
102+ FaviconBlobStore: {}
103+ FaviconFetcher: {}
104+ FaviconImageConverter: {}
105+ FaviconInvalidators: {}
106+ FaviconRefreshScheduler: {}
107+ FaviconRepository: {}
61 FaviconService: {}108 FaviconService: {}
62 FileSystem: {}109 FileSystem: {}
63 FilterManager: {}110 FilterManager: {}
@@ -134,3 +181,19 @@ packages:
134 SpinnerWidget: {}181 SpinnerWidget: {}
135 Widget: {}182 Widget: {}
136 WidgetFactory: {}183 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 (
17 func TestNewBridgeApp_WiresHistoryService(t *testing.T) {17 func TestNewBridgeApp_WiresHistoryService(t *testing.T) {
18 t.Parallel()18 t.Parallel()
19 19
20- bridge := &fakeBridgeService{historyEntries: []*entity.HistoryEntry{{Title: "Example", URL: "https://example.com"}}}20+ bridge := &bridgeServiceRecorder{historyEntries: []*entity.HistoryEntry{{Title: "Example", URL: "https://example.com"}}}
21- app := newBridgeApp(&fakeDOM{}, "dumb://history", bridge)21+ app := newBridgeApp(&recordingDOM{}, "dumb://history", bridge)
22 22
23 require.NoError(t, app.LoadInitial(context.Background()))23 require.NoError(t, app.LoadInitial(context.Background()))
24 assert.True(t, bridge.calledHistory.Load())24 assert.True(t, bridge.calledHistory.Load())
@@ -30,7 +30,7 @@ func TestNewBridgeApp_WiresHistoryService(t *testing.T) {
30 func TestNewBridgeApp_UsesCurrentConfigForNonConfigRoutes(t *testing.T) {30 func TestNewBridgeApp_UsesCurrentConfigForNonConfigRoutes(t *testing.T) {
31 t.Parallel()31 t.Parallel()
32 32
33- bridge := &fakeBridgeService{33+ bridge := &bridgeServiceRecorder{
34 historyEntries: []*entity.HistoryEntry{{Title: "Example", URL: "https://example.com"}},34 historyEntries: []*entity.HistoryEntry{{Title: "Example", URL: "https://example.com"}},
35 currentConfig: dto.SystemviewConfigPayload{35 currentConfig: dto.SystemviewConfigPayload{
36 Appearance: dto.WebUIAppearanceConfig{36 Appearance: dto.WebUIAppearanceConfig{
@@ -47,7 +47,7 @@ func TestNewBridgeApp_UsesCurrentConfigForNonConfigRoutes(t *testing.T) {
47 },47 },
48 },48 },
49 }49 }
50- dom := &fakeDOM{mounts: make(chan string, 4)}50+ dom := &recordingDOM{mounts: make(chan string, 4)}
51 app := newBridgeApp(dom, "dumb://history", bridge)51 app := newBridgeApp(dom, "dumb://history", bridge)
52 52
53 require.NoError(t, app.Run())53 require.NoError(t, app.Run())
@@ -62,12 +62,12 @@ func TestNewBridgeApp_UsesCurrentConfigForNonConfigRoutes(t *testing.T) {
62 func TestNewBridgeApp_WiresFavoritesService(t *testing.T) {62 func TestNewBridgeApp_WiresFavoritesService(t *testing.T) {
63 t.Parallel()63 t.Parallel()
64 64
65- bridge := &fakeBridgeService{65+ bridge := &bridgeServiceRecorder{
66 favorites: []*entity.Favorite{{Title: "Example", URL: "https://example.com"}},66 favorites: []*entity.Favorite{{Title: "Example", URL: "https://example.com"}},
67 folders: []*entity.Folder{{Name: "Read Later"}},67 folders: []*entity.Folder{{Name: "Read Later"}},
68 tags: []*entity.Tag{{Name: "Go"}},68 tags: []*entity.Tag{{Name: "Go"}},
69 }69 }
70- app := newBridgeApp(&fakeDOM{}, "dumb://favorites", bridge)70+ app := newBridgeApp(&recordingDOM{}, "dumb://favorites", bridge)
71 71
72 require.NoError(t, app.LoadInitial(context.Background()))72 require.NoError(t, app.LoadInitial(context.Background()))
73 assert.False(t, bridge.calledHistory.Load())73 assert.False(t, bridge.calledHistory.Load())
@@ -79,11 +79,11 @@ func TestNewBridgeApp_WiresFavoritesService(t *testing.T) {
79 func TestNewBridgeApp_WiresConfigService(t *testing.T) {79 func TestNewBridgeApp_WiresConfigService(t *testing.T) {
80 t.Parallel()80 t.Parallel()
81 81
82- bridge := &fakeBridgeService{82+ bridge := &bridgeServiceRecorder{
83 currentConfig: dto.SystemviewConfigPayload{EngineType: "webkit"},83 currentConfig: dto.SystemviewConfigPayload{EngineType: "webkit"},
84 keybindings: port.KeybindingsConfig{Groups: []port.KeybindingGroup{{DisplayName: "Default"}}},84 keybindings: port.KeybindingsConfig{Groups: []port.KeybindingGroup{{DisplayName: "Default"}}},
85 }85 }
86- app := newBridgeApp(&fakeDOM{}, "dumb://config", bridge)86+ app := newBridgeApp(&recordingDOM{}, "dumb://config", bridge)
87 87
88 require.NoError(t, app.LoadInitial(context.Background()))88 require.NoError(t, app.LoadInitial(context.Background()))
89 assert.False(t, bridge.calledHistory.Load())89 assert.False(t, bridge.calledHistory.Load())
@@ -92,12 +92,12 @@ func TestNewBridgeApp_WiresConfigService(t *testing.T) {
92 assert.True(t, bridge.calledKeybindings.Load())92 assert.True(t, bridge.calledKeybindings.Load())
93 }93 }
94 94
95-type fakeDOM struct {95+type recordingDOM struct {
96 html string96 html string
97 mounts chan string97 mounts chan string
98 }98 }
99 99
100-func (f *fakeDOM) Mount(html string) error {100+func (f *recordingDOM) Mount(html string) error {
101 f.html = html101 f.html = html
102 if f.mounts != nil {102 if f.mounts != nil {
103 select {103 select {
@@ -131,9 +131,9 @@ func receiveMountContaining(t *testing.T, mounts <-chan string, values ...string
131 }131 }
132 }132 }
133 133
134-// Handwritten fake intentionally tracks state across history, favorites, config,134+// Recording fixture intentionally tracks state across history, favorites, config,
135 // and keybindings boundaries for the composite bootstrap assertions.135 // and keybindings boundaries for the composite bootstrap assertions.
136-type fakeBridgeService struct {136+type bridgeServiceRecorder struct {
137 calledHistory atomic.Bool137 calledHistory atomic.Bool
138 calledFavorites atomic.Bool138 calledFavorites atomic.Bool
139 calledConfig atomic.Bool139 calledConfig atomic.Bool
@@ -147,112 +147,112 @@ type fakeBridgeService struct {
147 keybindings port.KeybindingsConfig147 keybindings port.KeybindingsConfig
148 }148 }
149 149
150-func (f *fakeBridgeService) Timeline(context.Context, int, int) ([]*entity.HistoryEntry, error) {150+func (f *bridgeServiceRecorder) Timeline(context.Context, int, int) ([]*entity.HistoryEntry, error) {
151 f.calledHistory.Store(true)151 f.calledHistory.Store(true)
152 return f.historyEntries, nil152 return f.historyEntries, nil
153 }153 }
154 154
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) {
156 f.calledHistory.Store(true)156 f.calledHistory.Store(true)
157 return f.historyEntries, nil157 return f.historyEntries, nil
158 }158 }
159 159
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) {
161 f.calledHistory.Store(true)161 f.calledHistory.Store(true)
162 return &entity.HistoryWindow{Entries: f.historyEntries, Before: before, After: before.Add(-24 * time.Hour)}, nil162 return &entity.HistoryWindow{Entries: f.historyEntries, Before: before, After: before.Add(-24 * time.Hour)}, nil
163 }163 }
164 164
165-func (*fakeBridgeService) Search(context.Context, string, int) ([]*entity.HistoryEntry, error) {165+func (*bridgeServiceRecorder) Search(context.Context, string, int) ([]*entity.HistoryEntry, error) {
166 return nil, nil166 return nil, nil
167 }167 }
168 168
169-func (*fakeBridgeService) DeleteEntry(context.Context, int64) error { return nil }169+func (*bridgeServiceRecorder) DeleteEntry(context.Context, int64) error { return nil }
170 170
171-func (*fakeBridgeService) DeleteRange(context.Context, string) error { return nil }171+func (*bridgeServiceRecorder) DeleteRange(context.Context, string) error { return nil }
172 172
173-func (*fakeBridgeService) Stats(context.Context) (*entity.HistoryStats, error) {173+func (*bridgeServiceRecorder) Stats(context.Context) (*entity.HistoryStats, error) {
174 return nil, nil174 return nil, nil
175 }175 }
176 176
177-func (*fakeBridgeService) Analytics(context.Context) (*entity.HistoryAnalytics, error) {177+func (*bridgeServiceRecorder) Analytics(context.Context) (*entity.HistoryAnalytics, error) {
178 return nil, nil178 return nil, nil
179 }179 }
180 180
181-func (*fakeBridgeService) DomainStats(context.Context, int) ([]*entity.DomainStat, error) {181+func (*bridgeServiceRecorder) DomainStats(context.Context, int) ([]*entity.DomainStat, error) {
182 return nil, nil182 return nil, nil
183 }183 }
184 184
185-func (*fakeBridgeService) DeleteDomain(context.Context, string) error { return nil }185+func (*bridgeServiceRecorder) DeleteDomain(context.Context, string) error { return nil }
186 186
187-func (f *fakeBridgeService) List(context.Context) ([]*entity.Favorite, error) {187+func (f *bridgeServiceRecorder) List(context.Context) ([]*entity.Favorite, error) {
188 f.calledFavorites.Store(true)188 f.calledFavorites.Store(true)
189 return f.favorites, nil189 return f.favorites, nil
190 }190 }
191 191
192-func (*fakeBridgeService) CreateFavorite(context.Context, dto.FavoriteCreateInput) (*entity.Favorite, error) {192+func (*bridgeServiceRecorder) CreateFavorite(context.Context, dto.FavoriteCreateInput) (*entity.Favorite, error) {
193 return nil, nil193 return nil, nil
194 }194 }
195 195
196-func (*fakeBridgeService) UpdateFavorite(context.Context, dto.FavoriteUpdateInput) (*entity.Favorite, error) {196+func (*bridgeServiceRecorder) UpdateFavorite(context.Context, dto.FavoriteUpdateInput) (*entity.Favorite, error) {
197 return nil, nil197 return nil, nil
198 }198 }
199 199
200-func (*fakeBridgeService) DeleteFavorite(context.Context, int64) error { return nil }200+func (*bridgeServiceRecorder) DeleteFavorite(context.Context, int64) error { return nil }
201 201
202-func (f *fakeBridgeService) ListFolders(context.Context) ([]*entity.Folder, error) {202+func (f *bridgeServiceRecorder) ListFolders(context.Context) ([]*entity.Folder, error) {
203 return f.folders, nil203 return f.folders, nil
204 }204 }
205 205
206-func (f *fakeBridgeService) ListTags(context.Context) ([]*entity.Tag, error) {206+func (f *bridgeServiceRecorder) ListTags(context.Context) ([]*entity.Tag, error) {
207 return f.tags, nil207 return f.tags, nil
208 }208 }
209 209
210-func (*fakeBridgeService) SetShortcut(context.Context, int64, *int) error { return nil }210+func (*bridgeServiceRecorder) SetShortcut(context.Context, int64, *int) error { return nil }
211 211
212-func (*fakeBridgeService) SetFolder(context.Context, int64, *int64) error { return nil }212+func (*bridgeServiceRecorder) SetFolder(context.Context, int64, *int64) error { return nil }
213 213
214-func (*fakeBridgeService) CreateFolder(context.Context, string, string, *int64) (*entity.Folder, error) {214+func (*bridgeServiceRecorder) CreateFolder(context.Context, string, string, *int64) (*entity.Folder, error) {
215 return nil, nil215 return nil, nil
216 }216 }
217 217
218-func (*fakeBridgeService) UpdateFolder(context.Context, int64, string, string) error { return nil }218+func (*bridgeServiceRecorder) UpdateFolder(context.Context, int64, string, string) error { return nil }
219 219
220-func (*fakeBridgeService) DeleteFolder(context.Context, int64) error { return nil }220+func (*bridgeServiceRecorder) DeleteFolder(context.Context, int64) error { return nil }
221 221
222-func (*fakeBridgeService) CreateTag(context.Context, string, string) (*entity.Tag, error) {222+func (*bridgeServiceRecorder) CreateTag(context.Context, string, string) (*entity.Tag, error) {
223 return nil, nil223 return nil, nil
224 }224 }
225 225
226-func (*fakeBridgeService) UpdateTag(context.Context, int64, string, string) error { return nil }226+func (*bridgeServiceRecorder) UpdateTag(context.Context, int64, string, string) error { return nil }
227 227
228-func (*fakeBridgeService) DeleteTag(context.Context, int64) error { return nil }228+func (*bridgeServiceRecorder) DeleteTag(context.Context, int64) error { return nil }
229 229
230-func (*fakeBridgeService) AssignTag(context.Context, int64, int64) error { return nil }230+func (*bridgeServiceRecorder) AssignTag(context.Context, int64, int64) error { return nil }
231 231
232-func (*fakeBridgeService) RemoveTag(context.Context, int64, int64) error { return nil }232+func (*bridgeServiceRecorder) RemoveTag(context.Context, int64, int64) error { return nil }
233 233
234-func (f *fakeBridgeService) Current(context.Context) (dto.SystemviewConfigPayload, error) {234+func (f *bridgeServiceRecorder) Current(context.Context) (dto.SystemviewConfigPayload, error) {
235 f.calledConfig.Store(true)235 f.calledConfig.Store(true)
236 return f.currentConfig, nil236 return f.currentConfig, nil
237 }237 }
238 238
239-func (*fakeBridgeService) Default(context.Context) (dto.SystemviewConfigPayload, error) {239+func (*bridgeServiceRecorder) Default(context.Context) (dto.SystemviewConfigPayload, error) {
240 return dto.SystemviewConfigPayload{}, nil240 return dto.SystemviewConfigPayload{}, nil
241 }241 }
242 242
243-func (*fakeBridgeService) Save(context.Context, dto.WebUIConfig) error { return nil }243+func (*bridgeServiceRecorder) Save(context.Context, dto.WebUIConfig) error { return nil }
244 244
245-func (f *fakeBridgeService) GetKeybindings(context.Context) (port.KeybindingsConfig, error) {245+func (f *bridgeServiceRecorder) GetKeybindings(context.Context) (port.KeybindingsConfig, error) {
246 f.calledKeybindings.Store(true)246 f.calledKeybindings.Store(true)
247 return f.keybindings, nil247 return f.keybindings, nil
248 }248 }
249 249
250-func (*fakeBridgeService) SetKeybinding(context.Context, port.SetKeybindingRequest) (port.SetKeybindingResponse, error) {250+func (*bridgeServiceRecorder) SetKeybinding(context.Context, port.SetKeybindingRequest) (port.SetKeybindingResponse, error) {
251 return port.SetKeybindingResponse{}, nil251 return port.SetKeybindingResponse{}, nil
252 }252 }
253 253
254-func (*fakeBridgeService) ResetKeybinding(context.Context, port.ResetKeybindingRequest) error {254+func (*bridgeServiceRecorder) ResetKeybinding(context.Context, port.ResetKeybindingRequest) error {
255 return nil255 return nil
256 }256 }
257 257
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 (
7 "time"7 "time"
8 8
9 appport "github.com/bnema/dumber/internal/application/port"9 appport "github.com/bnema/dumber/internal/application/port"
10+ portmocks "github.com/bnema/dumber/internal/application/port/mocks"
10 "github.com/bnema/dumber/internal/domain/favicon"11 "github.com/bnema/dumber/internal/domain/favicon"
12+ "github.com/stretchr/testify/mock"
11 )13 )
12 14
13 func TestFaviconResolveExactHit(t *testing.T) {15 func TestFaviconResolveExactHit(t *testing.T) {
14- fx := newFaviconFixture()16+ fx := newFaviconFixture(t)
15 fx.seed("docs.example.com", []byte("png32"), false)17 fx.seed("docs.example.com", []byte("png32"), false)
16 got, err := fx.uc.Resolve(context.Background(), "https://docs.example.com/a", 32, ResolveOptions{Purpose: ResolvePurposeUI})18 got, err := fx.uc.Resolve(context.Background(), "https://docs.example.com/a", 32, ResolveOptions{Purpose: ResolvePurposeUI})
17 if err != nil {19 if err != nil {
@@ -23,7 +25,7 @@ func TestFaviconResolveExactHit(t *testing.T) {
23 }25 }
24 26
25 func TestFaviconResolveParentFallback(t *testing.T) {27 func TestFaviconResolveParentFallback(t *testing.T) {
26- fx := newFaviconFixture()28+ fx := newFaviconFixture(t)
27 fx.seed("example.com", []byte("parent"), false)29 fx.seed("example.com", []byte("parent"), false)
28 got, err := fx.uc.Resolve(context.Background(), "https://docs.example.com/a", 32, ResolveOptions{Purpose: ResolvePurposeUI})30 got, err := fx.uc.Resolve(context.Background(), "https://docs.example.com/a", 32, ResolveOptions{Purpose: ResolvePurposeUI})
29 if err != nil {31 if err != nil {
@@ -35,7 +37,7 @@ func TestFaviconResolveParentFallback(t *testing.T) {
35 }37 }
36 38
37 func TestFaviconResolvePrefersParentPathFallbackBeforeDomain(t *testing.T) {39 func TestFaviconResolvePrefersParentPathFallbackBeforeDomain(t *testing.T) {
38- fx := newFaviconFixture()40+ fx := newFaviconFixture(t)
39 fx.seed("github.com", []byte("github-default"), false)41 fx.seed("github.com", []byte("github-default"), false)
40 fx.seed("github.com/bnema/gordon", []byte("repo-default"), false)42 fx.seed("github.com/bnema/gordon", []byte("repo-default"), false)
41 43
@@ -49,7 +51,7 @@ func TestFaviconResolvePrefersParentPathFallbackBeforeDomain(t *testing.T) {
49 }51 }
50 52
51 func TestFaviconResolvePathSpecificDoesNotLeakToParentOrSiblings(t *testing.T) {53 func TestFaviconResolvePathSpecificDoesNotLeakToParentOrSiblings(t *testing.T) {
52- fx := newFaviconFixture()54+ fx := newFaviconFixture(t)
53 fx.seed("github.com", []byte("github-default"), false)55 fx.seed("github.com", []byte("github-default"), false)
54 56
55 if _, err := fx.uc.Observe(57 if _, err := fx.uc.Observe(
@@ -91,7 +93,7 @@ func TestFaviconResolvePathSpecificDoesNotLeakToParentOrSiblings(t *testing.T) {
91 }93 }
92 94
93 func TestFaviconResolveRepairsOrphanCanonicalBlobMetadata(t *testing.T) {95 func TestFaviconResolveRepairsOrphanCanonicalBlobMetadata(t *testing.T) {
94- fx := newFaviconFixture()96+ fx := newFaviconFixture(t)
95 fx.blobs.png["example.com"] = []byte("png")97 fx.blobs.png["example.com"] = []byte("png")
96 fx.blobs.sized["example.com"] = map[int][]byte{SystemviewIconSize: []byte("png32")}98 fx.blobs.sized["example.com"] = map[int][]byte{SystemviewIconSize: []byte("png32")}
97 99
@@ -112,7 +114,7 @@ func TestFaviconResolveRepairsOrphanCanonicalBlobMetadata(t *testing.T) {
112 }114 }
113 115
114 func TestFaviconResolveRepairOrphanWithoutConverterReturnsMiss(t *testing.T) {116 func TestFaviconResolveRepairOrphanWithoutConverterReturnsMiss(t *testing.T) {
115- fx := newFaviconFixture()117+ fx := newFaviconFixture(t)
116 fx.uc.converter = nil118 fx.uc.converter = nil
117 fx.blobs.original["example.com"] = []byte("original")119 fx.blobs.original["example.com"] = []byte("original")
118 fx.blobs.contentTypes["example.com"] = "image/png"120 fx.blobs.contentTypes["example.com"] = "image/png"
@@ -124,7 +126,7 @@ func TestFaviconResolveRepairOrphanWithoutConverterReturnsMiss(t *testing.T) {
124 }126 }
125 127
126 func TestFaviconResolveStaleSchedulesDedupedBackgroundRefresh(t *testing.T) {128 func TestFaviconResolveStaleSchedulesDedupedBackgroundRefresh(t *testing.T) {
127- fx := newFaviconFixture()129+ fx := newFaviconFixture(t)
128 fx.seed("example.com", []byte("old"), true)130 fx.seed("example.com", []byte("old"), true)
129 for i := 0; i < 2; i++ {131 for i := 0; i < 2; i++ {
130 got, err := fx.uc.Resolve(context.Background(), "https://example.com", 32, ResolveOptions{ScheduleBackgroundRefresh: true})132 got, err := fx.uc.Resolve(context.Background(), "https://example.com", 32, ResolveOptions{ScheduleBackgroundRefresh: true})
@@ -144,7 +146,7 @@ func TestFaviconResolveStaleSchedulesDedupedBackgroundRefresh(t *testing.T) {
144 }146 }
145 147
146 func TestFaviconResolveStaleExactRefreshIgnoresFreshParent(t *testing.T) {148 func TestFaviconResolveStaleExactRefreshIgnoresFreshParent(t *testing.T) {
147- fx := newFaviconFixture()149+ fx := newFaviconFixture(t)
148 fx.seed("docs.example.com", []byte("old-exact"), true)150 fx.seed("docs.example.com", []byte("old-exact"), true)
149 fx.seed("example.com", []byte("fresh-parent"), false)151 fx.seed("example.com", []byte("fresh-parent"), false)
150 got, err := fx.uc.Resolve(152 got, err := fx.uc.Resolve(
@@ -166,7 +168,7 @@ func TestFaviconResolveStaleExactRefreshIgnoresFreshParent(t *testing.T) {
166 }168 }
167 169
168 func TestFaviconObserveSameHashUpdatesMetadataOnly(t *testing.T) {170 func TestFaviconObserveSameHashUpdatesMetadataOnly(t *testing.T) {
169- fx := newFaviconFixture()171+ fx := newFaviconFixture(t)
170 fx.seedOriginal([]byte("same"))172 fx.seedOriginal([]byte("same"))
171 if _, err := fx.uc.Observe(context.Background(), "https://example.com", "https://cdn.test/icon.png", []byte("same"), favicon.SourceEngine, "image/png"); err != nil {173 if _, err := fx.uc.Observe(context.Background(), "https://example.com", "https://cdn.test/icon.png", []byte("same"), favicon.SourceEngine, "image/png"); err != nil {
172 t.Fatal(err)174 t.Fatal(err)
@@ -180,7 +182,7 @@ func TestFaviconObserveSameHashUpdatesMetadataOnly(t *testing.T) {
180 }182 }
181 183
182 func TestFaviconObserveChangedHashRegeneratesAndInvalidates(t *testing.T) {184 func TestFaviconObserveChangedHashRegeneratesAndInvalidates(t *testing.T) {
183- fx := newFaviconFixture()185+ fx := newFaviconFixture(t)
184 fx.seedOriginal([]byte("old"))186 fx.seedOriginal([]byte("old"))
185 if _, err := fx.uc.Observe(context.Background(), "https://example.com", "https://cdn.test/icon.png", []byte("new"), favicon.SourceEngine, "image/png"); err != nil {187 if _, err := fx.uc.Observe(context.Background(), "https://example.com", "https://cdn.test/icon.png", []byte("new"), favicon.SourceEngine, "image/png"); err != nil {
186 t.Fatal(err)188 t.Fatal(err)
@@ -197,7 +199,7 @@ func TestFaviconObserveChangedHashRegeneratesAndInvalidates(t *testing.T) {
197 }199 }
198 200
199 func TestFaviconObserveWithoutConverterStoresOriginalOnly(t *testing.T) {201 func TestFaviconObserveWithoutConverterStoresOriginalOnly(t *testing.T) {
200- fx := newFaviconFixture()202+ fx := newFaviconFixture(t)
201 fx.uc.converter = nil203 fx.uc.converter = nil
202 if _, err := fx.uc.Observe(context.Background(), "https://example.com", "https://example.com/icon.png", []byte("raw"), favicon.SourceEngine, "image/png"); err != nil {204 if _, err := fx.uc.Observe(context.Background(), "https://example.com", "https://example.com/icon.png", []byte("raw"), favicon.SourceEngine, "image/png"); err != nil {
203 t.Fatal(err)205 t.Fatal(err)
@@ -211,7 +213,7 @@ func TestFaviconObserveWithoutConverterStoresOriginalOnly(t *testing.T) {
211 }213 }
212 214
213 func TestFaviconResolveMissingFetchesOnlyWhenBlockingAllowed(t *testing.T) {215 func TestFaviconResolveMissingFetchesOnlyWhenBlockingAllowed(t *testing.T) {
214- fx := newFaviconFixture()216+ fx := newFaviconFixture(t)
215 if _, err := fx.uc.Resolve(context.Background(), "https://example.com", 32, ResolveOptions{}); !errors.Is(err, ErrFaviconMiss) {217 if _, err := fx.uc.Resolve(context.Background(), "https://example.com", 32, ResolveOptions{}); !errors.Is(err, ErrFaviconMiss) {
216 t.Fatalf("default resolve err=%v", err)218 t.Fatalf("default resolve err=%v", err)
217 }219 }
@@ -228,7 +230,7 @@ func TestFaviconResolveMissingFetchesOnlyWhenBlockingAllowed(t *testing.T) {
228 }230 }
229 231
230 func TestFaviconRefreshIfStaleUsesParentFallbackBeforeFetching(t *testing.T) {232 func TestFaviconRefreshIfStaleUsesParentFallbackBeforeFetching(t *testing.T) {
231- fx := newFaviconFixture()233+ fx := newFaviconFixture(t)
232 fx.seed("example.com", []byte("parent"), false)234 fx.seed("example.com", []byte("parent"), false)
233 if err := fx.uc.RefreshIfStale(context.Background(), "https://docs.example.com/page"); err != nil {235 if err := fx.uc.RefreshIfStale(context.Background(), "https://docs.example.com/page"); err != nil {
234 t.Fatal(err)236 t.Fatal(err)
@@ -239,7 +241,7 @@ func TestFaviconRefreshIfStaleUsesParentFallbackBeforeFetching(t *testing.T) {
239 }241 }
240 242
241 func TestFaviconUseCasePropagatesRepositoryErrors(t *testing.T) {243 func TestFaviconUseCasePropagatesRepositoryErrors(t *testing.T) {
242- fx := newFaviconFixture()244+ fx := newFaviconFixture(t)
243 fx.repo.err = errors.New("database unavailable")245 fx.repo.err = errors.New("database unavailable")
244 if _, err := fx.uc.Resolve(context.Background(), "https://example.com", 32, ResolveOptions{}); !errors.Is(err, fx.repo.err) {246 if _, err := fx.uc.Resolve(context.Background(), "https://example.com", 32, ResolveOptions{}); !errors.Is(err, fx.repo.err) {
245 t.Fatalf("Resolve err=%v, want %v", err, fx.repo.err)247 t.Fatalf("Resolve err=%v, want %v", err, fx.repo.err)
@@ -253,7 +255,7 @@ func TestFaviconUseCasePropagatesRepositoryErrors(t *testing.T) {
253 }255 }
254 256
255 func TestFaviconResolvePropagatesBlobErrors(t *testing.T) {257 func TestFaviconResolvePropagatesBlobErrors(t *testing.T) {
256- fx := newFaviconFixture()258+ fx := newFaviconFixture(t)
257 fx.seed("example.com", []byte("cached"), false)259 fx.seed("example.com", []byte("cached"), false)
258 fx.blobs.err = errors.New("disk unavailable")260 fx.blobs.err = errors.New("disk unavailable")
259 if _, err := fx.uc.Resolve(context.Background(), "https://example.com", 32, ResolveOptions{}); !errors.Is(err, fx.blobs.err) {261 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) {
262 }264 }
263 265
264 func TestFaviconResolveBlockingRefreshesWhenMetadataFreshButBlobMissing(t *testing.T) {266 func TestFaviconResolveBlockingRefreshesWhenMetadataFreshButBlobMissing(t *testing.T) {
265- fx := newFaviconFixture()267+ fx := newFaviconFixture(t)
266 fx.repo.byKey["example.com"] = &favicon.Metadata{Key: "example.com", PageURL: "https://example.com", ContentHash: favicon.Hash([]byte("old")), LastCheckedAt: fx.now}268 fx.repo.byKey["example.com"] = &favicon.Metadata{Key: "example.com", PageURL: "https://example.com", ContentHash: favicon.Hash([]byte("old")), LastCheckedAt: fx.now}
267 got, err := fx.uc.Resolve(context.Background(), "https://example.com", 32, ResolveOptions{AllowBlockingRefresh: true})269 got, err := fx.uc.Resolve(context.Background(), "https://example.com", 32, ResolveOptions{AllowBlockingRefresh: true})
268 if err != nil {270 if err != nil {
@@ -274,7 +276,7 @@ func TestFaviconResolveBlockingRefreshesWhenMetadataFreshButBlobMissing(t *testi
274 }276 }
275 277
276 func TestFaviconResolveBlockingPropagatesFetchErrors(t *testing.T) {278 func TestFaviconResolveBlockingPropagatesFetchErrors(t *testing.T) {
277- fx := newFaviconFixture()279+ fx := newFaviconFixture(t)
278 fx.fetcher.err = errors.New("network unavailable")280 fx.fetcher.err = errors.New("network unavailable")
279 if _, err := fx.uc.Resolve(context.Background(), "https://example.com", 32, ResolveOptions{AllowBlockingRefresh: true}); !errors.Is(err, fx.fetcher.err) {281 if _, err := fx.uc.Resolve(context.Background(), "https://example.com", 32, ResolveOptions{AllowBlockingRefresh: true}); !errors.Is(err, fx.fetcher.err) {
280 t.Fatalf("Resolve err=%v, want %v", err, fx.fetcher.err)282 t.Fatalf("Resolve err=%v, want %v", err, fx.fetcher.err)
@@ -282,7 +284,7 @@ func TestFaviconResolveBlockingPropagatesFetchErrors(t *testing.T) {
282 }284 }
283 285
284 func TestFaviconRefreshFromIconURLsUsesPageURLKey(t *testing.T) {286 func TestFaviconRefreshFromIconURLsUsesPageURLKey(t *testing.T) {
285- fx := newFaviconFixture()287+ fx := newFaviconFixture(t)
286 if err := fx.uc.RefreshFromIconURLs(context.Background(), "https://docs.example.com/page", []string{"https://cdn.example.net/icon.png"}); err != nil {288 if err := fx.uc.RefreshFromIconURLs(context.Background(), "https://docs.example.com/page", []string{"https://cdn.example.net/icon.png"}); err != nil {
287 t.Fatal(err)289 t.Fatal(err)
288 }290 }
@@ -295,8 +297,8 @@ func TestFaviconRefreshFromIconURLsUsesPageURLKey(t *testing.T) {
295 }297 }
296 298
297 func TestFaviconRefreshStoresDuckDuckGoFallbackUnderHostKey(t *testing.T) {299 func TestFaviconRefreshStoresDuckDuckGoFallbackUnderHostKey(t *testing.T) {
298- fx := newFaviconFixture()300+ fx := newFaviconFixture(t)
299- fx.fetcher.responses = map[string]fakeFetchedIcon{301+ fx.fetcher.responses = map[string]faviconFetchResponse{
300 "https://example.com/favicon.ico": {302 "https://example.com/favicon.ico": {
301 bytes: []byte("host-fallback"),303 bytes: []byte("host-fallback"),
302 contentType: "image/png",304 contentType: "image/png",
@@ -316,8 +318,8 @@ func TestFaviconRefreshStoresDuckDuckGoFallbackUnderHostKey(t *testing.T) {
316 }318 }
317 319
318 func TestFaviconRefreshRejectsResolvedKeyOutsideCandidateHierarchy(t *testing.T) {320 func TestFaviconRefreshRejectsResolvedKeyOutsideCandidateHierarchy(t *testing.T) {
319- fx := newFaviconFixture()321+ fx := newFaviconFixture(t)
320- fx.fetcher.responses = map[string]fakeFetchedIcon{322+ fx.fetcher.responses = map[string]faviconFetchResponse{
321 "https://example.com/favicon.ico": {323 "https://example.com/favicon.ico": {
322 bytes: []byte("bad-fallback"),324 bytes: []byte("bad-fallback"),
323 contentType: "image/png",325 contentType: "image/png",
@@ -335,8 +337,8 @@ func TestFaviconRefreshRejectsResolvedKeyOutsideCandidateHierarchy(t *testing.T)
335 }337 }
336 338
337 func TestFaviconRefreshRejectsFetcherPageURLOutsideRequestedHierarchy(t *testing.T) {339 func TestFaviconRefreshRejectsFetcherPageURLOutsideRequestedHierarchy(t *testing.T) {
338- fx := newFaviconFixture()340+ fx := newFaviconFixture(t)
339- fx.fetcher.responses = map[string]fakeFetchedIcon{341+ fx.fetcher.responses = map[string]faviconFetchResponse{
340 "https://example.com/favicon.ico": {342 "https://example.com/favicon.ico": {
341 bytes: []byte("bad-page-url"),343 bytes: []byte("bad-page-url"),
342 contentType: "image/png",344 contentType: "image/png",
@@ -355,8 +357,8 @@ func TestFaviconRefreshRejectsFetcherPageURLOutsideRequestedHierarchy(t *testing
355 }357 }
356 358
357 func TestFaviconRefreshFromIconURLsSkipsUnsupportedCandidates(t *testing.T) {359 func TestFaviconRefreshFromIconURLsSkipsUnsupportedCandidates(t *testing.T) {
358- fx := newFaviconFixture()360+ fx := newFaviconFixture(t)
359- fx.fetcher.responses = map[string]fakeFetchedIcon{361+ fx.fetcher.responses = map[string]faviconFetchResponse{
360 "https://cdn.example.net/icon.svg": {bytes: []byte("svg"), contentType: "image/svg+xml"},362 "https://cdn.example.net/icon.svg": {bytes: []byte("svg"), contentType: "image/svg+xml"},
361 "https://cdn.example.net/icon.png": {bytes: []byte("png"), contentType: "image/png"},363 "https://cdn.example.net/icon.png": {bytes: []byte("png"), contentType: "image/png"},
362 }364 }
@@ -385,7 +387,7 @@ func TestFaviconRefreshFromIconURLsSkipsUnsupportedCandidates(t *testing.T) {
385 }387 }
386 388
387 func TestFaviconObserveConversionErrorDoesNotMutateExistingAssets(t *testing.T) {389 func TestFaviconObserveConversionErrorDoesNotMutateExistingAssets(t *testing.T) {
388- fx := newFaviconFixture()390+ fx := newFaviconFixture(t)
389 fx.seedOriginal([]byte("old"))391 fx.seedOriginal([]byte("old"))
390 fx.blobs.png["example.com"] = []byte("old-png")392 fx.blobs.png["example.com"] = []byte("old-png")
391 fx.blobs.sized["example.com"] = map[int][]byte{32: []byte("old-32")}393 fx.blobs.sized["example.com"] = map[int][]byte{32: []byte("old-32")}
@@ -403,7 +405,7 @@ func TestFaviconObserveConversionErrorDoesNotMutateExistingAssets(t *testing.T)
403 }405 }
404 406
405 func TestFaviconEnsureSizedPropagatesReadSizedErrors(t *testing.T) {407 func TestFaviconEnsureSizedPropagatesReadSizedErrors(t *testing.T) {
406- fx := newFaviconFixture()408+ fx := newFaviconFixture(t)
407 fx.seedOriginal([]byte("old"))409 fx.seedOriginal([]byte("old"))
408 fx.blobs.readSizedErr = errors.New("sized store unavailable")410 fx.blobs.readSizedErr = errors.New("sized store unavailable")
409 if err := fx.uc.EnsureSized(context.Background(), "example.com", 32); !errors.Is(err, fx.blobs.readSizedErr) {411 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) {
415 }417 }
416 418
417 type faviconFixture struct {419 type faviconFixture struct {
418- repo *fakeFaviconRepo420+ repo *faviconRepoState
419- blobs *fakeBlobStore421+ blobs *faviconBlobStoreState
420- converter *fakeConverter422+ converter *faviconConverterState
421- scheduler *fakeScheduler423+ scheduler *faviconSchedulerState
422- invalidators *fakeInvalidators424+ invalidators *faviconInvalidatorsState
423- fetcher *fakeFetcher425+ fetcher *faviconFetcherState
424 uc *FaviconUseCase426 uc *FaviconUseCase
425 now time.Time427 now time.Time
426 }428 }
@@ -429,29 +431,98 @@ type faviconTestContextKey string
429 431
430 const faviconFixtureBackgroundKey faviconTestContextKey = "bg"432 const faviconFixtureBackgroundKey faviconTestContextKey = "bg"
431 433
432-func newFaviconFixture() *faviconFixture {434+func newFaviconFixture(t *testing.T) *faviconFixture {
435+ t.Helper()
436+
433 now := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)437 now := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
434 fx := &faviconFixture{438 fx := &faviconFixture{
435- repo: newFakeRepo(),439+ repo: newFaviconRepoState(),
436- blobs: newFakeBlobStore(),440+ blobs: newFaviconBlobStoreState(),
437- converter: &fakeConverter{},441+ converter: &faviconConverterState{},
438- scheduler: &fakeScheduler{seen: map[favicon.Key]bool{}},442+ scheduler: &faviconSchedulerState{seen: map[favicon.Key]bool{}},
439- invalidators: &fakeInvalidators{},443+ invalidators: &faviconInvalidatorsState{},
440- fetcher: &fakeFetcher{},444+ fetcher: &faviconFetcherState{},
441 now: now,445 now: now,
442 }446 }
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+
443 fx.uc = NewFaviconUseCase(FaviconDeps{455 fx.uc = NewFaviconUseCase(FaviconDeps{
444- Repository: fx.repo,456+ Repository: repo,
445- BlobStore: fx.blobs,457+ BlobStore: blobs,
446- Converter: fx.converter,458+ Converter: converter,
447- Scheduler: fx.scheduler,459+ Scheduler: scheduler,
448- Invalidators: fx.invalidators,460+ Invalidators: invalidators,
449- Fetcher: fx.fetcher,461+ Fetcher: fetcher,
450 Now: func() time.Time { return fx.now },462 Now: func() time.Time { return fx.now },
451 Background: context.WithValue(context.Background(), faviconFixtureBackgroundKey, true),463 Background: context.WithValue(context.Background(), faviconFixtureBackgroundKey, true),
452 })464 })
453 return fx465 return fx
454 }466 }
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+
455 func (f *faviconFixture) seed(key string, sized []byte, stale bool) {526 func (f *faviconFixture) seed(key string, sized []byte, stale bool) {
456 checked := f.now527 checked := f.now
457 if stale {528 if stale {
@@ -460,6 +531,7 @@ func (f *faviconFixture) seed(key string, sized []byte, stale bool) {
460 f.repo.byKey[favicon.Key(key)] = &favicon.Metadata{Key: favicon.Key(key), PageURL: "https://" + key, ContentHash: favicon.Hash([]byte("orig")), LastCheckedAt: checked}531 f.repo.byKey[favicon.Key(key)] = &favicon.Metadata{Key: favicon.Key(key), PageURL: "https://" + key, ContentHash: favicon.Hash([]byte("orig")), LastCheckedAt: checked}
461 f.blobs.sized[favicon.Key(key)] = map[int][]byte{32: sized}532 f.blobs.sized[favicon.Key(key)] = map[int][]byte{32: sized}
462 }533 }
534+
463 func (f *faviconFixture) seedOriginal(b []byte) {535 func (f *faviconFixture) seedOriginal(b []byte) {
464 const contentType = "image/png"536 const contentType = "image/png"
465 key := favicon.Key("example.com")537 key := favicon.Key("example.com")
@@ -474,15 +546,16 @@ func (f *faviconFixture) seedOriginal(b []byte) {
474 f.blobs.contentTypes[key] = contentType546 f.blobs.contentTypes[key] = contentType
475 }547 }
476 548
477-type fakeFaviconRepo struct {549+type faviconRepoState struct {
478 byKey map[favicon.Key]*favicon.Metadata550 byKey map[favicon.Key]*favicon.Metadata
479 err error551 err error
480 }552 }
481 553
482-func newFakeRepo() *fakeFaviconRepo {554+func newFaviconRepoState() *faviconRepoState {
483- return &fakeFaviconRepo{byKey: map[favicon.Key]*favicon.Metadata{}}555+ return &faviconRepoState{byKey: map[favicon.Key]*favicon.Metadata{}}
484 }556 }
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) {
486 if r.err != nil {559 if r.err != nil {
487 return nil, r.err560 return nil, r.err
488 }561 }
@@ -492,18 +565,20 @@ func (r *fakeFaviconRepo) Get(_ context.Context, key favicon.Key) (*favicon.Meta
492 }565 }
493 return nil, nil566 return nil, nil
494 }567 }
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) {
496 if r.err != nil {570 if r.err != nil {
497 return nil, r.err571 return nil, r.err
498 }572 }
499 for _, k := range keys {573 for _, k := range keys {
500- if m, _ := r.Get(ctx, k); m != nil {574+ if m, _ := r.get(ctx, k); m != nil {
501 return m, nil575 return m, nil
502 }576 }
503 }577 }
504 return nil, nil578 return nil, nil
505 }579 }
506-func (r *fakeFaviconRepo) Upsert(_ context.Context, meta favicon.Metadata) error {580+
581+func (r *faviconRepoState) upsert(_ context.Context, meta favicon.Metadata) error {
507 if r.err != nil {582 if r.err != nil {
508 return r.err583 return r.err
509 }584 }
@@ -511,7 +586,8 @@ func (r *fakeFaviconRepo) Upsert(_ context.Context, meta favicon.Metadata) error
511 r.byKey[meta.Key] = &cp586 r.byKey[meta.Key] = &cp
512 return nil587 return nil
513 }588 }
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 {
515 if r.err != nil {591 if r.err != nil {
516 return r.err592 return r.err
517 }593 }
@@ -519,7 +595,8 @@ func (r *fakeFaviconRepo) UpdateLastChecked(_ context.Context, key favicon.Key,
519 r.byKey[key].LastCheckedAt = checkedAt595 r.byKey[key].LastCheckedAt = checkedAt
520 return nil596 return nil
521 }597 }
522-func (r *fakeFaviconRepo) Delete(_ context.Context, key favicon.Key) error {598+
599+func (r *faviconRepoState) delete(_ context.Context, key favicon.Key) error {
523 if r.err != nil {600 if r.err != nil {
524 return r.err601 return r.err
525 }602 }
@@ -527,7 +604,7 @@ func (r *fakeFaviconRepo) Delete(_ context.Context, key favicon.Key) error {
527 return nil604 return nil
528 }605 }
529 606
530-type fakeBlobStore struct {607+type faviconBlobStoreState struct {
531 original, png map[favicon.Key][]byte608 original, png map[favicon.Key][]byte
532 sized map[favicon.Key]map[int][]byte609 sized map[favicon.Key]map[int][]byte
533 contentTypes map[favicon.Key]string610 contentTypes map[favicon.Key]string
@@ -536,10 +613,16 @@ type fakeBlobStore struct {
536 readSizedErr error613 readSizedErr error
537 }614 }
538 615
539-func newFakeBlobStore() *fakeBlobStore {616+func newFaviconBlobStoreState() *faviconBlobStoreState {
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{}}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+ }
541 }623 }
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) {
543 if b.err != nil {626 if b.err != nil {
544 return nil, "", b.err627 return nil, "", b.err
545 }628 }
@@ -549,7 +632,8 @@ func (b *fakeBlobStore) ReadOriginal(_ context.Context, key favicon.Key) ([]byte
549 }632 }
550 return v, b.contentTypes[key], nil633 return v, b.contentTypes[key], nil
551 }634 }
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 {
553 if b.err != nil {637 if b.err != nil {
554 return b.err638 return b.err
555 }639 }
@@ -558,7 +642,8 @@ func (b *fakeBlobStore) WriteOriginal(_ context.Context, key favicon.Key, data [
558 b.contentTypes[key] = ct642 b.contentTypes[key] = ct
559 return nil643 return nil
560 }644 }
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) {
562 if b.err != nil {647 if b.err != nil {
563 return nil, "", b.err648 return nil, "", b.err
564 }649 }
@@ -568,14 +653,16 @@ func (b *fakeBlobStore) ReadPNG(_ context.Context, key favicon.Key) ([]byte, str
568 }653 }
569 return v, "image/png", nil654 return v, "image/png", nil
570 }655 }
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 {
572 if b.err != nil {658 if b.err != nil {
573 return b.err659 return b.err
574 }660 }
575 b.png[key] = data661 b.png[key] = data
576 return nil662 return nil
577 }663 }
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) {
579 if b.err != nil {666 if b.err != nil {
580 return nil, "", b.err667 return nil, "", b.err
581 }668 }
@@ -587,7 +674,8 @@ func (b *fakeBlobStore) ReadSizedPNG(_ context.Context, key favicon.Key, size in
587 }674 }
588 return b.sized[key][size], "image/png", nil675 return b.sized[key][size], "image/png", nil
589 }676 }
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 {
591 if b.err != nil {679 if b.err != nil {
592 return b.err680 return b.err
593 }681 }
@@ -597,7 +685,8 @@ func (b *fakeBlobStore) WriteSizedPNG(_ context.Context, key favicon.Key, size i
597 b.sized[key][size] = data685 b.sized[key][size] = data
598 return nil686 return nil
599 }687 }
600-func (b *fakeBlobStore) RemoveDerived(_ context.Context, key favicon.Key) error {688+
689+func (b *faviconBlobStoreState) removeDerivedForKey(_ context.Context, key favicon.Key) error {
601 if b.err != nil {690 if b.err != nil {
602 return b.err691 return b.err
603 }692 }
@@ -607,13 +696,13 @@ func (b *fakeBlobStore) RemoveDerived(_ context.Context, key favicon.Key) error
607 return nil696 return nil
608 }697 }
609 698
610-type fakeConverter struct {699+type faviconConverterState struct {
611 calls int700 calls int
612 err error701 err error
613 errByContentType map[string]error702 errByContentType map[string]error
614 }703 }
615 704
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) {
617 c.calls++706 c.calls++
618 if c.err != nil {707 if c.err != nil {
619 return nil, c.err708 return nil, c.err
@@ -628,14 +717,14 @@ func (c *fakeConverter) Convert(_ context.Context, original []byte, contentType
628 return out, nil717 return out, nil
629 }718 }
630 719
631-type fakeScheduler struct {720+type faviconSchedulerState struct {
632 seen map[favicon.Key]bool721 seen map[favicon.Key]bool
633 works map[favicon.Key]func(context.Context)722 works map[favicon.Key]func(context.Context)
634 schedules int723 schedules int
635 usedCallerContext bool724 usedCallerContext bool
636 }725 }
637 726
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 {
639 if ctx.Value(faviconFixtureBackgroundKey) != true {728 if ctx.Value(faviconFixtureBackgroundKey) != true {
640 s.usedCallerContext = true729 s.usedCallerContext = true
641 }730 }
@@ -651,33 +740,33 @@ func (s *fakeScheduler) Schedule(ctx context.Context, key favicon.Key, work func
651 return true740 return true
652 }741 }
653 742
654-func (s *fakeScheduler) run(key favicon.Key) {743+func (s *faviconSchedulerState) run(key favicon.Key) {
655 if work := s.works[key]; work != nil {744 if work := s.works[key]; work != nil {
656 work(context.Background())745 work(context.Background())
657 }746 }
658 }747 }
659 748
660-type fakeInvalidators struct{ calls int }749+type faviconInvalidatorsState struct{ calls int }
661 750
662-func (i *fakeInvalidators) InvalidateAll(_ context.Context, _ favicon.Key) error {751+func (i *faviconInvalidatorsState) invalidateAll(_ context.Context, _ favicon.Key) error {
663 i.calls++752 i.calls++
664 return nil753 return nil
665 }754 }
666 755
667-type fakeFetchedIcon struct {756+type faviconFetchResponse struct {
668 bytes []byte757 bytes []byte
669 contentType string758 contentType string
670 pageURL string759 pageURL string
671 resolvedKey favicon.Key760 resolvedKey favicon.Key
672 }761 }
673 762
674-type fakeFetcher struct {763+type faviconFetcherState struct {
675 calls int764 calls int
676 err error765 err error
677- responses map[string]fakeFetchedIcon766+ responses map[string]faviconFetchResponse
678 }767 }
679 768
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) {
681 f.calls++770 f.calls++
682 if f.err != nil {771 if f.err != nil {
683 return nil, f.err772 return nil, f.err
@@ -688,11 +777,18 @@ func (f *fakeFetcher) Fetch(_ context.Context, req appport.FaviconFetchRequest)
688 }777 }
689 response := f.responses[iconURL]778 response := f.responses[iconURL]
690 if response.bytes == nil {779 if response.bytes == nil {
691- response = fakeFetchedIcon{bytes: []byte("fetched"), contentType: "image/png"}780+ response = faviconFetchResponse{bytes: []byte("fetched"), contentType: "image/png"}
692 }781 }
693 pageURL := req.PageURL782 pageURL := req.PageURL
694 if response.pageURL != "" {783 if response.pageURL != "" {
695 pageURL = response.pageURL784 pageURL = response.pageURL
696 }785 }
697- return &appport.FaviconFetchedIcon{PageURL: pageURL, IconURL: iconURL, ResolvedKey: response.resolvedKey, Bytes: response.bytes, Source: favicon.SourceDuckDuckGo, ContentType: response.contentType}, nil786+ 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
698 }794 }
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 (
5 "testing"5 "testing"
6 6
7 "github.com/bnema/dumber/internal/application/port"7 "github.com/bnema/dumber/internal/application/port"
8+ bootstrapmocks "github.com/bnema/dumber/internal/bootstrap/mocks"
8 "github.com/bnema/dumber/internal/domain/entity"9 "github.com/bnema/dumber/internal/domain/entity"
9 "github.com/bnema/dumber/internal/infrastructure/config"10 "github.com/bnema/dumber/internal/infrastructure/config"
11+ "github.com/stretchr/testify/mock"
10 )12 )
11 13
12-type fakeConfigManager struct {14+type configManagerState struct {
13 current *config.Config15 current *config.Config
14 watchCalls int16 watchCalls int
15 callbacks []func(*config.Config)17 callbacks []func(*config.Config)
16 }18 }
17 19
18-func (f *fakeConfigManager) Watch() error {20+func newMockConfigManager(t *testing.T, state *configManagerState) *bootstrapmocks.MockConfigChangeManager {
19- f.watchCalls++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++
20 return nil32 return nil
21 }33 }
22 34
23-func (f *fakeConfigManager) OnConfigChange(callback func(*config.Config)) {35+func (s *configManagerState) onConfigChange(callback func(*config.Config)) {
24- f.callbacks = append(f.callbacks, callback)36+ s.callbacks = append(s.callbacks, callback)
25 }37 }
26 38
27-func (f *fakeConfigManager) Get() *config.Config {39+func (s *configManagerState) get() *config.Config {
28- return f.current40+ return s.current
29 }41 }
30 42
31-func (f *fakeConfigManager) emit(next *config.Config) {43+func (s *configManagerState) emit(next *config.Config) {
32- f.current = next44+ s.current = next
33- for _, callback := range f.callbacks {45+ for _, callback := range s.callbacks {
34 callback(next)46 callback(next)
35 }47 }
36 }48 }
@@ -71,8 +83,8 @@ func TestEngineSettingsPayloadFromConfigMapsRuntimeFields(t *testing.T) {
71 func TestRuntimeConfigProviderUpdatesSnapshotBeforeCallback(t *testing.T) {83 func TestRuntimeConfigProviderUpdatesSnapshotBeforeCallback(t *testing.T) {
72 initial := config.DefaultConfig()84 initial := config.DefaultConfig()
73 initial.DefaultUIScale = 185 initial.DefaultUIScale = 1
74- manager := &fakeConfigManager{}86+ manager := &configManagerState{}
75- provider := NewRuntimeConfigProvider(initial, manager)87+ provider := NewRuntimeConfigProvider(initial, newMockConfigManager(t, manager))
76 88
77 var seen port.RuntimeConfigSnapshot89 var seen port.RuntimeConfigSnapshot
78 provider.OnChange(func(snapshot port.RuntimeConfigSnapshot) {90 provider.OnChange(func(snapshot port.RuntimeConfigSnapshot) {
@@ -94,8 +106,8 @@ func TestRuntimeConfigProviderUpdatesSnapshotBeforeCallback(t *testing.T) {
94 func TestRuntimeConfigProviderCurrentReflectsManagerGetWithoutSubscription(t *testing.T) {106 func TestRuntimeConfigProviderCurrentReflectsManagerGetWithoutSubscription(t *testing.T) {
95 initial := config.DefaultConfig()107 initial := config.DefaultConfig()
96 initial.DefaultUIScale = 1108 initial.DefaultUIScale = 1
97- manager := &fakeConfigManager{current: initial}109+ manager := &configManagerState{current: initial}
98- provider := NewRuntimeConfigProvider(initial, manager)110+ provider := NewRuntimeConfigProvider(initial, newMockConfigManager(t, manager))
99 111
100 next := config.DefaultConfig()112 next := config.DefaultConfig()
101 next.DefaultUIScale = 1.8113 next.DefaultUIScale = 1.8
@@ -109,8 +121,8 @@ func TestRuntimeConfigProviderCurrentReflectsManagerGetWithoutSubscription(t *te
109 func TestRuntimeConfigProviderOnChangeCallbackCanCallCurrent(t *testing.T) {121 func TestRuntimeConfigProviderOnChangeCallbackCanCallCurrent(t *testing.T) {
110 initial := config.DefaultConfig()122 initial := config.DefaultConfig()
111 initial.DefaultUIScale = 1123 initial.DefaultUIScale = 1
112- manager := &fakeConfigManager{current: initial}124+ manager := &configManagerState{current: initial}
113- provider := NewRuntimeConfigProvider(initial, manager)125+ provider := NewRuntimeConfigProvider(initial, newMockConfigManager(t, manager))
114 126
115 completed := false127 completed := false
116 var seen port.RuntimeConfigSnapshot128 var seen port.RuntimeConfigSnapshot
@@ -132,8 +144,8 @@ func TestRuntimeConfigProviderOnChangeCallbackCanCallCurrent(t *testing.T) {
132 }144 }
133 145
134 func TestRuntimeConfigProviderOnChangeNilCallbackDoesNotRegister(t *testing.T) {146 func TestRuntimeConfigProviderOnChangeNilCallbackDoesNotRegister(t *testing.T) {
135- manager := &fakeConfigManager{current: config.DefaultConfig()}147+ manager := &configManagerState{current: config.DefaultConfig()}
136- provider := NewRuntimeConfigProvider(config.DefaultConfig(), manager)148+ provider := NewRuntimeConfigProvider(config.DefaultConfig(), newMockConfigManager(t, manager))
137 149
138 provider.OnChange(nil)150 provider.OnChange(nil)
139 151
@@ -143,8 +155,8 @@ func TestRuntimeConfigProviderOnChangeNilCallbackDoesNotRegister(t *testing.T) {
143 }155 }
144 156
145 func TestRuntimeConfigProviderWatchDelegatesToManager(t *testing.T) {157 func TestRuntimeConfigProviderWatchDelegatesToManager(t *testing.T) {
146- manager := &fakeConfigManager{}158+ manager := &configManagerState{}
147- provider := NewRuntimeConfigProvider(config.DefaultConfig(), manager)159+ provider := NewRuntimeConfigProvider(config.DefaultConfig(), newMockConfigManager(t, manager))
148 160
149 if err := provider.Watch(); err != nil {161 if err := provider.Watch(); err != nil {
150 t.Fatalf("Watch returned error: %v", err)162 t.Fatalf("Watch returned error: %v", err)
@@ -159,7 +171,7 @@ func TestRuntimeConfigProviderCurrentReturnsMapClone(t *testing.T) {
159 cfg.SearchShortcuts = map[string]config.SearchShortcut{171 cfg.SearchShortcuts = map[string]config.SearchShortcut{
160 "gh": {URL: "https://github.com/search?q=%s", Description: "GitHub"},172 "gh": {URL: "https://github.com/search?q=%s", Description: "GitHub"},
161 }173 }
162- provider := NewRuntimeConfigProvider(cfg, &fakeConfigManager{current: cfg})174+ provider := NewRuntimeConfigProvider(cfg, newMockConfigManager(t, &configManagerState{current: cfg}))
163 175
164 first := provider.Current()176 first := provider.Current()
165 first.UI.SearchShortcuts["gh"] = port.RuntimeSearchShortcut{URL: "mutated"}177 first.UI.SearchShortcuts["gh"] = port.RuntimeSearchShortcut{URL: "mutated"}
@@ -172,7 +184,7 @@ func TestRuntimeConfigProviderCurrentReturnsMapClone(t *testing.T) {
172 184
173 func TestRuntimeConfigProviderCurrentReturnsNestedConfigClone(t *testing.T) {185 func TestRuntimeConfigProviderCurrentReturnsNestedConfigClone(t *testing.T) {
174 cfg := runtimeConfigWithNestedMutableFields()186 cfg := runtimeConfigWithNestedMutableFields()
175- provider := NewRuntimeConfigProvider(cfg, &fakeConfigManager{current: cfg})187+ provider := NewRuntimeConfigProvider(cfg, newMockConfigManager(t, &configManagerState{current: cfg}))
176 188
177 first := provider.Current()189 first := provider.Current()
178 mutateNestedRuntimeConfigSnapshot(first)190 mutateNestedRuntimeConfigSnapshot(first)
@@ -182,8 +194,8 @@ func TestRuntimeConfigProviderCurrentReturnsNestedConfigClone(t *testing.T) {
182 }194 }
183 195
184 func TestRuntimeConfigProviderOnChangePassesNestedConfigClone(t *testing.T) {196 func TestRuntimeConfigProviderOnChangePassesNestedConfigClone(t *testing.T) {
185- manager := &fakeConfigManager{}197+ manager := &configManagerState{}
186- provider := NewRuntimeConfigProvider(config.DefaultConfig(), manager)198+ provider := NewRuntimeConfigProvider(config.DefaultConfig(), newMockConfigManager(t, manager))
187 199
188 provider.OnChange(func(snapshot port.RuntimeConfigSnapshot) {200 provider.OnChange(func(snapshot port.RuntimeConfigSnapshot) {
189 mutateNestedRuntimeConfigSnapshot(snapshot)201 mutateNestedRuntimeConfigSnapshot(snapshot)
@@ -246,7 +258,7 @@ func mutateNestedRuntimeConfigSnapshot(snapshot port.RuntimeConfigSnapshot) {
246 snapshot.UI.Session.SessionMode.Actions["added-session"] = entity.ActionBinding{Keys: []string{"added"}}258 snapshot.UI.Session.SessionMode.Actions["added-session"] = entity.ActionBinding{Keys: []string{"added"}}
247 }259 }
248 260
249-func mutateActionBinding(actions map[string]entity.ActionBinding, action string, key string) {261+func mutateActionBinding(actions map[string]entity.ActionBinding, action, key string) {
250 binding := actions[action]262 binding := actions[action]
251 binding.Keys[0] = key263 binding.Keys[0] = key
252 actions[action] = binding264 actions[action] = binding
@@ -279,7 +291,7 @@ func assertNestedRuntimeConfigSnapshotUnchanged(t *testing.T, snapshot port.Runt
279 assertMapEntryAbsent(t, snapshot.UI.Session.SessionMode.Actions, "added-session")291 assertMapEntryAbsent(t, snapshot.UI.Session.SessionMode.Actions, "added-session")
280 }292 }
281 293
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) {
283 t.Helper()295 t.Helper()
284 296
285 binding, ok := actions[action]297 binding, ok := actions[action]
M internal/cli/cmd/logs_test.go +17 -15
@@ -8,23 +8,13 @@ import (
8 "testing"8 "testing"
9 "time"9 "time"
10 10
11+ cmdmocks "github.com/bnema/dumber/internal/cli/cmd/mocks"
11 "github.com/bnema/dumber/internal/domain/entity"12 "github.com/bnema/dumber/internal/domain/entity"
12 "github.com/bnema/dumber/internal/logging"13 "github.com/bnema/dumber/internal/logging"
14+ "github.com/stretchr/testify/mock"
13 "github.com/stretchr/testify/require"15 "github.com/stretchr/testify/require"
14 )16 )
15 17
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-
28 func TestGetSessionsMerged_DBAndFS(t *testing.T) {18 func TestGetSessionsMerged_DBAndFS(t *testing.T) {
29 logDir := t.TempDir()19 logDir := t.TempDir()
30 20
@@ -34,7 +24,11 @@ func TestGetSessionsMerged_DBAndFS(t *testing.T) {
34 require.NoError(t, os.WriteFile(filepath.Join(logDir, logging.SessionFilename(string(s1.ID))), []byte("hi\n"), 0o600))24 require.NoError(t, os.WriteFile(filepath.Join(logDir, logging.SessionFilename(string(s1.ID))), []byte("hi\n"), 0o600))
35 require.NoError(t, os.WriteFile(filepath.Join(logDir, logging.SessionFilename("legacy_only")), []byte("legacy\n"), 0o600))25 require.NoError(t, os.WriteFile(filepath.Join(logDir, logging.SessionFilename("legacy_only")), []byte("legacy\n"), 0o600))
36 26
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()
38 32
39 merged, err := getSessionsMerged(context.Background(), mgr, logDir)33 merged, err := getSessionsMerged(context.Background(), mgr, logDir)
40 require.NoError(t, err)34 require.NoError(t, err)
@@ -54,7 +48,11 @@ func TestGetSessionsMerged_MissingTableFallsBack(t *testing.T) {
54 logDir := t.TempDir()48 logDir := t.TempDir()
55 require.NoError(t, os.WriteFile(filepath.Join(logDir, logging.SessionFilename("legacy_only")), []byte("legacy\n"), 0o600))49 require.NoError(t, os.WriteFile(filepath.Join(logDir, logging.SessionFilename("legacy_only")), []byte("legacy\n"), 0o600))
56 50
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()
58 merged, err := getSessionsMerged(context.Background(), mgr, logDir)56 merged, err := getSessionsMerged(context.Background(), mgr, logDir)
59 require.NoError(t, err)57 require.NoError(t, err)
60 require.Len(t, merged, 1)58 require.Len(t, merged, 1)
@@ -65,7 +63,11 @@ func TestFindSession_DBShortIDMatch(t *testing.T) {
65 logDir := t.TempDir()63 logDir := t.TempDir()
66 64
67 s1 := &entity.Session{ID: entity.SessionID("20251217_205106_a7b3"), Type: entity.SessionTypeBrowser, StartedAt: time.Date(2025, 12, 17, 20, 51, 6, 0, time.UTC)}65 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()
69 71
70 info, err := findSession(context.Background(), mgr, logDir, "a7b3")72 info, err := findSession(context.Background(), mgr, logDir, "a7b3")
71 require.NoError(t, err)73 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 (
8 "testing"8 "testing"
9 9
10 "github.com/bnema/dumber/internal/application/port"10 "github.com/bnema/dumber/internal/application/port"
11+ pipewiremocks "github.com/bnema/dumber/internal/infrastructure/audio/pipewire/mocks"
11 pwpipewire "github.com/bnema/purego-pipewire/pipewire"12 pwpipewire "github.com/bnema/purego-pipewire/pipewire"
12 "github.com/stretchr/testify/assert"13 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/require"14 "github.com/stretchr/testify/require"
14 )15 )
15 16
16-// --- Test doubles ---17+type playerRecorder struct {
18+ *pipewiremocks.MockPlayer
17 19
18-// fakePlayer is a test double for purego-pipewire Player.
19-type fakePlayer struct {
20 started bool20 started bool
21 paused bool21 paused bool
22 stopped bool22 stopped bool
@@ -28,7 +28,19 @@ type fakePlayer struct {
28 closeErr error28 closeErr error
29 }29 }
30 30
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 {
32 if p.startErr != nil {44 if p.startErr != nil {
33 return p.startErr45 return p.startErr
34 }46 }
@@ -37,13 +49,13 @@ func (p *fakePlayer) Start() error {
37 return nil49 return nil
38 }50 }
39 51
40-func (p *fakePlayer) Pause() error {52+func (p *playerRecorder) pause() error {
41 p.paused = true53 p.paused = true
42 p.state = pwpipewire.PlayerStatePaused54 p.state = pwpipewire.PlayerStatePaused
43 return nil55 return nil
44 }56 }
45 57
46-func (p *fakePlayer) Stop() error {58+func (p *playerRecorder) stop() error {
47 if p.stopErr != nil {59 if p.stopErr != nil {
48 return p.stopErr60 return p.stopErr
49 }61 }
@@ -52,7 +64,7 @@ func (p *fakePlayer) Stop() error {
52 return nil64 return nil
53 }65 }
54 66
55-func (p *fakePlayer) Close() error {67+func (p *playerRecorder) close() error {
56 if p.closeErr != nil {68 if p.closeErr != nil {
57 return p.closeErr69 return p.closeErr
58 }70 }
@@ -61,7 +73,7 @@ func (p *fakePlayer) Close() error {
61 return nil73 return nil
62 }74 }
63 75
64-func (p *fakePlayer) State() pwpipewire.PlayerState {76+func (p *playerRecorder) currentState() pwpipewire.PlayerState {
65 return p.state77 return p.state
66 }78 }
67 79
@@ -69,7 +81,7 @@ func (p *fakePlayer) State() pwpipewire.PlayerState {
69 81
70 func TestFactory_NewStream_ValidFormat_CreatesStream(t *testing.T) {82 func TestFactory_NewStream_ValidFormat_CreatesStream(t *testing.T) {
71 var capturedConfig pwpipewire.PlayerConfig83 var capturedConfig pwpipewire.PlayerConfig
72- player := &fakePlayer{}84+ player := newPlayerRecorder(t)
73 85
74 factory := &Factory{86 factory := &Factory{
75 createPlayer: func(config pwpipewire.PlayerConfig, _ pwpipewire.PlayerCallbacks) (pwpipewire.Player, error) {87 createPlayer: func(config pwpipewire.PlayerConfig, _ pwpipewire.PlayerCallbacks) (pwpipewire.Player, error) {
@@ -175,7 +187,8 @@ func TestFactory_NewStream_PlayerCreationFails_ReturnsError(t *testing.T) {
175 }187 }
176 188
177 func TestFactory_NewStream_PlayerStartFails_ClosesAndReturnsError(t *testing.T) {189 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")
179 factory := &Factory{192 factory := &Factory{
180 createPlayer: func(_ pwpipewire.PlayerConfig, _ pwpipewire.PlayerCallbacks) (pwpipewire.Player, error) {193 createPlayer: func(_ pwpipewire.PlayerConfig, _ pwpipewire.PlayerCallbacks) (pwpipewire.Player, error) {
181 return player, nil194 return player, nil
@@ -224,7 +237,7 @@ func TestFactory_NewStream_MultipleFormats_Accepted(t *testing.T) {
224 assert.Equal(t, fmt.SampleRate, config.SampleRate)237 assert.Equal(t, fmt.SampleRate, config.SampleRate)
225 assert.Equal(t, fmt.ChannelCount, config.Channels)238 assert.Equal(t, fmt.ChannelCount, config.Channels)
226 assert.Equal(t, fmt.FramesPerBuffer, config.FramesPerBuffer)239 assert.Equal(t, fmt.FramesPerBuffer, config.FramesPerBuffer)
227- return &fakePlayer{}, nil240+ return newPlayerRecorder(t), nil
228 },241 },
229 }242 }
230 243
@@ -244,7 +257,7 @@ func TestFactory_ImplementsPortInterface(_ *testing.T) {
244 // --- Stream adapter tests ---257 // --- Stream adapter tests ---
245 258
246 func TestPlayerStreamAdapter_Write_SendsToBuffer(t *testing.T) {259 func TestPlayerStreamAdapter_Write_SendsToBuffer(t *testing.T) {
247- player := &fakePlayer{}260+ player := newPlayerRecorder(t)
248 adapter := &playerStreamAdapter{261 adapter := &playerStreamAdapter{
249 player: player,262 player: player,
250 buf: make(chan [][]float32, 4),263 buf: make(chan [][]float32, 4),
@@ -265,7 +278,7 @@ func TestPlayerStreamAdapter_Write_SendsToBuffer(t *testing.T) {
265 }278 }
266 279
267 func TestPlayerStreamAdapter_Write_AfterClose_ReturnsError(t *testing.T) {280 func TestPlayerStreamAdapter_Write_AfterClose_ReturnsError(t *testing.T) {
268- player := &fakePlayer{}281+ player := newPlayerRecorder(t)
269 adapter := &playerStreamAdapter{282 adapter := &playerStreamAdapter{
270 player: player,283 player: player,
271 buf: make(chan [][]float32, 4),284 buf: make(chan [][]float32, 4),
@@ -279,7 +292,7 @@ func TestPlayerStreamAdapter_Write_AfterClose_ReturnsError(t *testing.T) {
279 }292 }
280 293
281 func TestPlayerStreamAdapter_Close_StopsAndClosesPlayer(t *testing.T) {294 func TestPlayerStreamAdapter_Close_StopsAndClosesPlayer(t *testing.T) {
282- player := &fakePlayer{}295+ player := newPlayerRecorder(t)
283 adapter := &playerStreamAdapter{296 adapter := &playerStreamAdapter{
284 player: player,297 player: player,
285 buf: make(chan [][]float32, 4),298 buf: make(chan [][]float32, 4),
@@ -294,7 +307,7 @@ func TestPlayerStreamAdapter_Close_StopsAndClosesPlayer(t *testing.T) {
294 }307 }
295 308
296 func TestPlayerStreamAdapter_Close_IsIdempotent(t *testing.T) {309 func TestPlayerStreamAdapter_Close_IsIdempotent(t *testing.T) {
297- player := &fakePlayer{}310+ player := newPlayerRecorder(t)
298 adapter := &playerStreamAdapter{311 adapter := &playerStreamAdapter{
299 player: player,312 player: player,
300 buf: make(chan [][]float32, 4),313 buf: make(chan [][]float32, 4),
@@ -306,7 +319,8 @@ func TestPlayerStreamAdapter_Close_IsIdempotent(t *testing.T) {
306 }319 }
307 320
308 func TestPlayerStreamAdapter_Close_PropagatesStopError(t *testing.T) {321 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")
310 adapter := &playerStreamAdapter{324 adapter := &playerStreamAdapter{
311 player: player,325 player: player,
312 buf: make(chan [][]float32, 4),326 buf: make(chan [][]float32, 4),
@@ -319,7 +333,8 @@ func TestPlayerStreamAdapter_Close_PropagatesStopError(t *testing.T) {
319 }333 }
320 334
321 func TestPlayerStreamAdapter_Close_PropagatesCloseError(t *testing.T) {335 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")
323 adapter := &playerStreamAdapter{338 adapter := &playerStreamAdapter{
324 player: player,339 player: player,
325 buf: make(chan [][]float32, 4),340 buf: make(chan [][]float32, 4),
@@ -427,7 +442,7 @@ func TestCopyToPCM_EmptySamples_ReturnsZero(t *testing.T) {
427 442
428 func TestPlayerStreamAdapter_Write_DropsWhenBufferFull(t *testing.T) {443 func TestPlayerStreamAdapter_Write_DropsWhenBufferFull(t *testing.T) {
429 adapter := &playerStreamAdapter{444 adapter := &playerStreamAdapter{
430- player: &fakePlayer{},445+ player: newPlayerRecorder(t),
431 buf: make(chan [][]float32, 4),446 buf: make(chan [][]float32, 4),
432 closed: make(chan struct{}),447 closed: make(chan struct{}),
433 }448 }
@@ -450,7 +465,7 @@ func TestPlayerStreamAdapter_Write_DropsWhenBufferFull(t *testing.T) {
450 465
451 func TestPlayerStreamAdapter_Write_DropsLogOnlyOnFirstAndPowerOfTwo(t *testing.T) {466 func TestPlayerStreamAdapter_Write_DropsLogOnlyOnFirstAndPowerOfTwo(t *testing.T) {
452 adapter := &playerStreamAdapter{467 adapter := &playerStreamAdapter{
453- player: &fakePlayer{},468+ player: newPlayerRecorder(t),
454 buf: make(chan [][]float32, 1), // tiny buffer to force drops469 buf: make(chan [][]float32, 1), // tiny buffer to force drops
455 closed: make(chan struct{}),470 closed: make(chan struct{}),
456 }471 }
@@ -470,7 +485,7 @@ func TestPlayerStreamAdapter_Write_DropsLogOnlyOnFirstAndPowerOfTwo(t *testing.T
470 // --- Concurrent Write / Close tests ---485 // --- Concurrent Write / Close tests ---
471 486
472 func TestPlayerStreamAdapter_ConcurrentWriteClose_NoRace(t *testing.T) {487 func TestPlayerStreamAdapter_ConcurrentWriteClose_NoRace(t *testing.T) {
473- player := &fakePlayer{}488+ player := newPlayerRecorder(t)
474 adapter := &playerStreamAdapter{489 adapter := &playerStreamAdapter{
475 player: player,490 player: player,
476 buf: make(chan [][]float32, 4),491 buf: make(chan [][]float32, 4),
@@ -507,7 +522,7 @@ func TestNewFactory_Succeeds(t *testing.T) {
507 522
508 func TestStream_WriteFlowsThroughToFill(t *testing.T) {523 func TestStream_WriteFlowsThroughToFill(t *testing.T) {
509 var capturedCallbacks pwpipewire.PlayerCallbacks524 var capturedCallbacks pwpipewire.PlayerCallbacks
510- player := &fakePlayer{}525+ player := newPlayerRecorder(t)
511 526
512 factory := &Factory{527 factory := &Factory{
513 createPlayer: func(_ pwpipewire.PlayerConfig, callbacks pwpipewire.PlayerCallbacks) (pwpipewire.Player, error) {528 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 *
66 DataKind: "json",66 DataKind: "json",
67 TargetOrigin: "https://app.example",67 TargetOrigin: "https://app.example",
68 SourceOrigin: "https://evil.example",68 SourceOrigin: "https://evil.example",
69- SourceHref: "https://evil.example/fake",69+ SourceHref: "https://evil.example/decoy",
70 })70 })
71 }71 }
M internal/infrastructure/clipboard/clipboard_test.go +55 -37
@@ -9,10 +9,14 @@ import (
9 "testing"9 "testing"
10 10
11 "github.com/bnema/dumber/internal/domain/entity"11 "github.com/bnema/dumber/internal/domain/entity"
12+ clipboardmocks "github.com/bnema/dumber/internal/infrastructure/clipboard/mocks"
13+ "github.com/stretchr/testify/mock"
12 "github.com/stretchr/testify/require"14 "github.com/stretchr/testify/require"
13 )15 )
14 16
15-type fakeToolkitClipboard struct {17+type toolkitClipboardRecorder struct {
18+ *clipboardmocks.MockToolkitClipboard
19+
16 writeTextCalls int20 writeTextCalls int
17 readTextCalls int21 readTextCalls int
18 writeImageCalls int22 writeImageCalls int
@@ -23,67 +27,81 @@ type fakeToolkitClipboard struct {
23 writeImageErr error27 writeImageErr error
24 }28 }
25 29
26-func (f *fakeToolkitClipboard) WriteText(_ context.Context, text string) error {30+func newToolkitClipboardRecorder(t *testing.T) *toolkitClipboardRecorder {
27- f.writeTextCalls++31+ t.Helper()
28- f.text = text32+
29- return f.writeTextErr33+ 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
30 }44 }
31 45
32-func (f *fakeToolkitClipboard) ReadText(_ context.Context) (string, error) {46+func (r *toolkitClipboardRecorder) readText(_ context.Context) (string, error) {
33- f.readTextCalls++47+ r.readTextCalls++
34- return f.text, f.readTextErr48+ return r.text, r.readTextErr
35 }49 }
36 50
37-func (f *fakeToolkitClipboard) WriteImage(_ context.Context, image entity.ImageData) error {51+func (r *toolkitClipboardRecorder) writeImage(_ context.Context, image entity.ImageData) error {
38- f.writeImageCalls++52+ r.writeImageCalls++
39- f.image = image53+ r.image = image
40- return f.writeImageErr54+ return r.writeImageErr
41 }55 }
42 56
43 func TestAdapter_ReadTextFallsBackToToolkitClipboardWhenNoSystemTool(t *testing.T) {57 func TestAdapter_ReadTextFallsBackToToolkitClipboardWhenNoSystemTool(t *testing.T) {
44- fake := &fakeToolkitClipboard{text: "toolkit text"}58+ toolkit := newToolkitClipboardRecorder(t)
45- adapter := &Adapter{toolkit: fake}59+ toolkit.text = "toolkit text"
60+ adapter := &Adapter{toolkit: toolkit}
46 61
47 text, err := adapter.ReadText(context.Background())62 text, err := adapter.ReadText(context.Background())
48 63
49 require.NoError(t, err)64 require.NoError(t, err)
50 require.Equal(t, "toolkit text", text)65 require.Equal(t, "toolkit text", text)
51- require.Equal(t, 1, fake.readTextCalls)66+ require.Equal(t, 1, toolkit.readTextCalls)
52 }67 }
53 68
54 func TestAdapter_HasTextReturnsTrueWhenToolkitFallbackHasText(t *testing.T) {69 func TestAdapter_HasTextReturnsTrueWhenToolkitFallbackHasText(t *testing.T) {
55- fake := &fakeToolkitClipboard{text: "toolkit text"}70+ toolkit := newToolkitClipboardRecorder(t)
56- adapter := &Adapter{toolkit: fake}71+ toolkit.text = "toolkit text"
72+ adapter := &Adapter{toolkit: toolkit}
57 73
58 hasText, err := adapter.HasText(context.Background())74 hasText, err := adapter.HasText(context.Background())
59 75
60 require.NoError(t, err)76 require.NoError(t, err)
61 require.True(t, hasText)77 require.True(t, hasText)
62- require.Equal(t, 1, fake.readTextCalls)78+ require.Equal(t, 1, toolkit.readTextCalls)
63 }79 }
64 80
65 func TestAdapter_WriteTextReturnsToolkitErrorWhenNoSystemTool(t *testing.T) {81 func TestAdapter_WriteTextReturnsToolkitErrorWhenNoSystemTool(t *testing.T) {
66 sentinel := errors.New("toolkit text failed")82 sentinel := errors.New("toolkit text failed")
67- fake := &fakeToolkitClipboard{writeTextErr: sentinel}83+ toolkit := newToolkitClipboardRecorder(t)
68- adapter := &Adapter{toolkit: fake}84+ toolkit.writeTextErr = sentinel
85+ adapter := &Adapter{toolkit: toolkit}
69 86
70 err := adapter.WriteText(context.Background(), "hello")87 err := adapter.WriteText(context.Background(), "hello")
71 88
72 require.Equal(t, sentinel, err)89 require.Equal(t, sentinel, err)
73- require.Equal(t, 1, fake.writeTextCalls)90+ require.Equal(t, 1, toolkit.writeTextCalls)
74- require.Equal(t, "hello", fake.text)91+ require.Equal(t, "hello", toolkit.text)
75 }92 }
76 93
77 func TestAdapter_WriteImageReturnsToolkitErrorWhenNoSystemTool(t *testing.T) {94 func TestAdapter_WriteImageReturnsToolkitErrorWhenNoSystemTool(t *testing.T) {
78 sentinel := errors.New("toolkit image failed")95 sentinel := errors.New("toolkit image failed")
79- fake := &fakeToolkitClipboard{writeImageErr: sentinel}96+ toolkit := newToolkitClipboardRecorder(t)
80- adapter := &Adapter{toolkit: fake}97+ toolkit.writeImageErr = sentinel
98+ adapter := &Adapter{toolkit: toolkit}
81 99
82 err := adapter.WriteImage(context.Background(), entity.ImageData{Bytes: []byte{1}, MimeType: "image/png"})100 err := adapter.WriteImage(context.Background(), entity.ImageData{Bytes: []byte{1}, MimeType: "image/png"})
83 101
84 require.Equal(t, sentinel, err)102 require.Equal(t, sentinel, err)
85- require.Equal(t, 1, fake.writeImageCalls)103+ require.Equal(t, 1, toolkit.writeImageCalls)
86- require.Equal(t, entity.ImageData{Bytes: []byte{1}, MimeType: "image/png"}, fake.image)104+ require.Equal(t, entity.ImageData{Bytes: []byte{1}, MimeType: "image/png"}, toolkit.image)
87 }105 }
88 106
89 func TestNew_FallsBackToToolkitClipboardWhenSystemToolsUnavailable(t *testing.T) {107 func TestNew_FallsBackToToolkitClipboardWhenSystemToolsUnavailable(t *testing.T) {
@@ -94,9 +112,9 @@ func TestNew_FallsBackToToolkitClipboardWhenSystemToolsUnavailable(t *testing.T)
94 lookPath = oldLookPath112 lookPath = oldLookPath
95 })113 })
96 114
97- fake := &fakeToolkitClipboard{}115+ toolkit := newToolkitClipboardRecorder(t)
98 newToolkitClipboard = func() toolkitClipboard {116 newToolkitClipboard = func() toolkitClipboard {
99- return fake117+ return toolkit
100 }118 }
101 lookPath = func(_ string) (string, error) {119 lookPath = func(_ string) (string, error) {
102 return "", errors.New("missing")120 return "", errors.New("missing")
@@ -107,8 +125,8 @@ func TestNew_FallsBackToToolkitClipboardWhenSystemToolsUnavailable(t *testing.T)
107 125
108 adapter := New().(*Adapter)126 adapter := New().(*Adapter)
109 require.NoError(t, adapter.WriteImage(context.Background(), entity.ImageData{Bytes: []byte{1, 2, 3}}))127 require.NoError(t, adapter.WriteImage(context.Background(), entity.ImageData{Bytes: []byte{1, 2, 3}}))
110- require.Equal(t, 1, fake.writeImageCalls)128+ require.Equal(t, 1, toolkit.writeImageCalls)
111- require.Equal(t, []byte{1, 2, 3}, fake.image.Bytes)129+ require.Equal(t, []byte{1, 2, 3}, toolkit.image.Bytes)
112 }130 }
113 131
114 func TestAdapter_WriteImagePrefersToolkitClipboardWhenAvailable(t *testing.T) {132 func TestAdapter_WriteImagePrefersToolkitClipboardWhenAvailable(t *testing.T) {
@@ -123,14 +141,14 @@ func TestAdapter_WriteImagePrefersToolkitClipboardWhenAvailable(t *testing.T) {
123 script := "#!/bin/sh\ncat > \"" + stdinPath + "\"\n"141 script := "#!/bin/sh\ncat > \"" + stdinPath + "\"\n"
124 require.NoError(t, os.WriteFile(scriptPath, []byte(script), 0o755))142 require.NoError(t, os.WriteFile(scriptPath, []byte(script), 0o755))
125 143
126- fake := &fakeToolkitClipboard{}144+ toolkit := newToolkitClipboardRecorder(t)
127- newToolkitClipboard = func() toolkitClipboard { return fake }145+ newToolkitClipboard = func() toolkitClipboard { return toolkit }
128 adapter := &Adapter{copyCmd: scriptPath}146 adapter := &Adapter{copyCmd: scriptPath}
129 image := entity.ImageData{Bytes: []byte{1, 2, 3}, MimeType: "image/png"}147 image := entity.ImageData{Bytes: []byte{1, 2, 3}, MimeType: "image/png"}
130 148
131 require.NoError(t, adapter.WriteImage(context.Background(), image))149 require.NoError(t, adapter.WriteImage(context.Background(), image))
132- require.Equal(t, 1, fake.writeImageCalls)150+ require.Equal(t, 1, toolkit.writeImageCalls)
133- require.Equal(t, image, fake.image)151+ require.Equal(t, image, toolkit.image)
134 152
135 _, err := os.Stat(stdinPath)153 _, err := os.Stat(stdinPath)
136 require.Error(t, err)154 require.Error(t, err)
@@ -193,14 +211,14 @@ func TestAdapter_WriteImageRejectsEmptyBytes(t *testing.T) {
193 lookPath = oldLookPath211 lookPath = oldLookPath
194 })212 })
195 213
196- fake := &fakeToolkitClipboard{}214+ toolkit := newToolkitClipboardRecorder(t)
197- newToolkitClipboard = func() toolkitClipboard { return fake }215+ newToolkitClipboard = func() toolkitClipboard { return toolkit }
198 lookPath = func(_ string) (string, error) { return "/usr/bin/clipboard", nil }216 lookPath = func(_ string) (string, error) { return "/usr/bin/clipboard", nil }
199 217
200 err := New().(*Adapter).WriteImage(context.Background(), entity.ImageData{})218 err := New().(*Adapter).WriteImage(context.Background(), entity.ImageData{})
201 require.Error(t, err)219 require.Error(t, err)
202 require.Contains(t, err.Error(), "empty image data")220 require.Contains(t, err.Error(), "empty image data")
203- require.Zero(t, fake.writeImageCalls)221+ require.Zero(t, toolkit.writeImageCalls)
204 }222 }
205 223
206 func TestAdapter_WriteImageWithCommandUsesImageMimeType(t *testing.T) {224 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 (
9 9
10 "github.com/bnema/dumber/internal/application/dto"10 "github.com/bnema/dumber/internal/application/dto"
11 "github.com/bnema/dumber/internal/application/port"11 "github.com/bnema/dumber/internal/application/port"
12+ systemviewsbridgemocks "github.com/bnema/dumber/internal/infrastructure/systemviewsbridge/mocks"
13+ "github.com/stretchr/testify/mock"
12 )14 )
13 15
14 func TestBuildMessageEnvelope(t *testing.T) {16 func TestBuildMessageEnvelope(t *testing.T) {
@@ -41,8 +43,8 @@ func TestBuildMessageEnvelope(t *testing.T) {
41 func TestClientSendPrefersNativeTransport(t *testing.T) {43 func TestClientSendPrefersNativeTransport(t *testing.T) {
42 t.Parallel()44 t.Parallel()
43 45
44- native := &fakeTransport{available: true, response: []byte(`{"transport":"native"}`)}46+ native := newTransportRecorder(t, true, []byte(`{"transport":"native"}`))
45- fetch := &fakeTransport{available: true, response: []byte(`{"transport":"fetch"}`)}47+ fetch := newTransportRecorder(t, true, []byte(`{"transport":"fetch"}`))
46 48
47 client := NewClient(native, fetch)49 client := NewClient(native, fetch)
48 got, err := client.Send(context.Background(), "favorite_list", struct {50 got, err := client.Send(context.Background(), "favorite_list", struct {
@@ -75,8 +77,8 @@ func TestClientSendPrefersNativeTransport(t *testing.T) {
75 func TestClientSendFallsBackToFetchTransport(t *testing.T) {77 func TestClientSendFallsBackToFetchTransport(t *testing.T) {
76 t.Parallel()78 t.Parallel()
77 79
78- native := &fakeTransport{available: false, response: []byte(`{"transport":"native"}`)}80+ native := newTransportRecorder(t, false, []byte(`{"transport":"native"}`))
79- fetch := &fakeTransport{available: true, response: []byte(`{"transport":"fetch"}`)}81+ fetch := newTransportRecorder(t, true, []byte(`{"transport":"fetch"}`))
80 82
81 client := NewClient(native, fetch)83 client := NewClient(native, fetch)
82 got, err := client.Send(context.Background(), "favorite_list", struct {84 got, err := client.Send(context.Background(), "favorite_list", struct {
@@ -101,7 +103,7 @@ func TestClientSendFallsBackToFetchTransport(t *testing.T) {
101 func TestClientTimelineDecodesEntries(t *testing.T) {103 func TestClientTimelineDecodesEntries(t *testing.T) {
102 t.Parallel()104 t.Parallel()
103 105
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"}]}`))
105 client := NewClient(native, nil)107 client := NewClient(native, nil)
106 108
107 entries, err := client.Timeline(context.Background(), 25, 0)109 entries, err := client.Timeline(context.Background(), 25, 0)
@@ -130,7 +132,7 @@ func TestClientTimelineDecodesEntries(t *testing.T) {
130 func TestClientTimelineWindowOmitsZeroCursor(t *testing.T) {132 func TestClientTimelineWindowOmitsZeroCursor(t *testing.T) {
131 t.Parallel()133 t.Parallel()
132 134
133- native := &fakeTransport{available: true, response: []byte(`{"requestId":"req-window","success":true}`)}135+ native := newTransportRecorder(t, true, []byte(`{"requestId":"req-window","success":true}`))
134 client := NewClient(native, nil)136 client := NewClient(native, nil)
135 137
136 _, err := client.TimelineWindow(context.Background(), time.Time{}, "")138 _, err := client.TimelineWindow(context.Background(), time.Time{}, "")
@@ -157,7 +159,7 @@ func TestClientTimelineWindowOmitsZeroCursor(t *testing.T) {
157 func TestClientDeleteRangeRejectsEmptyRange(t *testing.T) {159 func TestClientDeleteRangeRejectsEmptyRange(t *testing.T) {
158 t.Parallel()160 t.Parallel()
159 161
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}`))
161 client := NewClient(transport, nil)163 client := NewClient(transport, nil)
162 164
163 err := client.DeleteRange(context.Background(), " ")165 err := client.DeleteRange(context.Background(), " ")
@@ -172,7 +174,7 @@ func TestClientDeleteRangeRejectsEmptyRange(t *testing.T) {
172 func TestClientDeleteRangeSendsRange(t *testing.T) {174 func TestClientDeleteRangeSendsRange(t *testing.T) {
173 t.Parallel()175 t.Parallel()
174 176
175- native := &fakeTransport{available: true, response: []byte(`{"requestId":"req-10","success":true}`)}177+ native := newTransportRecorder(t, true, []byte(`{"requestId":"req-10","success":true}`))
176 client := NewClient(native, nil)178 client := NewClient(native, nil)
177 179
178 if err := client.DeleteRange(context.Background(), "week"); err != nil {180 if err := client.DeleteRange(context.Background(), "week"); err != nil {
@@ -191,7 +193,7 @@ func TestClientDeleteRangeSendsRange(t *testing.T) {
191 func TestClientListDecodesFavorites(t *testing.T) {193 func TestClientListDecodesFavorites(t *testing.T) {
192 t.Parallel()194 t.Parallel()
193 195
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"}]}`))
195 client := NewClient(native, nil)197 client := NewClient(native, nil)
196 198
197 favorites, err := client.List(context.Background())199 favorites, err := client.List(context.Background())
@@ -217,7 +219,7 @@ func TestClientListDecodesFavorites(t *testing.T) {
217 func TestClientListFoldersDecodesFolders(t *testing.T) {219 func TestClientListFoldersDecodesFolders(t *testing.T) {
218 t.Parallel()220 t.Parallel()
219 221
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"}]}`))
221 client := NewClient(native, nil)223 client := NewClient(native, nil)
222 224
223 folders, err := client.ListFolders(context.Background())225 folders, err := client.ListFolders(context.Background())
@@ -243,7 +245,7 @@ func TestClientListFoldersDecodesFolders(t *testing.T) {
243 func TestClientListTagsDecodesTags(t *testing.T) {245 func TestClientListTagsDecodesTags(t *testing.T) {
244 t.Parallel()246 t.Parallel()
245 247
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"}]}`))
247 client := NewClient(native, nil)249 client := NewClient(native, nil)
248 250
249 tags, err := client.ListTags(context.Background())251 tags, err := client.ListTags(context.Background())
@@ -271,7 +273,8 @@ func TestClientCurrentAndDefaultDecodeConfigPayload(t *testing.T) {
271 273
272 resp := []byte(`{"requestId":"req-14","success":true,"data":{"engine_type":"webkit","default_search_engine":"DuckDuckGo"}}`)274 resp := []byte(`{"requestId":"req-14","success":true,"data":{"engine_type":"webkit","default_search_engine":"DuckDuckGo"}}`)
273 275
274- currentClient := NewClient(&fakeTransport{available: true, response: resp}, nil)276+ currentTransport := newTransportRecorder(t, true, resp)
277+ currentClient := NewClient(currentTransport, nil)
275 current, err := currentClient.Current(context.Background())278 current, err := currentClient.Current(context.Background())
276 if err != nil {279 if err != nil {
277 t.Fatalf("Current() error = %v", err)280 t.Fatalf("Current() error = %v", err)
@@ -281,14 +284,15 @@ func TestClientCurrentAndDefaultDecodeConfigPayload(t *testing.T) {
281 }284 }
282 285
283 var msg port.WebUIMessage286 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 {
285 t.Fatalf("unmarshal sent envelope: %v", unmarshalErr)288 t.Fatalf("unmarshal sent envelope: %v", unmarshalErr)
286 }289 }
287 if msg.Type != "/api/config" {290 if msg.Type != "/api/config" {
288 t.Fatalf("sent type = %q, want %q", msg.Type, "/api/config")291 t.Fatalf("sent type = %q, want %q", msg.Type, "/api/config")
289 }292 }
290 293
291- defaultClient := NewClient(&fakeTransport{available: true, response: resp}, nil)294+ defaultTransport := newTransportRecorder(t, true, resp)
295+ defaultClient := NewClient(defaultTransport, nil)
292 defaultCfg, err := defaultClient.Default(context.Background())296 defaultCfg, err := defaultClient.Default(context.Background())
293 if err != nil {297 if err != nil {
294 t.Fatalf("Default() error = %v", err)298 t.Fatalf("Default() error = %v", err)
@@ -297,7 +301,7 @@ func TestClientCurrentAndDefaultDecodeConfigPayload(t *testing.T) {
297 t.Fatalf("Default() = %+v", defaultCfg)301 t.Fatalf("Default() = %+v", defaultCfg)
298 }302 }
299 303
300- if err := json.Unmarshal(defaultClient.native.(*fakeTransport).last, &msg); err != nil {304+ if err := json.Unmarshal(defaultTransport.last, &msg); err != nil {
301 t.Fatalf("unmarshal sent envelope: %v", err)305 t.Fatalf("unmarshal sent envelope: %v", err)
302 }306 }
303 if msg.Type != "/api/config/default" {307 if msg.Type != "/api/config/default" {
@@ -372,7 +376,7 @@ func TestClientConfigActionsUseExpectedMessageTypes(t *testing.T) {
372 t.Run(tt.name, func(t *testing.T) {376 t.Run(tt.name, func(t *testing.T) {
373 t.Parallel()377 t.Parallel()
374 378
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":[]}}`))
376 if tt.name == "save config" || tt.name == "reset keybinding" || tt.name == "reset all keybindings" {380 if tt.name == "save config" || tt.name == "reset keybinding" || tt.name == "reset all keybindings" {
377 transport.response = []byte(`{"requestId":"req-15","success":true}`)381 transport.response = []byte(`{"requestId":"req-15","success":true}`)
378 }382 }
@@ -418,7 +422,7 @@ func TestClientCreateFolderIncludesParentID(t *testing.T) {
418 t.Parallel()422 t.Parallel()
419 423
420 parentID := int64(42)424 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}}`))
422 client := NewClient(transport, nil)426 client := NewClient(transport, nil)
423 427
424 folder, err := client.CreateFolder(context.Background(), "Nested", " 📁 ", &parentID)428 folder, err := client.CreateFolder(context.Background(), "Nested", " 📁 ", &parentID)
@@ -472,7 +476,7 @@ func TestClientCreateFolderIncludesParentID(t *testing.T) {
472 func TestClientDomainStatsRejectsNegativeLimit(t *testing.T) {476 func TestClientDomainStatsRejectsNegativeLimit(t *testing.T) {
473 t.Parallel()477 t.Parallel()
474 478
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":[]}`))
476 client := NewClient(transport, nil)480 client := NewClient(transport, nil)
477 481
478 _, err := client.DomainStats(context.Background(), -1)482 _, err := client.DomainStats(context.Background(), -1)
@@ -487,7 +491,7 @@ func TestClientDomainStatsRejectsNegativeLimit(t *testing.T) {
487 func TestClientDeleteDomainRejectsEmptyDomain(t *testing.T) {491 func TestClientDeleteDomainRejectsEmptyDomain(t *testing.T) {
488 t.Parallel()492 t.Parallel()
489 493
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}`))
491 client := NewClient(transport, nil)495 client := NewClient(transport, nil)
492 496
493 err := client.DeleteDomain(context.Background(), " ")497 err := client.DeleteDomain(context.Background(), " ")
@@ -502,7 +506,7 @@ func TestClientDeleteDomainRejectsEmptyDomain(t *testing.T) {
502 func TestClientUpdateFolderTrimsIcon(t *testing.T) {506 func TestClientUpdateFolderTrimsIcon(t *testing.T) {
503 t.Parallel()507 t.Parallel()
504 508
505- transport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-18","success":true}`)}509+ transport := newTransportRecorder(t, true, []byte(`{"requestId":"req-18","success":true}`))
506 client := NewClient(transport, nil)510 client := NewClient(transport, nil)
507 511
508 if err := client.UpdateFolder(context.Background(), 9, "Read", " 📚 "); err != nil {512 if err := client.UpdateFolder(context.Background(), 9, "Read", " 📚 "); err != nil {
@@ -530,7 +534,7 @@ func TestClientUpdateFolderTrimsIcon(t *testing.T) {
530 func TestClientUpdateFolderOmitsBlankIcon(t *testing.T) {534 func TestClientUpdateFolderOmitsBlankIcon(t *testing.T) {
531 t.Parallel()535 t.Parallel()
532 536
533- transport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-19","success":true}`)}537+ transport := newTransportRecorder(t, true, []byte(`{"requestId":"req-19","success":true}`))
534 client := NewClient(transport, nil)538 client := NewClient(transport, nil)
535 539
536 if err := client.UpdateFolder(context.Background(), 9, "Read", " "); err != nil {540 if err := client.UpdateFolder(context.Background(), 9, "Read", " "); err != nil {
@@ -555,7 +559,7 @@ func TestClientUpdateFolderOmitsBlankIcon(t *testing.T) {
555 func TestClientDeleteEntryRejectsInvalidID(t *testing.T) {559 func TestClientDeleteEntryRejectsInvalidID(t *testing.T) {
556 t.Parallel()560 t.Parallel()
557 561
558- transport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-20","success":true}`)}562+ transport := newTransportRecorder(t, true, []byte(`{"requestId":"req-20","success":true}`))
559 client := NewClient(transport, nil)563 client := NewClient(transport, nil)
560 564
561 err := client.DeleteEntry(context.Background(), 0)565 err := client.DeleteEntry(context.Background(), 0)
@@ -570,7 +574,7 @@ func TestClientDeleteEntryRejectsInvalidID(t *testing.T) {
570 func TestClientTagActionsTrimFields(t *testing.T) {574 func TestClientTagActionsTrimFields(t *testing.T) {
571 t.Parallel()575 t.Parallel()
572 576
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"}}`))
574 client := NewClient(createTransport, nil)578 client := NewClient(createTransport, nil)
575 if _, err := client.CreateTag(context.Background(), " Go ", " #00add8 "); err != nil {579 if _, err := client.CreateTag(context.Background(), " Go ", " #00add8 "); err != nil {
576 t.Fatalf("CreateTag() error = %v", err)580 t.Fatalf("CreateTag() error = %v", err)
@@ -590,7 +594,7 @@ func TestClientTagActionsTrimFields(t *testing.T) {
590 t.Fatalf("CreateTag payload = %#v", createPayload)594 t.Fatalf("CreateTag payload = %#v", createPayload)
591 }595 }
592 596
593- updateTransport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-22","success":true}`)}597+ updateTransport := newTransportRecorder(t, true, []byte(`{"requestId":"req-22","success":true}`))
594 client = NewClient(updateTransport, nil)598 client = NewClient(updateTransport, nil)
595 if err := client.UpdateTag(context.Background(), 5, " Go ", " "); err != nil {599 if err := client.UpdateTag(context.Background(), 5, " Go ", " "); err != nil {
596 t.Fatalf("UpdateTag() error = %v", err)600 t.Fatalf("UpdateTag() error = %v", err)
@@ -610,16 +614,29 @@ func TestClientTagActionsTrimFields(t *testing.T) {
610 }614 }
611 }615 }
612 616
613-type fakeTransport struct {617+type transportRecorder struct {
618+ *systemviewsbridgemocks.MockTransport
619+
614 available bool620 available bool
615 called bool621 called bool
616 last []byte622 last []byte
617 response []byte623 response []byte
618 }624 }
619 625
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+}
621 638
622-func (t *fakeTransport) Send(_ context.Context, body []byte) ([]byte, error) {639+func (t *transportRecorder) send(_ context.Context, body []byte) ([]byte, error) {
623 t.called = true640 t.called = true
624 t.last = append(t.last[:0], body...)641 t.last = append(t.last[:0], body...)
625 return t.response, nil642 return t.response, nil
@@ -641,7 +658,7 @@ func (t *blockingTransport) Send(ctx context.Context, _ []byte) ([]byte, error)
641 func TestClientRejectsInvalidFavoriteMutationIDs(t *testing.T) {658 func TestClientRejectsInvalidFavoriteMutationIDs(t *testing.T) {
642 t.Parallel()659 t.Parallel()
643 660
644- transport := &fakeTransport{available: true, response: []byte(`{"requestId":"req","success":true}`)}661+ transport := newTransportRecorder(t, true, []byte(`{"requestId":"req","success":true}`))
645 client := NewClient(transport, nil)662 client := NewClient(transport, nil)
646 663
647 checks := []struct {664 checks := []struct {
@@ -679,7 +696,7 @@ func TestClientRejectsInvalidFavoriteMutationIDs(t *testing.T) {
679 func TestClientRejectsNegativeHistoryLimits(t *testing.T) {696 func TestClientRejectsNegativeHistoryLimits(t *testing.T) {
680 t.Parallel()697 t.Parallel()
681 698
682- client := NewClient(&fakeTransport{available: true}, nil)699+ client := NewClient(newTransportRecorder(t, true, nil), nil)
683 700
684 if _, err := client.Timeline(context.Background(), -1, 0); err == nil {701 if _, err := client.Timeline(context.Background(), -1, 0); err == nil {
685 t.Fatal("Timeline() error = nil, want error")702 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) {
122 applier := NewApplier(tmpDir)122 applier := NewApplier(tmpDir)
123 ctx := context.Background()123 ctx := context.Background()
124 124
125- // Create a fake binary to stage125+ // Create a dummy binary to stage
126 srcBinary := filepath.Join(tmpDir, "new-binary")126 srcBinary := filepath.Join(tmpDir, "new-binary")
127 binaryContent := []byte("#!/bin/sh\necho 'new version'\n")127 binaryContent := []byte("#!/bin/sh\necho 'new version'\n")
128 require.NoError(t, os.WriteFile(srcBinary, binaryContent, 0o755))128 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
2441 }2441 }
2442 }2442 }
2443 2443
2444-// fakeRecordingWebView is a complete port.WebView that records which navigation2444+// recordingWebView is a complete port.WebView that records which navigation
2445 // methods were called and with what arguments. It does not use reflect or unsafe.2445 // methods were called and with what arguments. It does not use reflect or unsafe.
2446-type fakeRecordingWebView struct {2446+type recordingWebView struct {
2447 id port.WebViewID2447 id port.WebViewID
2448 2448
2449 loadURICalled bool2449 loadURICalled bool
@@ -2461,79 +2461,79 @@ type fakeRecordingWebView struct {
2461 destroyCalls int2461 destroyCalls int
2462 }2462 }
2463 2463
2464-func (f *fakeRecordingWebView) ID() port.WebViewID { return f.id }2464+func (f *recordingWebView) ID() port.WebViewID { return f.id }
2465 2465
2466-func (f *fakeRecordingWebView) LoadURI(_ context.Context, uri string) error {2466+func (f *recordingWebView) LoadURI(_ context.Context, uri string) error {
2467 f.loadURICalled = true2467 f.loadURICalled = true
2468 f.loadURILastURI = uri2468 f.loadURILastURI = uri
2469 return nil2469 return nil
2470 }2470 }
2471 2471
2472-func (f *fakeRecordingWebView) LoadHTML(_ context.Context, _, _ string) error {2472+func (f *recordingWebView) LoadHTML(_ context.Context, _, _ string) error {
2473 f.loadHTMLCalls++2473 f.loadHTMLCalls++
2474 return nil2474 return nil
2475 }2475 }
2476 2476
2477-func (f *fakeRecordingWebView) Reload(_ context.Context) error {2477+func (f *recordingWebView) Reload(_ context.Context) error {
2478 f.reloadCalls++2478 f.reloadCalls++
2479 return nil2479 return nil
2480 }2480 }
2481 2481
2482-func (f *fakeRecordingWebView) ReloadBypassCache(_ context.Context) error {2482+func (f *recordingWebView) ReloadBypassCache(_ context.Context) error {
2483 f.reloadBypassCacheCalls++2483 f.reloadBypassCacheCalls++
2484 return nil2484 return nil
2485 }2485 }
2486 2486
2487-func (f *fakeRecordingWebView) Stop(_ context.Context) error {2487+func (f *recordingWebView) Stop(_ context.Context) error {
2488 f.stopCalls++2488 f.stopCalls++
2489 return nil2489 return nil
2490 }2490 }
2491 2491
2492-func (f *fakeRecordingWebView) GoBack(_ context.Context) error {2492+func (f *recordingWebView) GoBack(_ context.Context) error {
2493 f.goBackCalls++2493 f.goBackCalls++
2494 return nil2494 return nil
2495 }2495 }
2496 2496
2497-func (f *fakeRecordingWebView) GoForward(_ context.Context) error {2497+func (f *recordingWebView) GoForward(_ context.Context) error {
2498 f.goForwardCalls++2498 f.goForwardCalls++
2499 return nil2499 return nil
2500 }2500 }
2501 2501
2502-func (f *fakeRecordingWebView) State() port.WebViewState { return port.WebViewState{} }2502+func (f *recordingWebView) State() port.WebViewState { return port.WebViewState{} }
2503 2503
2504-func (f *fakeRecordingWebView) URI() string { return f.loadURILastURI }2504+func (f *recordingWebView) URI() string { return f.loadURILastURI }
2505-func (f *fakeRecordingWebView) Title() string { return "" }2505+func (f *recordingWebView) Title() string { return "" }
2506 2506
2507-func (f *fakeRecordingWebView) IsLoading() bool { return false }2507+func (f *recordingWebView) IsLoading() bool { return false }
2508-func (f *fakeRecordingWebView) EstimatedProgress() float64 { return 0 }2508+func (f *recordingWebView) EstimatedProgress() float64 { return 0 }
2509-func (f *fakeRecordingWebView) CanGoBack() bool { return false }2509+func (f *recordingWebView) CanGoBack() bool { return false }
2510-func (f *fakeRecordingWebView) CanGoForward() bool { return false }2510+func (f *recordingWebView) CanGoForward() bool { return false }
2511 2511
2512-func (f *fakeRecordingWebView) SetZoomLevel(_ context.Context, level float64) error {2512+func (f *recordingWebView) SetZoomLevel(_ context.Context, level float64) error {
2513 f.setZoomLevelCalls++2513 f.setZoomLevelCalls++
2514 f.setZoomLevelLastLevel = level2514 f.setZoomLevelLastLevel = level
2515 return nil2515 return nil
2516 }2516 }
2517 2517
2518-func (f *fakeRecordingWebView) OpenDevTools() { f.openDevToolsCalls++ }2518+func (f *recordingWebView) OpenDevTools() { f.openDevToolsCalls++ }
2519-func (f *fakeRecordingWebView) PrintPage() { f.printPageCalls++ }2519+func (f *recordingWebView) PrintPage() { f.printPageCalls++ }
2520 2520
2521-func (f *fakeRecordingWebView) GetZoomLevel() float64 { return 1.0 }2521+func (f *recordingWebView) GetZoomLevel() float64 { return 1.0 }
2522-func (f *fakeRecordingWebView) GetFindController() port.FindController { return nil }2522+func (f *recordingWebView) GetFindController() port.FindController { return nil }
2523-func (f *fakeRecordingWebView) SetCallbacks(_ *port.WebViewCallbacks) {}2523+func (f *recordingWebView) SetCallbacks(_ *port.WebViewCallbacks) {}
2524-func (f *fakeRecordingWebView) RunJavaScript(_ context.Context, _ string) {}2524+func (f *recordingWebView) RunJavaScript(_ context.Context, _ string) {}
2525-func (f *fakeRecordingWebView) SetBackgroundColor(_, _, _, _ float64) {}2525+func (f *recordingWebView) SetBackgroundColor(_, _, _, _ float64) {}
2526-func (f *fakeRecordingWebView) ResetBackgroundToDefault() {}2526+func (f *recordingWebView) ResetBackgroundToDefault() {}
2527-func (f *fakeRecordingWebView) Favicon() port.Texture { return nil }2527+func (f *recordingWebView) Favicon() port.Texture { return nil }
2528-func (f *fakeRecordingWebView) Generation() uint64 { return 0 }2528+func (f *recordingWebView) Generation() uint64 { return 0 }
2529-func (f *fakeRecordingWebView) IsFullscreen() bool { return false }2529+func (f *recordingWebView) IsFullscreen() bool { return false }
2530-func (f *fakeRecordingWebView) IsPlayingAudio() bool { return false }2530+func (f *recordingWebView) IsPlayingAudio() bool { return false }
2531-func (f *fakeRecordingWebView) IsDestroyed() bool { return f.destroyCalls > 0 }2531+func (f *recordingWebView) IsDestroyed() bool { return f.destroyCalls > 0 }
2532-func (f *fakeRecordingWebView) Destroy() { f.destroyCalls++ }2532+func (f *recordingWebView) Destroy() { f.destroyCalls++ }
2533 2533
2534 func TestApp_AttachPopupToTabDestroysPopupWhenWorkspaceViewMissing(t *testing.T) {2534 func TestApp_AttachPopupToTabDestroysPopupWhenWorkspaceViewMissing(t *testing.T) {
2535 ctx := context.Background()2535 ctx := context.Background()
2536- popupWV := &fakeRecordingWebView{id: 1}2536+ popupWV := &recordingWebView{id: 1}
2537 app := &App{workspaceViews: map[entity.TabID]*component.WorkspaceView{}}2537 app := &App{workspaceViews: map[entity.TabID]*component.WorkspaceView{}}
2538 2538
2539 app.attachPopupToTab(ctx, entity.TabID("missing-tab"), entity.NewPane(entity.PaneID("popup-pane")), popupWV)2539 app.attachPopupToTab(ctx, entity.TabID("missing-tab"), entity.NewPane(entity.PaneID("popup-pane")), popupWV)
@@ -2545,7 +2545,7 @@ func TestApp_AttachPopupToTabDestroysPopupWhenWorkspaceViewMissing(t *testing.T)
2545 2545
2546 func TestApp_AttachPopupToTabDestroysPopupWhenPaneNil(t *testing.T) {2546 func TestApp_AttachPopupToTabDestroysPopupWhenPaneNil(t *testing.T) {
2547 ctx := context.Background()2547 ctx := context.Background()
2548- popupWV := &fakeRecordingWebView{id: 1}2548+ popupWV := &recordingWebView{id: 1}
2549 tabID := entity.TabID("tab-1")2549 tabID := entity.TabID("tab-1")
2550 app := &App{workspaceViews: map[entity.TabID]*component.WorkspaceView{tabID: &component.WorkspaceView{}}}2550 app := &App{workspaceViews: map[entity.TabID]*component.WorkspaceView{tabID: &component.WorkspaceView{}}}
2551 2551
@@ -2568,7 +2568,7 @@ func TestApp_AttachPopupToTabSkipsRegistrationWhenPaneViewMissing(t *testing.T)
2568 },2568 },
2569 }2569 }
2570 2570
2571- popupWV := &fakeRecordingWebView{id: 1}2571+ popupWV := &recordingWebView{id: 1}
2572 app.attachPopupToTab(ctx, tabID, pane, popupWV)2572 app.attachPopupToTab(ctx, tabID, pane, popupWV)
2573 2573
2574 if got := contentCoord.GetWebView(pane.ID); got != nil {2574 if got := contentCoord.GetWebView(pane.ID); got != nil {
@@ -2608,7 +2608,7 @@ func TestApp_AttachPopupToTabReleasesRegistrationWhenWrapFails(t *testing.T) {
2608 tabID: wsView,2608 tabID: wsView,
2609 },2609 },
2610 }2610 }
2611- popupWV := &fakeRecordingWebView{id: 1}2611+ popupWV := &recordingWebView{id: 1}
2612 2612
2613 app.attachPopupToTab(ctx, tabID, pane, popupWV)2613 app.attachPopupToTab(ctx, tabID, pane, popupWV)
2614 2614
@@ -2637,15 +2637,15 @@ func TestApp_BrowserWindowWebViewActionsIgnoreStaleFocusedWindow(t *testing.T) {
2637 secondTabs.SetActive(tab2.ID)2637 secondTabs.SetActive(tab2.ID)
2638 second := &browserWindow{id: "window-2", tabs: secondTabs}2638 second := &browserWindow{id: "window-2", tabs: secondTabs}
2639 2639
2640- // Create fake recording webviews.2640+ // Create recording webviews.
2641- fakeWv1 := &fakeRecordingWebView{id: 1}2641+ recordingWv1 := &recordingWebView{id: 1}
2642- fakeWv2 := &fakeRecordingWebView{id: 2}2642+ recordingWv2 := &recordingWebView{id: 2}
2643 2643
2644 // Create content coordinator with initialized internal maps to avoid2644 // Create content coordinator with initialized internal maps to avoid
2645 // nil-map panics in SetNavigationOrigin during NavigateWebView calls.2645 // nil-map panics in SetNavigationOrigin during NavigateWebView calls.
2646 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)2646 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)
2647- contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), fakeWv1)2647+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), recordingWv1)
2648- contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), fakeWv2)2648+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), recordingWv2)
2649 2649
2650 // Create NavigationCoordinator (no navigateUC needed for direct webview calls).2650 // Create NavigationCoordinator (no navigateUC needed for direct webview calls).
2651 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)2651 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)
@@ -2684,11 +2684,11 @@ func TestApp_BrowserWindowWebViewActionsIgnoreStaleFocusedWindow(t *testing.T) {
2684 t.Fatalf("goForwardBrowserWindow returned error: %v", err)2684 t.Fatalf("goForwardBrowserWindow returned error: %v", err)
2685 }2685 }
2686 2686
2687- assertRecordingWebViewIdle(t, "first", fakeWv1)2687+ assertRecordingWebViewIdle(t, "first", recordingWv1)
2688- assertRecordingWebViewActions(t, "second", fakeWv2, "https://google.com")2688+ assertRecordingWebViewActions(t, "second", recordingWv2, "https://google.com")
2689 }2689 }
2690 2690
2691-func assertRecordingWebViewIdle(t *testing.T, name string, wv *fakeRecordingWebView) {2691+func assertRecordingWebViewIdle(t *testing.T, name string, wv *recordingWebView) {
2692 t.Helper()2692 t.Helper()
2693 if wv.loadURICalled || wv.loadURILastURI != "" {2693 if wv.loadURICalled || wv.loadURILastURI != "" {
2694 t.Errorf("%s webview LoadURI was called, expected zero calls", name)2694 t.Errorf("%s webview LoadURI was called, expected zero calls", name)
@@ -2710,7 +2710,7 @@ func assertRecordingWebViewIdle(t *testing.T, name string, wv *fakeRecordingWebV
2710 }2710 }
2711 }2711 }
2712 2712
2713-func assertRecordingWebViewActions(t *testing.T, name string, wv *fakeRecordingWebView, wantURI string) {2713+func assertRecordingWebViewActions(t *testing.T, name string, wv *recordingWebView, wantURI string) {
2714 t.Helper()2714 t.Helper()
2715 if !wv.loadURICalled {2715 if !wv.loadURICalled {
2716 t.Errorf("%s webview LoadURI was not called", name)2716 t.Errorf("%s webview LoadURI was not called", name)
@@ -2735,7 +2735,7 @@ func assertRecordingWebViewActions(t *testing.T, name string, wv *fakeRecordingW
2735 }2735 }
2736 }2736 }
2737 2737
2738-func assertRecordingWebViewBrowserActionsIdle(t *testing.T, name string, wv *fakeRecordingWebView) {2738+func assertRecordingWebViewBrowserActionsIdle(t *testing.T, name string, wv *recordingWebView) {
2739 t.Helper()2739 t.Helper()
2740 if wv.reloadCalls > 0 {2740 if wv.reloadCalls > 0 {
2741 t.Errorf("%s webview Reload calls = %d, want 0", name, wv.reloadCalls)2741 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
2763 }2763 }
2764 }2764 }
2765 2765
2766-func assertRecordingWebViewBrowserActionsCalledOnce(t *testing.T, name string, wv *fakeRecordingWebView) {2766+func assertRecordingWebViewBrowserActionsCalledOnce(t *testing.T, name string, wv *recordingWebView) {
2767 t.Helper()2767 t.Helper()
2768 if wv.reloadCalls != 1 {2768 if wv.reloadCalls != 1 {
2769 t.Errorf("%s webview Reload calls = %d, want 1", name, wv.reloadCalls)2769 t.Errorf("%s webview Reload calls = %d, want 1", name, wv.reloadCalls)
@@ -2837,14 +2837,14 @@ func TestApp_DispatchBrowserWindowActionUsesSourceWindow(t *testing.T) {
2837 secondTabs.SetActive(tab2.ID)2837 secondTabs.SetActive(tab2.ID)
2838 second := &browserWindow{id: "window-2", tabs: secondTabs}2838 second := &browserWindow{id: "window-2", tabs: secondTabs}
2839 2839
2840- // Create fake recording webviews.2840+ // Create recording webviews.
2841- fakeWv1 := &fakeRecordingWebView{id: 1, loadURILastURI: "https://first.example"}2841+ recordingWv1 := &recordingWebView{id: 1, loadURILastURI: "https://first.example"}
2842- fakeWv2 := &fakeRecordingWebView{id: 2, loadURILastURI: "https://second.example"}2842+ recordingWv2 := &recordingWebView{id: 2, loadURILastURI: "https://second.example"}
2843 2843
2844- // Register fake webviews in content coordinator.2844+ // Register recording webviews in content coordinator.
2845 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)2845 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)
2846- contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), fakeWv1)2846+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), recordingWv1)
2847- contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), fakeWv2)2847+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), recordingWv2)
2848 2848
2849 // Create navCoord (no navigateUC needed for direct webview calls).2849 // Create navCoord (no navigateUC needed for direct webview calls).
2850 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)2850 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)
@@ -2880,11 +2880,11 @@ func TestApp_DispatchBrowserWindowActionUsesSourceWindow(t *testing.T) {
2880 }2880 }
2881 }2881 }
2882 2882
2883- // Assert first fake webview receives zero calls (stale focus is ignored).2883+ // Assert first recording webview receives zero calls (stale focus is ignored).
2884- assertRecordingWebViewBrowserActionsIdle(t, "first", fakeWv1)2884+ assertRecordingWebViewBrowserActionsIdle(t, "first", recordingWv1)
2885 2885
2886- // Assert second fake webview receives exactly one call per action.2886+ // Assert second recording webview receives exactly one call per action.
2887- assertRecordingWebViewBrowserActionsCalledOnce(t, "second", fakeWv2)2887+ assertRecordingWebViewBrowserActionsCalledOnce(t, "second", recordingWv2)
2888 }2888 }
2889 2889
2890 func TestApp_DispatchBrowserWindowActionZoomInSupportsFileURLs(t *testing.T) {2890 func TestApp_DispatchBrowserWindowActionZoomInSupportsFileURLs(t *testing.T) {
@@ -2896,9 +2896,9 @@ func TestApp_DispatchBrowserWindowActionZoomInSupportsFileURLs(t *testing.T) {
2896 tabs.SetActive(tab.ID)2896 tabs.SetActive(tab.ID)
2897 bw := &browserWindow{id: "window-1", tabs: tabs}2897 bw := &browserWindow{id: "window-1", tabs: tabs}
2898 2898
2899- fakeWv := &fakeRecordingWebView{id: 1, loadURILastURI: "file:///tmp/demo.html"}2899+ recordingWv := &recordingWebView{id: 1, loadURILastURI: "file:///tmp/demo.html"}
2900 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)2900 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)
2902 2902
2903 app := &App{2903 app := &App{
2904 browserWindows: map[string]*browserWindow{bw.id: bw},2904 browserWindows: map[string]*browserWindow{bw.id: bw},
@@ -2916,11 +2916,11 @@ func TestApp_DispatchBrowserWindowActionZoomInSupportsFileURLs(t *testing.T) {
2916 if err := app.dispatchBrowserWindowAction(ctx, bw, input.ActionZoomIn); err != nil {2916 if err := app.dispatchBrowserWindowAction(ctx, bw, input.ActionZoomIn); err != nil {
2917 t.Fatalf("dispatchBrowserWindowAction returned error: %v", err)2917 t.Fatalf("dispatchBrowserWindowAction returned error: %v", err)
2918 }2918 }
2919- if fakeWv.setZoomLevelCalls != 1 {2919+ if recordingWv.setZoomLevelCalls != 1 {
2920- t.Fatalf("webview SetZoomLevel calls = %d, want 1", fakeWv.setZoomLevelCalls)2920+ t.Fatalf("webview SetZoomLevel calls = %d, want 1", recordingWv.setZoomLevelCalls)
2921 }2921 }
2922- if diff := math.Abs(fakeWv.setZoomLevelLastLevel - 1.1); diff > 0.0001 {2922+ if diff := math.Abs(recordingWv.setZoomLevelLastLevel - 1.1); diff > 0.0001 {
2923- t.Fatalf("webview SetZoomLevel level = %f, want 1.1", fakeWv.setZoomLevelLastLevel)2923+ t.Fatalf("webview SetZoomLevel level = %f, want 1.1", recordingWv.setZoomLevelLastLevel)
2924 }2924 }
2925 }2925 }
2926 2926
@@ -3161,11 +3161,11 @@ func TestApp_WorkspaceOmniboxNavigateUsesOwnerWindow(t *testing.T) {
3161 first := &browserWindow{id: "window-1", tabs: firstTabs}3161 first := &browserWindow{id: "window-1", tabs: firstTabs}
3162 second := &browserWindow{id: "window-2", tabs: secondTabs}3162 second := &browserWindow{id: "window-2", tabs: secondTabs}
3163 3163
3164- fakeWv1 := &fakeRecordingWebView{id: 1}3164+ recordingWv1 := &recordingWebView{id: 1}
3165- fakeWv2 := &fakeRecordingWebView{id: 2}3165+ recordingWv2 := &recordingWebView{id: 2}
3166 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)3166 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)
3167- contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), fakeWv1)3167+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), recordingWv1)
3168- contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), fakeWv2)3168+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), recordingWv2)
3169 3169
3170 app := &App{3170 app := &App{
3171 deps: &Dependencies{Config: &config.Config{}},3171 deps: &Dependencies{Config: &config.Config{}},
@@ -3189,11 +3189,11 @@ func TestApp_WorkspaceOmniboxNavigateUsesOwnerWindow(t *testing.T) {
3189 t.Fatalf("omnibox navigate callback returned error: %v", err)3189 t.Fatalf("omnibox navigate callback returned error: %v", err)
3190 }3190 }
3191 3191
3192- if fakeWv1.loadURICalled {3192+ if recordingWv1.loadURICalled {
3193- t.Errorf("first window webview navigated to %q, want no navigation", fakeWv1.loadURILastURI)3193+ t.Errorf("first window webview navigated to %q, want no navigation", recordingWv1.loadURILastURI)
3194 }3194 }
3195- if !fakeWv2.loadURICalled || fakeWv2.loadURILastURI != "https://example.com" {3195+ if !recordingWv2.loadURICalled || recordingWv2.loadURILastURI != "https://example.com" {
3196- t.Errorf("second window webview navigation = called %v uri %q, want https://example.com", fakeWv2.loadURICalled, fakeWv2.loadURILastURI)3196+ t.Errorf("second window webview navigation = called %v uri %q, want https://example.com", recordingWv2.loadURICalled, recordingWv2.loadURILastURI)
3197 }3197 }
3198 }3198 }
3199 3199
M internal/ui/browser_window_history_sidebar_test.go +38 -38
@@ -41,13 +41,13 @@ func TestHistorySidebarConfig_OnNavigateNavigatesActivePaneAndKeepsSidebar(t *te
41 secondTabs.SetActive(tab2.ID)41 secondTabs.SetActive(tab2.ID)
42 second := &browserWindow{id: "window-2", tabs: secondTabs}42 second := &browserWindow{id: "window-2", tabs: secondTabs}
43 43
44- // Create fake webviews, one per pane.44+ // Create recording webviews, one per pane.
45- fakeWv1 := &fakeRecordingWebView{id: 1}45+ recordingWv1 := &recordingWebView{id: 1}
46- fakeWv2 := &fakeRecordingWebView{id: 2}46+ recordingWv2 := &recordingWebView{id: 2}
47 47
48 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)48 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)
49- contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), fakeWv1)49+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), recordingWv1)
50- contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), fakeWv2)50+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), recordingWv2)
51 51
52 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)52 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)
53 53
@@ -75,12 +75,12 @@ func TestHistorySidebarConfig_OnNavigateNavigatesActivePaneAndKeepsSidebar(t *te
75 require.NoError(t, err, "OnNavigate should succeed")75 require.NoError(t, err, "OnNavigate should succeed")
76 76
77 // The second window's webview must have received the navigation.77 // The second window's webview must have received the navigation.
78- assert.True(t, fakeWv2.loadURICalled, "second window webview should receive navigation")78+ assert.True(t, recordingWv2.loadURICalled, "second window webview should receive navigation")
79- assert.Equal(t, navigateURL, fakeWv2.loadURILastURI)79+ assert.Equal(t, navigateURL, recordingWv2.loadURILastURI)
80 assert.True(t, second.sidebarVisible, "default history activation should keep the sidebar visible")80 assert.True(t, second.sidebarVisible, "default history activation should keep the sidebar visible")
81 81
82 // The first window's webview must NOT have been touched (stale-focus guard).82 // 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")
84 }84 }
85 85
86 // TestHistorySidebarConfig_OnNavigateKeepOpenNavigatesWithoutClosing verifies86 // TestHistorySidebarConfig_OnNavigateKeepOpenNavigatesWithoutClosing verifies
@@ -103,8 +103,8 @@ func TestHistorySidebarConfig_OnNavigateKeepOpenNavigatesWithoutClosing(t *testi
103 }103 }
104 104
105 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)105 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)
106- fakeWv := &fakeRecordingWebView{id: 1}106+ recordingWv := &recordingWebView{id: 1}
107- contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), fakeWv)107+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), recordingWv)
108 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)108 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)
109 109
110 app := &App{110 app := &App{
@@ -122,8 +122,8 @@ func TestHistorySidebarConfig_OnNavigateKeepOpenNavigatesWithoutClosing(t *testi
122 navigateURL := "https://keep-open.com"122 navigateURL := "https://keep-open.com"
123 err := cfg.OnNavigateKeepOpen(ctx, navigateURL)123 err := cfg.OnNavigateKeepOpen(ctx, navigateURL)
124 require.NoError(t, err)124 require.NoError(t, err)
125- assert.True(t, fakeWv.loadURICalled)125+ assert.True(t, recordingWv.loadURICalled)
126- assert.Equal(t, navigateURL, fakeWv.loadURILastURI, "Ctrl+Enter navigation should go to the URL")126+ assert.Equal(t, navigateURL, recordingWv.loadURILastURI, "Ctrl+Enter navigation should go to the URL")
127 assert.True(t, bw.sidebarVisible, "Ctrl+Enter navigation should keep the sidebar visible")127 assert.True(t, bw.sidebarVisible, "Ctrl+Enter navigation should keep the sidebar visible")
128 }128 }
129 129
@@ -148,12 +148,12 @@ func TestHistorySidebar_OwnershipOnMultiWindowNavigation(t *testing.T) {
148 secondTabs.SetActive(tab2.ID)148 secondTabs.SetActive(tab2.ID)
149 second := &browserWindow{id: "window-2", tabs: secondTabs}149 second := &browserWindow{id: "window-2", tabs: secondTabs}
150 150
151- fakeWv1 := &fakeRecordingWebView{id: 1}151+ recordingWv1 := &recordingWebView{id: 1}
152- fakeWv2 := &fakeRecordingWebView{id: 2}152+ recordingWv2 := &recordingWebView{id: 2}
153 153
154 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)154 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)
155- contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), fakeWv1)155+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), recordingWv1)
156- contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), fakeWv2)156+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), recordingWv2)
157 157
158 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)158 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)
159 159
@@ -177,11 +177,11 @@ func TestHistorySidebar_OwnershipOnMultiWindowNavigation(t *testing.T) {
177 require.NoError(t, err)177 require.NoError(t, err)
178 178
179 // Second window's webview must receive the navigation.179 // Second window's webview must receive the navigation.
180- assert.True(t, fakeWv2.loadURICalled, "second window webview should receive navigation")180+ assert.True(t, recordingWv2.loadURICalled, "second window webview should receive navigation")
181- assert.Equal(t, "https://second-window.com", fakeWv2.loadURILastURI)181+ assert.Equal(t, "https://second-window.com", recordingWv2.loadURILastURI)
182 182
183 // First window's webview must NOT have been touched.183 // 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")
185 }185 }
186 186
187 // =============================================================================187 // =============================================================================
@@ -509,9 +509,9 @@ func TestApp_HistorySidebarConfig_NavigateCallbackNavigates(t *testing.T) {
509 sidebarVisible: true,509 sidebarVisible: true,
510 }510 }
511 511
512- fakeWv := &fakeRecordingWebView{id: 1}512+ recordingWv := &recordingWebView{id: 1}
513 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)513 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)
514- contentCoord.RegisterPopupWebView(paneID, fakeWv)514+ contentCoord.RegisterPopupWebView(paneID, recordingWv)
515 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)515 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)
516 516
517 app := &App{517 app := &App{
@@ -535,8 +535,8 @@ func TestApp_HistorySidebarConfig_NavigateCallbackNavigates(t *testing.T) {
535 require.NoError(t, err)535 require.NoError(t, err)
536 536
537 // Verify the navigation reached the correct webview.537 // Verify the navigation reached the correct webview.
538- assert.True(t, fakeWv.loadURICalled, "webview must receive navigation")538+ assert.True(t, recordingWv.loadURICalled, "webview must receive navigation")
539- assert.Equal(t, navigateURL, fakeWv.loadURILastURI)539+ assert.Equal(t, navigateURL, recordingWv.loadURILastURI)
540 assert.True(t, bw.sidebarVisible, "default history navigation should keep the sidebar open")540 assert.True(t, bw.sidebarVisible, "default history navigation should keep the sidebar open")
541 }541 }
542 542
@@ -557,9 +557,9 @@ func TestApp_NavigateHistorySidebarSelection_KeepsSidebarVisible(t *testing.T) {
557 sidebarVisible: true,557 sidebarVisible: true,
558 }558 }
559 559
560- fakeWv := &fakeRecordingWebView{id: 1}560+ recordingWv := &recordingWebView{id: 1}
561 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)561 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)
562- contentCoord.RegisterPopupWebView(paneID, fakeWv)562+ contentCoord.RegisterPopupWebView(paneID, recordingWv)
563 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)563 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)
564 564
565 app := &App{565 app := &App{
@@ -575,8 +575,8 @@ func TestApp_NavigateHistorySidebarSelection_KeepsSidebarVisible(t *testing.T) {
575 575
576 err := app.navigateHistorySidebarSelection(ctx, bw, "https://open.com")576 err := app.navigateHistorySidebarSelection(ctx, bw, "https://open.com")
577 require.NoError(t, err)577 require.NoError(t, err)
578- assert.True(t, fakeWv.loadURICalled)578+ assert.True(t, recordingWv.loadURICalled)
579- assert.Equal(t, "https://open.com", fakeWv.loadURILastURI)579+ assert.Equal(t, "https://open.com", recordingWv.loadURILastURI)
580 assert.True(t, bw.sidebarVisible, "history selection navigation should not hide the sidebar")580 assert.True(t, bw.sidebarVisible, "history selection navigation should not hide the sidebar")
581 }581 }
582 582
@@ -599,12 +599,12 @@ func TestApp_HistorySidebarConfig_NavigateCallbackOwnership(t *testing.T) {
599 secondTabs.SetActive(tab2.ID)599 secondTabs.SetActive(tab2.ID)
600 second := &browserWindow{id: "window-2", tabs: secondTabs}600 second := &browserWindow{id: "window-2", tabs: secondTabs}
601 601
602- fakeWv1 := &fakeRecordingWebView{id: 1}602+ recordingWv1 := &recordingWebView{id: 1}
603- fakeWv2 := &fakeRecordingWebView{id: 2}603+ recordingWv2 := &recordingWebView{id: 2}
604 604
605 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)605 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)
606- contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), fakeWv1)606+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-1"), recordingWv1)
607- contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), fakeWv2)607+ contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), recordingWv2)
608 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)608 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)
609 609
610 app := &App{610 app := &App{
@@ -629,11 +629,11 @@ func TestApp_HistorySidebarConfig_NavigateCallbackOwnership(t *testing.T) {
629 require.NoError(t, err)629 require.NoError(t, err)
630 630
631 // Second window's webview must receive navigation.631 // Second window's webview must receive navigation.
632- assert.True(t, fakeWv2.loadURICalled, "second window webview must receive navigation")632+ assert.True(t, recordingWv2.loadURICalled, "second window webview must receive navigation")
633- assert.Equal(t, "https://ownership.com", fakeWv2.loadURILastURI)633+ assert.Equal(t, "https://ownership.com", recordingWv2.loadURILastURI)
634 634
635 // First window's webview must NOT be touched.635 // 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")
637 }637 }
638 638
639 // TestApp_HistorySidebarConfig_KeepOpenCallback verifies that639 // TestApp_HistorySidebarConfig_KeepOpenCallback verifies that
@@ -655,9 +655,9 @@ func TestApp_HistorySidebarConfig_KeepOpenCallback(t *testing.T) {
655 sidebarVisible: true,655 sidebarVisible: true,
656 }656 }
657 657
658- fakeWv := &fakeRecordingWebView{id: 1}658+ recordingWv := &recordingWebView{id: 1}
659 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)659 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)
660- contentCoord.RegisterPopupWebView(paneID, fakeWv)660+ contentCoord.RegisterPopupWebView(paneID, recordingWv)
661 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)661 navCoord := coordinator.NewNavigationCoordinator(ctx, nil, contentCoord)
662 662
663 app := &App{663 app := &App{
@@ -678,8 +678,8 @@ func TestApp_HistorySidebarConfig_KeepOpenCallback(t *testing.T) {
678 err := cfg.OnNavigateKeepOpen(ctx, navigateURL)678 err := cfg.OnNavigateKeepOpen(ctx, navigateURL)
679 require.NoError(t, err)679 require.NoError(t, err)
680 680
681- assert.True(t, fakeWv.loadURICalled, "webview must receive navigation")681+ assert.True(t, recordingWv.loadURICalled, "webview must receive navigation")
682- assert.Equal(t, navigateURL, fakeWv.loadURILastURI)682+ assert.Equal(t, navigateURL, recordingWv.loadURILastURI)
683 683
684 // Sidebar must remain visible (keep-open contract).684 // Sidebar must remain visible (keep-open contract).
685 assert.True(t, bw.sidebarVisible, "sidebar must stay visible after keep-open navigation")685 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) {
130 130
131 pool := &recordingWebViewPool{}131 pool := &recordingWebViewPool{}
132 contentCoord := contentcoord.NewCoordinator(ctx, pool, nil, nil, nil, nil, nil, nil)132 contentCoord := contentcoord.NewCoordinator(ctx, pool, nil, nil, nil, nil, nil, nil)
133- closedWV := &fakeRecordingWebView{id: 101}133+ closedWV := &recordingWebView{id: 101}
134- survivingWV := &fakeRecordingWebView{id: 102}134+ survivingWV := &recordingWebView{id: 102}
135 contentCoord.RegisterPopupWebView(closedTab.Workspace.ActivePaneID, closedWV)135 contentCoord.RegisterPopupWebView(closedTab.Workspace.ActivePaneID, closedWV)
136 contentCoord.RegisterPopupWebView(survivingTab.Workspace.ActivePaneID, survivingWV)136 contentCoord.RegisterPopupWebView(survivingTab.Workspace.ActivePaneID, survivingWV)
137 137
@@ -173,8 +173,8 @@ func TestTabCoordinator_SwitchDoesNotReleaseWorkspaceWebViews(t *testing.T) {
173 173
174 pool := &recordingWebViewPool{}174 pool := &recordingWebViewPool{}
175 contentCoord := contentcoord.NewCoordinator(ctx, pool, nil, nil, nil, nil, nil, nil)175 contentCoord := contentcoord.NewCoordinator(ctx, pool, nil, nil, nil, nil, nil, nil)
176- firstWV := &fakeRecordingWebView{id: 111}176+ firstWV := &recordingWebView{id: 111}
177- secondWV := &fakeRecordingWebView{id: 112}177+ secondWV := &recordingWebView{id: 112}
178 contentCoord.RegisterPopupWebView(firstTab.Workspace.ActivePaneID, firstWV)178 contentCoord.RegisterPopupWebView(firstTab.Workspace.ActivePaneID, firstWV)
179 contentCoord.RegisterPopupWebView(secondTab.Workspace.ActivePaneID, secondWV)179 contentCoord.RegisterPopupWebView(secondTab.Workspace.ActivePaneID, secondWV)
180 180
@@ -209,7 +209,7 @@ func TestTabCoordinator_CloseLastTabReleasesWorkspaceBeforeWindowRemoval(t *test
209 209
210 pool := &recordingWebViewPool{}210 pool := &recordingWebViewPool{}
211 contentCoord := contentcoord.NewCoordinator(ctx, pool, nil, nil, nil, nil, nil, nil)211 contentCoord := contentcoord.NewCoordinator(ctx, pool, nil, nil, nil, nil, nil, nil)
212- closedWV := &fakeRecordingWebView{id: 121}212+ closedWV := &recordingWebView{id: 121}
213 contentCoord.RegisterPopupWebView(closedTab.Workspace.ActivePaneID, closedWV)213 contentCoord.RegisterPopupWebView(closedTab.Workspace.ActivePaneID, closedWV)
214 214
215 bw := &browserWindow{id: "window-1", tabs: tabs}215 bw := &browserWindow{id: "window-1", tabs: tabs}
@@ -247,8 +247,8 @@ func TestBrowserWindow_RemoveBrowserWindowReleasesOwnedTabWorkspaceWebViews(t *t
247 247
248 pool := &recordingWebViewPool{}248 pool := &recordingWebViewPool{}
249 contentCoord := contentcoord.NewCoordinator(ctx, pool, nil, nil, nil, nil, nil, nil)249 contentCoord := contentcoord.NewCoordinator(ctx, pool, nil, nil, nil, nil, nil, nil)
250- ownedWV := &fakeRecordingWebView{id: 201}250+ ownedWV := &recordingWebView{id: 201}
251- otherWV := &fakeRecordingWebView{id: 202}251+ otherWV := &recordingWebView{id: 202}
252 contentCoord.RegisterPopupWebView(ownedTab.Workspace.ActivePaneID, ownedWV)252 contentCoord.RegisterPopupWebView(ownedTab.Workspace.ActivePaneID, ownedWV)
253 contentCoord.RegisterPopupWebView(otherTab.Workspace.ActivePaneID, otherWV)253 contentCoord.RegisterPopupWebView(otherTab.Workspace.ActivePaneID, otherWV)
254 254
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 (
4 "context"4 "context"
5 "testing"5 "testing"
6 6
7- "github.com/stretchr/testify/require"7+ coordinatormocks "github.com/bnema/dumber/internal/ui/coordinator/mocks"
8+ "github.com/stretchr/testify/mock"
8 )9 )
9 10
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-
21 func TestNavigationCoordinator_OmniboxProviderOpenAndZoom(t *testing.T) {11 func TestNavigationCoordinator_OmniboxProviderOpenAndZoom(t *testing.T) {
22 c := &NavigationCoordinator{}12 c := &NavigationCoordinator{}
23- provider := &fakeOmniboxProvider{}13+ provider := coordinatormocks.NewMockOmniboxProvider(t)
14+ provider.EXPECT().ToggleOmnibox(mock.Anything).Once()
15+ provider.EXPECT().UpdateOmniboxZoom(1.25).Once()
24 16
25 c.SetOmniboxProvider(provider)17 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+ }
27 c.NotifyZoomChanged(context.Background(), 1.25)21 c.NotifyZoomChanged(context.Background(), 1.25)
28-
29- require.True(t, provider.toggled)
30- require.Equal(t, []float64{1.25}, provider.zoomUpdates)
31 }22 }
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 (
6 6
7 "github.com/bnema/dumber/internal/application/port"7 "github.com/bnema/dumber/internal/application/port"
8 "github.com/bnema/dumber/internal/domain/entity"8 "github.com/bnema/dumber/internal/domain/entity"
9+ dialogmocks "github.com/bnema/dumber/internal/ui/dialog/mocks"
9 "github.com/stretchr/testify/assert"10 "github.com/stretchr/testify/assert"
11+ "github.com/stretchr/testify/mock"
10 )12 )
11 13
12-type fakePermissionPopup struct {14+type permissionPopupShowCall struct {
13- showCalls []fakePopupShowCall
14- callback func(allowed, persistent bool)
15-}
16-
17-type fakePopupShowCall struct {
18 heading string15 heading string
19 body string16 body string
20 }17 }
21 18
22-func (f *fakePermissionPopup) Show(19+func TestPermissionDialog_QueuesRequestsWhilePopupVisible(t *testing.T) {
23- _ context.Context,20+ popup := dialogmocks.NewMockPermissionPopup(t)
24- heading string,21+ var showCalls []permissionPopupShowCall
25- body string,22+ var callback func(allowed, persistent bool)
26- callback func(allowed, persistent bool),23+ popup.EXPECT().
27-) {24+ Show(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
28- f.showCalls = append(f.showCalls, fakePopupShowCall{25+ Run(func(_ context.Context, heading string, body string, cb func(allowed, persistent bool)) {
29- heading: heading,26+ showCalls = append(showCalls, permissionPopupShowCall{
30- body: body,27+ heading: heading,
31- })28+ body: body,
32- f.callback = callback29+ })
33-}30+ callback = cb
34-31+ }).
35-func (f *fakePermissionPopup) Respond(allowed, persistent bool) {32+ Twice()
36- if f.callback == nil {33+
37- return34+ respond := func(allowed, persistent bool) {
35+ if callback == nil {
36+ return
37+ }
38+ cb := callback
39+ callback = nil
40+ cb(allowed, persistent)
38 }41 }
39- cb := f.callback
40- f.callback = nil
41- cb(allowed, persistent)
42-}
43 42
44-func TestPermissionDialog_QueuesRequestsWhilePopupVisible(t *testing.T) {
45- popup := &fakePermissionPopup{}
46 d := &PermissionDialog{popup: popup}43 d := &PermissionDialog{popup: popup}
47 44
48 var firstResult port.PermissionDialogResult45 var firstResult port.PermissionDialogResult
@@ -71,22 +68,22 @@ func TestPermissionDialog_QueuesRequestsWhilePopupVisible(t *testing.T) {
71 },68 },
72 )69 )
73 70
74- if assert.Len(t, popup.showCalls, 1) {71+ if assert.Len(t, showCalls, 1) {
75- assert.Equal(t, "Allow Microphone Access?", popup.showCalls[0].heading)72+ assert.Equal(t, "Allow Microphone Access?", showCalls[0].heading)
76 }73 }
77 assert.False(t, firstCalled)74 assert.False(t, firstCalled)
78 assert.False(t, secondCalled)75 assert.False(t, secondCalled)
79 76
80- popup.Respond(true, false)77+ respond(true, false)
81 assert.True(t, firstCalled)78 assert.True(t, firstCalled)
82 assert.Equal(t, port.PermissionDialogResult{Allowed: true, Persistent: false}, firstResult)79 assert.Equal(t, port.PermissionDialogResult{Allowed: true, Persistent: false}, firstResult)
83 80
84- if assert.Len(t, popup.showCalls, 2) {81+ if assert.Len(t, showCalls, 2) {
85- assert.Equal(t, "Allow Camera Access?", popup.showCalls[1].heading)82+ assert.Equal(t, "Allow Camera Access?", showCalls[1].heading)
86 }83 }
87 assert.False(t, secondCalled)84 assert.False(t, secondCalled)
88 85
89- popup.Respond(false, true)86+ respond(false, true)
90 assert.True(t, secondCalled)87 assert.True(t, secondCalled)
91 assert.Equal(t, port.PermissionDialogResult{Allowed: false, Persistent: true}, secondResult)88 assert.Equal(t, port.PermissionDialogResult{Allowed: false, Persistent: true}, secondResult)
92 }89 }
M internal/ui/systemviews/app_test.go +98 -98
@@ -48,7 +48,7 @@ func TestParseRoute(t *testing.T) {
48 func TestAppRunMountsPlaceholderAndRecordsRoute(t *testing.T) {48 func TestAppRunMountsPlaceholderAndRecordsRoute(t *testing.T) {
49 t.Parallel()49 t.Parallel()
50 50
51- dom := &fakeDOM{}51+ dom := &recordingDOM{}
52 app := NewApp(Dependencies{52 app := NewApp(Dependencies{
53 DOM: dom,53 DOM: dom,
54 LocationURI: "dumb://history",54 LocationURI: "dumb://history",
@@ -64,7 +64,7 @@ func TestAppRunMountsPlaceholderAndRecordsRoute(t *testing.T) {
64 func TestAppLoadInitialHistoryRouteUsesStyledSections(t *testing.T) {64 func TestAppLoadInitialHistoryRouteUsesStyledSections(t *testing.T) {
65 t.Parallel()65 t.Parallel()
66 66
67- history := &fakeHistoryService{entries: []*entity.HistoryEntry{{67+ history := &recordingHistoryService{entries: []*entity.HistoryEntry{{
68 URL: "https://example.com",68 URL: "https://example.com",
69 Title: "Example",69 Title: "Example",
70 }}}70 }}}
@@ -98,7 +98,7 @@ func TestAppLoadInitialHistoryRouteUsesStyledSections(t *testing.T) {
98 func TestAppLoadInitialHistoryRouteShowsTotalStatsWithWindowedTimeline(t *testing.T) {98 func TestAppLoadInitialHistoryRouteShowsTotalStatsWithWindowedTimeline(t *testing.T) {
99 t.Parallel()99 t.Parallel()
100 100
101- history := &fakeHistoryService{101+ history := &recordingHistoryService{
102 entries: []*entity.HistoryEntry{102 entries: []*entity.HistoryEntry{
103 {ID: 1, URL: "https://older.example", Title: "Older", VisitCount: 2},103 {ID: 1, URL: "https://older.example", Title: "Older", VisitCount: 2},
104 {ID: 2, URL: "https://newer.example", Title: "Newer", VisitCount: 3},104 {ID: 2, URL: "https://newer.example", Title: "Newer", VisitCount: 3},
@@ -119,8 +119,8 @@ func TestAppLoadInitialHistoryRouteShowsTotalStatsWithWindowedTimeline(t *testin
119 func TestAppRunMountsHistoryLoadingBeforeAsyncHydration(t *testing.T) {119 func TestAppRunMountsHistoryLoadingBeforeAsyncHydration(t *testing.T) {
120 t.Parallel()120 t.Parallel()
121 121
122- dom := &fakeActionDOM{fakeDOM: fakeDOM{mounts: make(chan string, 4)}}122+ dom := &recordingActionDOM{recordingDOM: recordingDOM{mounts: make(chan string, 4)}}
123- history := &fakeHistoryService{123+ history := &recordingHistoryService{
124 entries: []*entity.HistoryEntry{{ID: 1, URL: "https://example.com", Title: "Loaded entry"}},124 entries: []*entity.HistoryEntry{{ID: 1, URL: "https://example.com", Title: "Loaded entry"}},
125 timelineStarted: make(chan struct{}),125 timelineStarted: make(chan struct{}),
126 releaseTimeline: make(chan struct{}),126 releaseTimeline: make(chan struct{}),
@@ -147,7 +147,7 @@ func TestAppRunMountsHistoryLoadingBeforeAsyncHydration(t *testing.T) {
147 func TestSurfaceActionErrorPreventsStaleAsyncHydrationMount(t *testing.T) {147 func TestSurfaceActionErrorPreventsStaleAsyncHydrationMount(t *testing.T) {
148 t.Parallel()148 t.Parallel()
149 149
150- dom := &fakeDOM{mounts: make(chan string, 4)}150+ dom := &recordingDOM{mounts: make(chan string, 4)}
151 app := NewApp(Dependencies{DOM: dom, LocationURI: "dumb://history"})151 app := NewApp(Dependencies{DOM: dom, LocationURI: "dumb://history"})
152 app.currentRoute = RouteHistory152 app.currentRoute = RouteHistory
153 staleHTML := "<div>Stale hydrated entry</div>"153 staleHTML := "<div>Stale hydrated entry</div>"
@@ -168,8 +168,8 @@ func TestSurfaceActionErrorPreventsStaleAsyncHydrationMount(t *testing.T) {
168 func TestSurfaceActionErrorWhileAsyncHydrationBlockedPreventsStaleMount(t *testing.T) {168 func TestSurfaceActionErrorWhileAsyncHydrationBlockedPreventsStaleMount(t *testing.T) {
169 t.Parallel()169 t.Parallel()
170 170
171- dom := &fakeActionDOM{fakeDOM: fakeDOM{mounts: make(chan string, 4)}}171+ dom := &recordingActionDOM{recordingDOM: recordingDOM{mounts: make(chan string, 4)}}
172- history := &fakeHistoryService{172+ history := &recordingHistoryService{
173 entries: []*entity.HistoryEntry{{ID: 1, URL: "https://example.com", Title: "Stale hydrated entry"}},173 entries: []*entity.HistoryEntry{{ID: 1, URL: "https://example.com", Title: "Stale hydrated entry"}},
174 timelineStarted: make(chan struct{}),174 timelineStarted: make(chan struct{}),
175 releaseTimeline: make(chan struct{}),175 releaseTimeline: make(chan struct{}),
@@ -200,7 +200,7 @@ func TestSurfaceActionErrorWhileAsyncHydrationBlockedPreventsStaleMount(t *testi
200 200
201 func TestCurrentHistoryRouteSnapshotIncludesWindowAfter(t *testing.T) {201 func TestCurrentHistoryRouteSnapshotIncludesWindowAfter(t *testing.T) {
202 cursor := time.Date(2026, 4, 25, 9, 0, 0, 0, time.UTC)202 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"})
204 app.currentRoute = RouteHistory204 app.currentRoute = RouteHistory
205 app.historyWindowAfter = cursor205 app.historyWindowAfter = cursor
206 206
@@ -212,13 +212,13 @@ func TestAppHandleHistoryLoadMoreAppendsOlderWindow(t *testing.T) {
212 t.Parallel()212 t.Parallel()
213 213
214 cursor := time.Date(2026, 4, 25, 9, 0, 0, 0, time.UTC)214 cursor := time.Date(2026, 4, 25, 9, 0, 0, 0, time.UTC)
215- history := &fakeHistoryService{window: &entity.HistoryWindow{215+ history := &recordingHistoryService{window: &entity.HistoryWindow{
216 Entries: []*entity.HistoryEntry{{ID: 2, URL: "https://older.example", Title: "Older entry", LastVisited: cursor.Add(-time.Hour)}},216 Entries: []*entity.HistoryEntry{{ID: 2, URL: "https://older.example", Title: "Older entry", LastVisited: cursor.Add(-time.Hour)}},
217 Before: cursor,217 Before: cursor,
218 After: cursor.Add(-24 * time.Hour),218 After: cursor.Add(-24 * time.Hour),
219 HasMore: true,219 HasMore: true,
220 }}220 }}
221- dom := &fakeActionDOM{}221+ dom := &recordingActionDOM{}
222 app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"})222 app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"})
223 app.currentRoute = RouteHistory223 app.currentRoute = RouteHistory
224 app.historyEntries = []*entity.HistoryEntry{{ID: 1, URL: "https://newer.example", Title: "Newer entry", LastVisited: cursor}}224 app.historyEntries = []*entity.HistoryEntry{{ID: 1, URL: "https://newer.example", Title: "Newer entry", LastVisited: cursor}}
@@ -240,13 +240,13 @@ func TestAppHandleHistoryLoadMoreRemountsWhenAppendUnavailable(t *testing.T) {
240 t.Parallel()240 t.Parallel()
241 241
242 cursor := time.Date(2026, 4, 25, 9, 0, 0, 0, time.UTC)242 cursor := time.Date(2026, 4, 25, 9, 0, 0, 0, time.UTC)
243- history := &fakeHistoryService{window: &entity.HistoryWindow{243+ history := &recordingHistoryService{window: &entity.HistoryWindow{
244 Entries: []*entity.HistoryEntry{{ID: 2, URL: "https://older.example", Title: "Older entry", LastVisited: cursor.Add(-time.Hour)}},244 Entries: []*entity.HistoryEntry{{ID: 2, URL: "https://older.example", Title: "Older entry", LastVisited: cursor.Add(-time.Hour)}},
245 Before: cursor,245 Before: cursor,
246 After: cursor.Add(-24 * time.Hour),246 After: cursor.Add(-24 * time.Hour),
247 HasMore: true,247 HasMore: true,
248 }}248 }}
249- dom := &fakeDOM{}249+ dom := &recordingDOM{}
250 app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"})250 app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"})
251 app.currentRoute = RouteHistory251 app.currentRoute = RouteHistory
252 app.historyEntries = []*entity.HistoryEntry{{ID: 1, URL: "https://newer.example", Title: "Newer entry", LastVisited: cursor}}252 app.historyEntries = []*entity.HistoryEntry{{ID: 1, URL: "https://newer.example", Title: "Newer entry", LastVisited: cursor}}
@@ -267,8 +267,8 @@ func TestAppHandleHistoryLoadMoreIgnoresStaleCursor(t *testing.T) {
267 t.Parallel()267 t.Parallel()
268 268
269 cursor := time.Date(2026, 4, 25, 9, 0, 0, 0, time.UTC)269 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"}}}}270+ history := &recordingHistoryService{window: &entity.HistoryWindow{Entries: []*entity.HistoryEntry{{ID: 2, URL: "https://older.example"}}}}
271- dom := &fakeActionDOM{}271+ dom := &recordingActionDOM{}
272 app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"})272 app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"})
273 app.currentRoute = RouteHistory273 app.currentRoute = RouteHistory
274 app.historyWindowAfter = cursor274 app.historyWindowAfter = cursor
@@ -285,7 +285,7 @@ func TestAppHandleHistoryLoadMoreIgnoresStaleCursor(t *testing.T) {
285 func TestAppLoadInitialHistoryRouteRendersManagementActions(t *testing.T) {285 func TestAppLoadInitialHistoryRouteRendersManagementActions(t *testing.T) {
286 t.Parallel()286 t.Parallel()
287 287
288- history := &fakeHistoryService{288+ history := &recordingHistoryService{
289 entries: []*entity.HistoryEntry{{289 entries: []*entity.HistoryEntry{{
290 ID: 42,290 ID: 42,
291 URL: "https://example.com/page",291 URL: "https://example.com/page",
@@ -314,8 +314,8 @@ func TestAppLoadInitialHistoryRouteRendersManagementActions(t *testing.T) {
314 }314 }
315 315
316 func TestAppHandleHistoryActionsRefreshesDOM(t *testing.T) {316 func TestAppHandleHistoryActionsRefreshesDOM(t *testing.T) {
317- dom := &fakeDOM{}317+ dom := &recordingDOM{}
318- history := &fakeHistoryService{318+ history := &recordingHistoryService{
319 entries: []*entity.HistoryEntry{{ID: 42, URL: "https://example.com", Title: "Example"}},319 entries: []*entity.HistoryEntry{{ID: 42, URL: "https://example.com", Title: "Example"}},
320 searchEntries: []*entity.HistoryEntry{{ID: 7, URL: "https://search.example", Title: "Search result"}},320 searchEntries: []*entity.HistoryEntry{{ID: 7, URL: "https://search.example", Title: "Search result"}},
321 }321 }
@@ -364,8 +364,8 @@ func TestAppHandleHistoryActionsRefreshesDOM(t *testing.T) {
364 }364 }
365 365
366 func TestAppCloseStopsActionWorkerAndReleasesDOM(t *testing.T) {366 func TestAppCloseStopsActionWorkerAndReleasesDOM(t *testing.T) {
367- dom := &fakeActionDOM{}367+ dom := &recordingActionDOM{}
368- history := &fakeHistoryService{entries: []*entity.HistoryEntry{{ID: 1, URL: "https://example.com"}}}368+ history := &recordingHistoryService{entries: []*entity.HistoryEntry{{ID: 1, URL: "https://example.com"}}}
369 app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"})369 app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"})
370 370
371 ctx, cancel := context.WithCancel(context.Background())371 ctx, cancel := context.WithCancel(context.Background())
@@ -396,8 +396,8 @@ func TestAppRunCleansActionWorkerWhenBindingFails(t *testing.T) {
396 }396 }
397 397
398 func TestAppRunReleasesDOMWhenActionWorkerCannotStart(t *testing.T) {398 func TestAppRunReleasesDOMWhenActionWorkerCannotStart(t *testing.T) {
399- dom := &fakeActionDOM{}399+ dom := &recordingActionDOM{}
400- history := &fakeHistoryService{entries: []*entity.HistoryEntry{{ID: 1, URL: "https://example.com"}}}400+ history := &recordingHistoryService{entries: []*entity.HistoryEntry{{ID: 1, URL: "https://example.com"}}}
401 app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"})401 app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"})
402 402
403 ctx, cancel := context.WithCancel(context.Background())403 ctx, cancel := context.WithCancel(context.Background())
@@ -415,7 +415,7 @@ func TestAppRunReleasesDOMWhenActionWorkerCannotStart(t *testing.T) {
415 func TestAppLoadInitialHistoryRouteRendersErrorState(t *testing.T) {415 func TestAppLoadInitialHistoryRouteRendersErrorState(t *testing.T) {
416 t.Parallel()416 t.Parallel()
417 417
418- history := &fakeHistoryService{err: errors.New("database unavailable")}418+ history := &recordingHistoryService{err: errors.New("database unavailable")}
419 app := NewApp(Dependencies{419 app := NewApp(Dependencies{
420 History: history,420 History: history,
421 LocationURI: "dumb://history",421 LocationURI: "dumb://history",
@@ -432,11 +432,11 @@ func TestAppLoadInitialHistoryRouteRendersErrorState(t *testing.T) {
432 func TestAppLoadInitialHistoryRouteAppliesThemeTokens(t *testing.T) {432 func TestAppLoadInitialHistoryRouteAppliesThemeTokens(t *testing.T) {
433 t.Parallel()433 t.Parallel()
434 434
435- history := &fakeHistoryService{entries: []*entity.HistoryEntry{{435+ history := &recordingHistoryService{entries: []*entity.HistoryEntry{{
436 URL: "https://example.com",436 URL: "https://example.com",
437 Title: "Example",437 Title: "Example",
438 }}}438 }}}
439- config := &fakeConfigService{current: dto.SystemviewConfigPayload{439+ config := &recordingConfigService{current: dto.SystemviewConfigPayload{
440 Appearance: dto.WebUIAppearanceConfig{440 Appearance: dto.WebUIAppearanceConfig{
441 ColorScheme: "prefer-light",441 ColorScheme: "prefer-light",
442 LightPalette: dto.ColorPalette{442 LightPalette: dto.ColorPalette{
@@ -479,7 +479,7 @@ func TestAppLoadInitialHistoryRouteAppliesThemeTokens(t *testing.T) {
479 func TestAppLoadInitialFavoritesRouteRendersData(t *testing.T) {479 func TestAppLoadInitialFavoritesRouteRendersData(t *testing.T) {
480 t.Parallel()480 t.Parallel()
481 481
482- favorites := &fakeFavoritesService{482+ favorites := &recordingFavoritesService{
483 favorites: []*entity.Favorite{{URL: "https://example.com", Title: "Example"}},483 favorites: []*entity.Favorite{{URL: "https://example.com", Title: "Example"}},
484 folders: []*entity.Folder{{Name: "Read Later"}},484 folders: []*entity.Folder{{Name: "Read Later"}},
485 tags: []*entity.Tag{{Name: "Go"}},485 tags: []*entity.Tag{{Name: "Go"}},
@@ -519,7 +519,7 @@ func TestAppLoadInitialFavoritesRouteRendersCRUDControls(t *testing.T) {
519 519
520 folderID := entity.FolderID(1)520 folderID := entity.FolderID(1)
521 shortcut := 3521 shortcut := 3
522- favorites := &fakeFavoritesService{522+ favorites := &recordingFavoritesService{
523 favorites: []*entity.Favorite{{ID: 42, URL: "https://example.com", Title: "Example", FolderID: &folderID, ShortcutKey: &shortcut}},523 favorites: []*entity.Favorite{{ID: 42, URL: "https://example.com", Title: "Example", FolderID: &folderID, ShortcutKey: &shortcut}},
524 folders: []*entity.Folder{{ID: folderID, Name: "Read Later", Icon: "📚"}},524 folders: []*entity.Folder{{ID: folderID, Name: "Read Later", Icon: "📚"}},
525 tags: []*entity.Tag{{ID: 7, Name: "Go", Color: "#00add8"}},525 tags: []*entity.Tag{{ID: 7, Name: "Go", Color: "#00add8"}},
@@ -546,8 +546,8 @@ func TestAppLoadInitialFavoritesRouteRendersCRUDControls(t *testing.T) {
546 }546 }
547 547
548 func TestAppHandleFavoriteActionsRefreshesDOM(t *testing.T) {548 func TestAppHandleFavoriteActionsRefreshesDOM(t *testing.T) {
549- dom := &fakeDOM{}549+ dom := &recordingDOM{}
550- favorites := &fakeFavoritesService{550+ favorites := &recordingFavoritesService{
551 favorites: []*entity.Favorite{{ID: 42, URL: "https://example.com", Title: "Example"}},551 favorites: []*entity.Favorite{{ID: 42, URL: "https://example.com", Title: "Example"}},
552 folders: []*entity.Folder{{ID: 5, Name: "Read Later"}},552 folders: []*entity.Folder{{ID: 5, Name: "Read Later"}},
553 tags: []*entity.Tag{{ID: 7, Name: "Go", Color: "#00add8"}},553 tags: []*entity.Tag{{ID: 7, Name: "Go", Color: "#00add8"}},
@@ -675,7 +675,7 @@ func TestAppLoadInitialConfigRouteRendersData(t *testing.T) {
675 },675 },
676 },676 },
677 }677 }
678- service := &fakeConfigService{678+ service := &recordingConfigService{
679 current: config,679 current: config,
680 keybindings: port.KeybindingsConfig{Groups: []port.KeybindingGroup{680 keybindings: port.KeybindingsConfig{Groups: []port.KeybindingGroup{
681 {681 {
@@ -756,7 +756,7 @@ func TestAppLoadInitialConfigRouteRendersData(t *testing.T) {
756 func TestAppLoadInitialConfigRouteRendersEditControls(t *testing.T) {756 func TestAppLoadInitialConfigRouteRendersEditControls(t *testing.T) {
757 t.Parallel()757 t.Parallel()
758 758
759- service := &fakeConfigService{759+ service := &recordingConfigService{
760 current: testConfigPayload(),760 current: testConfigPayload(),
761 keybindings: port.KeybindingsConfig{Groups: []port.KeybindingGroup{{761 keybindings: port.KeybindingsConfig{Groups: []port.KeybindingGroup{{
762 Mode: "default",762 Mode: "default",
@@ -793,8 +793,8 @@ func TestAppLoadInitialConfigRouteRendersEditControls(t *testing.T) {
793 }793 }
794 794
795 func TestAppHandleConfigActionsRefreshesDOM(t *testing.T) {795 func TestAppHandleConfigActionsRefreshesDOM(t *testing.T) {
796- dom := &fakeDOM{}796+ dom := &recordingDOM{}
797- service := &fakeConfigService{797+ service := &recordingConfigService{
798 current: testConfigPayload(),798 current: testConfigPayload(),
799 defaultCfg: testDefaultConfigPayload(),799 defaultCfg: testDefaultConfigPayload(),
800 keybindings: port.KeybindingsConfig{Groups: []port.KeybindingGroup{{800 keybindings: port.KeybindingsConfig{Groups: []port.KeybindingGroup{{
@@ -883,7 +883,7 @@ func TestAppHandleConfigActionsRefreshesDOM(t *testing.T) {
883 func TestAppRejectsSearchURLsWithoutPlaceholder(t *testing.T) {883 func TestAppRejectsSearchURLsWithoutPlaceholder(t *testing.T) {
884 t.Parallel()884 t.Parallel()
885 885
886- service := &fakeConfigService{current: testConfigPayload(), defaultCfg: testDefaultConfigPayload()}886+ service := &recordingConfigService{current: testConfigPayload(), defaultCfg: testDefaultConfigPayload()}
887 app := NewApp(Dependencies{Config: service, LocationURI: "dumb://config"})887 app := NewApp(Dependencies{Config: service, LocationURI: "dumb://config"})
888 888
889 err := app.handleConfigAction(context.Background(), DOMAction{889 err := app.handleConfigAction(context.Background(), DOMAction{
@@ -909,7 +909,7 @@ func TestAppLoadInitialConfigRouteUsesResolvedAppearanceOnlyForShellTheme(t *tes
909 resolved.DarkPalette.Background = "#222222"909 resolved.DarkPalette.Background = "#222222"
910 resolved.DarkPalette.SurfaceVariant = "#444444"910 resolved.DarkPalette.SurfaceVariant = "#444444"
911 cfg.ResolvedAppearance = &resolved911 cfg.ResolvedAppearance = &resolved
912- service := &fakeConfigService{912+ service := &recordingConfigService{
913 current: cfg,913 current: cfg,
914 keybindings: port.KeybindingsConfig{},914 keybindings: port.KeybindingsConfig{},
915 }915 }
@@ -956,8 +956,8 @@ func testDefaultConfigPayload() dto.SystemviewConfigPayload {
956 return cfg956 return cfg
957 }957 }
958 958
959-// Handwritten fake to capture DOM mounts for stateful render assertions.959+// Recording fixture to capture DOM mounts for stateful render assertions.
960-type fakeDOM struct {960+type recordingDOM struct {
961 mu sync.Mutex961 mu sync.Mutex
962 mounted bool962 mounted bool
963 html string963 html string
@@ -965,7 +965,7 @@ type fakeDOM struct {
965 mounts chan string965 mounts chan string
966 }966 }
967 967
968-func (d *fakeDOM) Mount(markup string) error {968+func (d *recordingDOM) Mount(markup string) error {
969 d.mu.Lock()969 d.mu.Lock()
970 d.mounted = true970 d.mounted = true
971 d.html = markup971 d.html = markup
@@ -980,19 +980,19 @@ func (d *fakeDOM) Mount(markup string) error {
980 return nil980 return nil
981 }981 }
982 982
983-func (d *fakeDOM) Mounted() bool {983+func (d *recordingDOM) Mounted() bool {
984 d.mu.Lock()984 d.mu.Lock()
985 defer d.mu.Unlock()985 defer d.mu.Unlock()
986 return d.mounted986 return d.mounted
987 }987 }
988 988
989-func (d *fakeDOM) HTML() string {989+func (d *recordingDOM) HTML() string {
990 d.mu.Lock()990 d.mu.Lock()
991 defer d.mu.Unlock()991 defer d.mu.Unlock()
992 return d.html992 return d.html
993 }993 }
994 994
995-func (d *fakeDOM) AppendedHTML() string {995+func (d *recordingDOM) AppendedHTML() string {
996 d.mu.Lock()996 d.mu.Lock()
997 defer d.mu.Unlock()997 defer d.mu.Unlock()
998 return d.appendedHTML998 return d.appendedHTML
@@ -1018,30 +1018,30 @@ func assertNoMount(t *testing.T, mounts <-chan string, wait time.Duration) {
1018 }1018 }
1019 }1019 }
1020 1020
1021-type fakeActionDOM struct {1021+type recordingActionDOM struct {
1022- fakeDOM1022+ recordingDOM
1023 handler DOMActionHandler1023 handler DOMActionHandler
1024 released bool1024 released bool
1025 }1025 }
1026 1026
1027-func (d *fakeActionDOM) BindActions(handler DOMActionHandler) error {1027+func (d *recordingActionDOM) BindActions(handler DOMActionHandler) error {
1028 d.handler = handler1028 d.handler = handler
1029 return nil1029 return nil
1030 }1030 }
1031 1031
1032-func (d *fakeActionDOM) AppendHistoryTimeline(markup string) error {1032+func (d *recordingActionDOM) AppendHistoryTimeline(markup string) error {
1033 d.mu.Lock()1033 d.mu.Lock()
1034 d.appendedHTML += markup1034 d.appendedHTML += markup
1035 d.mu.Unlock()1035 d.mu.Unlock()
1036 return nil1036 return nil
1037 }1037 }
1038 1038
1039-func (d *fakeActionDOM) Release() {1039+func (d *recordingActionDOM) Release() {
1040 d.released = true1040 d.released = true
1041 }1041 }
1042 1042
1043 type failingActionDOM struct {1043 type failingActionDOM struct {
1044- fakeDOM1044+ recordingDOM
1045 bindErr error1045 bindErr error
1046 }1046 }
1047 1047
@@ -1049,8 +1049,8 @@ func (d *failingActionDOM) BindActions(DOMActionHandler) error {
1049 return d.bindErr1049 return d.bindErr
1050 }1050 }
1051 1051
1052-// Handwritten fake to capture history state for stateful render assertions.1052+// Recording fixture to capture history state for stateful render assertions.
1053-type fakeHistoryService struct {1053+type recordingHistoryService struct {
1054 called bool1054 called bool
1055 limit int1055 limit int
1056 offset int1056 offset int
@@ -1080,7 +1080,7 @@ type fakeHistoryService struct {
1080 startOnce sync.Once1080 startOnce sync.Once
1081 }1081 }
1082 1082
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) {
1084 s.called = true1084 s.called = true
1085 s.limit = limit1085 s.limit = limit
1086 s.offset = offset1086 s.offset = offset
@@ -1093,7 +1093,7 @@ func (s *fakeHistoryService) Timeline(_ context.Context, limit, offset int) ([]*
1093 return s.entries, s.err1093 return s.entries, s.err
1094 }1094 }
1095 1095
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) {
1097 s.domainCalled = true1097 s.domainCalled = true
1098 s.domain = domain1098 s.domain = domain
1099 s.limit = limit1099 s.limit = limit
@@ -1101,7 +1101,7 @@ func (s *fakeHistoryService) TimelineByDomain(_ context.Context, domain string,
1101 return s.entries, s.err1101 return s.entries, s.err
1102 }1102 }
1103 1103
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) {
1105 s.called = true1105 s.called = true
1106 s.windowBefore = before1106 s.windowBefore = before
1107 s.windowDomain = domain1107 s.windowDomain = domain
@@ -1120,7 +1120,7 @@ func (s *fakeHistoryService) TimelineWindow(_ context.Context, before time.Time,
1120 return &entity.HistoryWindow{Entries: s.entries, Before: before, After: before.Add(-24 * time.Hour)}, nil1120 return &entity.HistoryWindow{Entries: s.entries, Before: before, After: before.Add(-24 * time.Hour)}, nil
1121 }1121 }
1122 1122
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) {
1124 s.searchCalled = true1124 s.searchCalled = true
1125 s.query = query1125 s.query = query
1126 s.searchLimit = limit1126 s.searchLimit = limit
@@ -1130,37 +1130,37 @@ func (s *fakeHistoryService) Search(_ context.Context, query string, limit int)
1130 return s.entries, nil1130 return s.entries, nil
1131 }1131 }
1132 1132
1133-func (s *fakeHistoryService) DeleteEntry(_ context.Context, id int64) error {1133+func (s *recordingHistoryService) DeleteEntry(_ context.Context, id int64) error {
1134 s.deletedEntryID = id1134 s.deletedEntryID = id
1135 return nil1135 return nil
1136 }1136 }
1137 1137
1138-func (s *fakeHistoryService) DeleteRange(_ context.Context, rangeID string) error {1138+func (s *recordingHistoryService) DeleteRange(_ context.Context, rangeID string) error {
1139 s.deletedRangeID = rangeID1139 s.deletedRangeID = rangeID
1140 return nil1140 return nil
1141 }1141 }
1142 1142
1143-func (s *fakeHistoryService) Stats(context.Context) (*entity.HistoryStats, error) {1143+func (s *recordingHistoryService) Stats(context.Context) (*entity.HistoryStats, error) {
1144 s.statsCalled = true1144 s.statsCalled = true
1145 return s.stats, nil1145 return s.stats, nil
1146 }1146 }
1147 1147
1148-func (s *fakeHistoryService) Analytics(context.Context) (*entity.HistoryAnalytics, error) {1148+func (s *recordingHistoryService) Analytics(context.Context) (*entity.HistoryAnalytics, error) {
1149 s.analyticsCalled = true1149 s.analyticsCalled = true
1150 return nil, nil1150 return nil, nil
1151 }1151 }
1152 1152
1153-func (s *fakeHistoryService) DomainStats(context.Context, int) ([]*entity.DomainStat, error) {1153+func (s *recordingHistoryService) DomainStats(context.Context, int) ([]*entity.DomainStat, error) {
1154 return s.domainStats, nil1154 return s.domainStats, nil
1155 }1155 }
1156 1156
1157-func (s *fakeHistoryService) DeleteDomain(_ context.Context, domain string) error {1157+func (s *recordingHistoryService) DeleteDomain(_ context.Context, domain string) error {
1158 s.deletedDomain = domain1158 s.deletedDomain = domain
1159 return nil1159 return nil
1160 }1160 }
1161 1161
1162-// Handwritten fake to capture favorites state for stateful render assertions.1162+// Recording fixture to capture favorites state for stateful render assertions.
1163-type fakeFavoritesService struct {1163+type recordingFavoritesService struct {
1164 calledList bool1164 calledList bool
1165 calledFolders bool1165 calledFolders bool
1166 calledTags bool1166 calledTags bool
@@ -1184,85 +1184,85 @@ type fakeFavoritesService struct {
1184 removedTagID int641184 removedTagID int64
1185 }1185 }
1186 1186
1187-func (s *fakeFavoritesService) List(context.Context) ([]*entity.Favorite, error) {1187+func (s *recordingFavoritesService) List(context.Context) ([]*entity.Favorite, error) {
1188 s.calledList = true1188 s.calledList = true
1189 return s.favorites, nil1189 return s.favorites, nil
1190 }1190 }
1191 1191
1192-func (s *fakeFavoritesService) ListFolders(context.Context) ([]*entity.Folder, error) {1192+func (s *recordingFavoritesService) ListFolders(context.Context) ([]*entity.Folder, error) {
1193 s.calledFolders = true1193 s.calledFolders = true
1194 return s.folders, nil1194 return s.folders, nil
1195 }1195 }
1196 1196
1197-func (s *fakeFavoritesService) ListTags(context.Context) ([]*entity.Tag, error) {1197+func (s *recordingFavoritesService) ListTags(context.Context) ([]*entity.Tag, error) {
1198 s.calledTags = true1198 s.calledTags = true
1199 return s.tags, nil1199 return s.tags, nil
1200 }1200 }
1201 1201
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) {
1203 s.createdFavorite = input1203 s.createdFavorite = input
1204 return &entity.Favorite{ID: 99, URL: input.URL, Title: input.Title, FolderID: input.FolderID}, nil1204 return &entity.Favorite{ID: 99, URL: input.URL, Title: input.Title, FolderID: input.FolderID}, nil
1205 }1205 }
1206 1206
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) {
1208 s.updatedFavorite = input1208 s.updatedFavorite = input
1209 return &entity.Favorite{ID: input.ID, URL: "https://example.com", Title: input.Title, FaviconURL: input.FaviconURL, FolderID: input.FolderID, ShortcutKey: input.ShortcutKey}, nil1209 return &entity.Favorite{ID: input.ID, URL: "https://example.com", Title: input.Title, FaviconURL: input.FaviconURL, FolderID: input.FolderID, ShortcutKey: input.ShortcutKey}, nil
1210 }1210 }
1211 1211
1212-func (s *fakeFavoritesService) DeleteFavorite(_ context.Context, id int64) error {1212+func (s *recordingFavoritesService) DeleteFavorite(_ context.Context, id int64) error {
1213 s.deletedFavorite = id1213 s.deletedFavorite = id
1214 return nil1214 return nil
1215 }1215 }
1216 1216
1217-func (s *fakeFavoritesService) SetShortcut(context.Context, int64, *int) error { return nil }1217+func (s *recordingFavoritesService) SetShortcut(context.Context, int64, *int) error { return nil }
1218 1218
1219-func (s *fakeFavoritesService) SetFolder(context.Context, int64, *int64) error { return nil }1219+func (s *recordingFavoritesService) SetFolder(context.Context, int64, *int64) error { return nil }
1220 1220
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) {
1222 s.createdFolder = name1222 s.createdFolder = name
1223 s.createdFolderIcon = icon1223 s.createdFolderIcon = icon
1224 return &entity.Folder{ID: 77, Name: name, Icon: icon}, nil1224 return &entity.Folder{ID: 77, Name: name, Icon: icon}, nil
1225 }1225 }
1226 1226
1227-func (s *fakeFavoritesService) UpdateFolder(_ context.Context, id int64, _, _ string) error {1227+func (s *recordingFavoritesService) UpdateFolder(_ context.Context, id int64, _, _ string) error {
1228 s.updatedFolderID = id1228 s.updatedFolderID = id
1229 return nil1229 return nil
1230 }1230 }
1231 1231
1232-func (s *fakeFavoritesService) DeleteFolder(_ context.Context, id int64) error {1232+func (s *recordingFavoritesService) DeleteFolder(_ context.Context, id int64) error {
1233 s.deletedFolderID = id1233 s.deletedFolderID = id
1234 return nil1234 return nil
1235 }1235 }
1236 1236
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) {
1238 s.createdTag = name1238 s.createdTag = name
1239 return &entity.Tag{ID: 55, Name: name, Color: color}, nil1239 return &entity.Tag{ID: 55, Name: name, Color: color}, nil
1240 }1240 }
1241 1241
1242-func (s *fakeFavoritesService) UpdateTag(_ context.Context, id int64, _, _ string) error {1242+func (s *recordingFavoritesService) UpdateTag(_ context.Context, id int64, _, _ string) error {
1243 s.updatedTagID = id1243 s.updatedTagID = id
1244 return nil1244 return nil
1245 }1245 }
1246 1246
1247-func (s *fakeFavoritesService) DeleteTag(_ context.Context, id int64) error {1247+func (s *recordingFavoritesService) DeleteTag(_ context.Context, id int64) error {
1248 s.deletedTagID = id1248 s.deletedTagID = id
1249 return nil1249 return nil
1250 }1250 }
1251 1251
1252-func (s *fakeFavoritesService) AssignTag(_ context.Context, favoriteID, tagID int64) error {1252+func (s *recordingFavoritesService) AssignTag(_ context.Context, favoriteID, tagID int64) error {
1253 s.assignedFavoriteID = favoriteID1253 s.assignedFavoriteID = favoriteID
1254 s.assignedTagID = tagID1254 s.assignedTagID = tagID
1255 return nil1255 return nil
1256 }1256 }
1257 1257
1258-func (s *fakeFavoritesService) RemoveTag(_ context.Context, favoriteID, tagID int64) error {1258+func (s *recordingFavoritesService) RemoveTag(_ context.Context, favoriteID, tagID int64) error {
1259 s.removedFavoriteID = favoriteID1259 s.removedFavoriteID = favoriteID
1260 s.removedTagID = tagID1260 s.removedTagID = tagID
1261 return nil1261 return nil
1262 }1262 }
1263 1263
1264-// Handwritten fake to capture config state for stateful render assertions.1264+// Recording fixture to capture config state for stateful render assertions.
1265-type fakeConfigService struct {1265+type recordingConfigService struct {
1266 calledCurrent bool1266 calledCurrent bool
1267 calledDefault bool1267 calledDefault bool
1268 calledSave bool1268 calledSave bool
@@ -1280,17 +1280,17 @@ type fakeConfigService struct {
1280 resetReq port.ResetKeybindingRequest1280 resetReq port.ResetKeybindingRequest
1281 }1281 }
1282 1282
1283-func (s *fakeConfigService) Current(context.Context) (dto.SystemviewConfigPayload, error) {1283+func (s *recordingConfigService) Current(context.Context) (dto.SystemviewConfigPayload, error) {
1284 s.calledCurrent = true1284 s.calledCurrent = true
1285 return s.current, nil1285 return s.current, nil
1286 }1286 }
1287 1287
1288-func (s *fakeConfigService) Default(context.Context) (dto.SystemviewConfigPayload, error) {1288+func (s *recordingConfigService) Default(context.Context) (dto.SystemviewConfigPayload, error) {
1289 s.calledDefault = true1289 s.calledDefault = true
1290 return s.defaultCfg, nil1290 return s.defaultCfg, nil
1291 }1291 }
1292 1292
1293-func (s *fakeConfigService) Save(_ context.Context, cfg dto.WebUIConfig) error {1293+func (s *recordingConfigService) Save(_ context.Context, cfg dto.WebUIConfig) error {
1294 s.calledSave = true1294 s.calledSave = true
1295 s.savedConfig = cfg1295 s.savedConfig = cfg
1296 s.current.Appearance = cfg.Appearance1296 s.current.Appearance = cfg.Appearance
@@ -1308,30 +1308,30 @@ func (s *fakeConfigService) Save(_ context.Context, cfg dto.WebUIConfig) error {
1308 return nil1308 return nil
1309 }1309 }
1310 1310
1311-func (s *fakeConfigService) GetKeybindings(context.Context) (port.KeybindingsConfig, error) {1311+func (s *recordingConfigService) GetKeybindings(context.Context) (port.KeybindingsConfig, error) {
1312 s.calledKeybindings = true1312 s.calledKeybindings = true
1313 return s.keybindings, nil1313 return s.keybindings, nil
1314 }1314 }
1315 1315
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) {
1317 s.calledSet = true1317 s.calledSet = true
1318 s.setReq = req1318 s.setReq = req
1319 return s.setResp, nil1319 return s.setResp, nil
1320 }1320 }
1321 1321
1322-func (s *fakeConfigService) ResetKeybinding(_ context.Context, req port.ResetKeybindingRequest) error {1322+func (s *recordingConfigService) ResetKeybinding(_ context.Context, req port.ResetKeybindingRequest) error {
1323 s.calledReset = true1323 s.calledReset = true
1324 s.resetReq = req1324 s.resetReq = req
1325 return nil1325 return nil
1326 }1326 }
1327 1327
1328-func (s *fakeConfigService) ResetAllKeybindings(context.Context) error {1328+func (s *recordingConfigService) ResetAllKeybindings(context.Context) error {
1329 s.calledResetAll = true1329 s.calledResetAll = true
1330 return nil1330 return nil
1331 }1331 }
1332 1332
1333 func TestHandleFavoriteActionValidatesCreateURL(t *testing.T) {1333 func TestHandleFavoriteActionValidatesCreateURL(t *testing.T) {
1334- app := NewApp(Dependencies{Favorites: &fakeFavoritesService{}})1334+ app := NewApp(Dependencies{Favorites: &recordingFavoritesService{}})
1335 1335
1336 err := app.handleFavoriteAction(context.Background(), DOMAction{1336 err := app.handleFavoriteAction(context.Background(), DOMAction{
1337 Action: favoriteActionCreate,1337 Action: favoriteActionCreate,
@@ -1356,7 +1356,7 @@ func TestHandleFavoriteActionValidatesCreateURL(t *testing.T) {
1356 }1356 }
1357 1357
1358 func TestHandleFavoriteCreateAcceptsInternalDumbRoutes(t *testing.T) {1358 func TestHandleFavoriteCreateAcceptsInternalDumbRoutes(t *testing.T) {
1359- favorites := &fakeFavoritesService{}1359+ favorites := &recordingFavoritesService{}
1360 app := NewApp(Dependencies{Favorites: favorites})1360 app := NewApp(Dependencies{Favorites: favorites})
1361 1361
1362 require.NoError(t, app.handleFavoriteAction(context.Background(), DOMAction{1362 require.NoError(t, app.handleFavoriteAction(context.Background(), DOMAction{
@@ -1373,7 +1373,7 @@ func TestHandleFavoriteCreateAcceptsInternalDumbRoutes(t *testing.T) {
1373 }1373 }
1374 1374
1375 func TestHandleFavoriteCreateRejectsUnknownDumbRoutes(t *testing.T) {1375 func TestHandleFavoriteCreateRejectsUnknownDumbRoutes(t *testing.T) {
1376- favorites := &fakeFavoritesService{}1376+ favorites := &recordingFavoritesService{}
1377 app := NewApp(Dependencies{Favorites: favorites})1377 app := NewApp(Dependencies{Favorites: favorites})
1378 1378
1379 err := app.handleFavoriteAction(context.Background(), DOMAction{1379 err := app.handleFavoriteAction(context.Background(), DOMAction{
@@ -1397,7 +1397,7 @@ func TestParsePositiveInt64DistinguishesParseAndRangeErrors(t *testing.T) {
1397 }1397 }
1398 1398
1399 func TestHandleFavoriteTagActionsAcceptSnakeCaseIDs(t *testing.T) {1399 func TestHandleFavoriteTagActionsAcceptSnakeCaseIDs(t *testing.T) {
1400- favorites := &fakeFavoritesService{}1400+ favorites := &recordingFavoritesService{}
1401 app := NewApp(Dependencies{Favorites: favorites})1401 app := NewApp(Dependencies{Favorites: favorites})
1402 1402
1403 require.NoError(t, app.handleFavoriteAction(context.Background(), DOMAction{1403 require.NoError(t, app.handleFavoriteAction(context.Background(), DOMAction{
@@ -1416,8 +1416,8 @@ func TestHandleFavoriteTagActionsAcceptSnakeCaseIDs(t *testing.T) {
1416 }1416 }
1417 1417
1418 func TestHandleHistorySearchClearsDomainFilter(t *testing.T) {1418 func TestHandleHistorySearchClearsDomainFilter(t *testing.T) {
1419- dom := &fakeDOM{}1419+ dom := &recordingDOM{}
1420- history := &fakeHistoryService{searchEntries: []*entity.HistoryEntry{{ID: 7, URL: "https://search.example", Title: "Search result"}}}1420+ history := &recordingHistoryService{searchEntries: []*entity.HistoryEntry{{ID: 7, URL: "https://search.example", Title: "Search result"}}}
1421 app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"})1421 app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"})
1422 app.historyDomainFilter = "example.com"1422 app.historyDomainFilter = "example.com"
1423 1423
@@ -1433,7 +1433,7 @@ func TestHandleHistorySearchClearsDomainFilter(t *testing.T) {
1433 }1433 }
1434 1434
1435 func TestAppRunWithContextRejectsNilContext(t *testing.T) {1435 func TestAppRunWithContextRejectsNilContext(t *testing.T) {
1436- app := NewApp(Dependencies{DOM: &fakeDOM{}, LocationURI: "dumb://history"})1436+ app := NewApp(Dependencies{DOM: &recordingDOM{}, LocationURI: "dumb://history"})
1437 1437
1438 var ctx context.Context1438 var ctx context.Context
1439 err := app.RunWithContext(ctx)1439 err := app.RunWithContext(ctx)
@@ -1442,7 +1442,7 @@ func TestAppRunWithContextRejectsNilContext(t *testing.T) {
1442 }1442 }
1443 1443
1444 func TestAppRunWithContextRequiresDOMBeforeLoading(t *testing.T) {1444 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"}}}
1446 app := NewApp(Dependencies{History: history, LocationURI: "dumb://history"})1446 app := NewApp(Dependencies{History: history, LocationURI: "dumb://history"})
1447 1447
1448 err := app.RunWithContext(context.Background())1448 err := app.RunWithContext(context.Background())
@@ -1453,9 +1453,9 @@ func TestAppRunWithContextRequiresDOMBeforeLoading(t *testing.T) {
1453 1453
1454 func TestHandleActionsRejectUnknownAction(t *testing.T) {1454 func TestHandleActionsRejectUnknownAction(t *testing.T) {
1455 app := NewApp(Dependencies{1455 app := NewApp(Dependencies{
1456- History: &fakeHistoryService{},1456+ History: &recordingHistoryService{},
1457- Favorites: &fakeFavoritesService{},1457+ Favorites: &recordingFavoritesService{},
1458- Config: &fakeConfigService{current: testConfigPayload()},1458+ Config: &recordingConfigService{current: testConfigPayload()},
1459 })1459 })
1460 1460
1461 require.ErrorContains(t, app.handleHistoryAction(context.Background(), DOMAction{Action: "history.unknown"}), "unknown history action")1461 require.ErrorContains(t, app.handleHistoryAction(context.Background(), DOMAction{Action: "history.unknown"}), "unknown history action")
@@ -1464,7 +1464,7 @@ func TestHandleActionsRejectUnknownAction(t *testing.T) {
1464 }1464 }
1465 1465
1466 func TestSavePerformanceConfigRejectsOutOfRangeValues(t *testing.T) {1466 func TestSavePerformanceConfigRejectsOutOfRangeValues(t *testing.T) {
1467- service := &fakeConfigService{current: testConfigPayload()}1467+ service := &recordingConfigService{current: testConfigPayload()}
1468 app := NewApp(Dependencies{Config: service})1468 app := NewApp(Dependencies{Config: service})
1469 1469
1470 err := app.handleConfigAction(context.Background(), DOMAction{1470 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