detached a605e6b

refactor(tests): replace simple fakes with generated mocks

commit
a605e6ba451c3c5fdc903d830cb0a17a24d9ac72
author
brice · · 2026-06-23 14:59 UTC
committer
brice · · 2026-06-23 20:01 UTC
parent
94cc600
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +171 -257
M internal/application/usecase/execute_context_menu_action_test.go +88 -144
@@ -6,79 +6,21 @@ import (
66 "testing"
77
88 "github.com/bnema/dumber/internal/application/port"
9+ portmocks "github.com/bnema/dumber/internal/application/port/mocks"
910 "github.com/bnema/dumber/internal/domain/entity"
11+ "github.com/stretchr/testify/mock"
1012 "github.com/stretchr/testify/require"
1113 )
1214
13-type fakeClipboard struct {
14- writeTextCalls int
15- writeImageCalls int
16- text string
17- image entity.ImageData
18-}
19-
20-func (f *fakeClipboard) WriteText(_ context.Context, text string) error {
21- f.writeTextCalls++
22- f.text = text
23- return nil
24-}
25-
26-func (f *fakeClipboard) WriteImage(_ context.Context, image entity.ImageData) error {
27- f.writeImageCalls++
28- f.image = image
29- return nil
30-}
31-
32-func (*fakeClipboard) ReadText(context.Context) (string, error) { return "", nil }
33-func (*fakeClipboard) Clear(context.Context) error { return nil }
34-func (*fakeClipboard) HasText(context.Context) (bool, error) { return false, nil }
35-
36-type fakeImageResolver struct {
37- resolveCalls int
38- uri string
39- image entity.ImageData
40- err error
41-}
42-
43-func (f *fakeImageResolver) ResolveImageData(_ context.Context, uri string) (entity.ImageData, error) {
44- f.resolveCalls++
45- f.uri = uri
46- return f.image, f.err
47-}
48-
49-type fakeResolvedImageSaver struct {
50- saveCalls int
51- image entity.ImageData
52- menuContext port.MenuContext
53- err error
54-}
55-
56-func (f *fakeResolvedImageSaver) SaveResolvedImage(_ context.Context, image entity.ImageData, menuContext port.MenuContext) error {
57- f.saveCalls++
58- f.image = image
59- f.menuContext = menuContext
60- return f.err
61-}
62-
63-type fakeMenuActionDelegator struct {
64- delegateCalls int
65- action port.MenuAction
66- menuContext port.MenuContext
67- err error
68-}
69-
70-func (f *fakeMenuActionDelegator) DelegateMenuAction(_ context.Context, action port.MenuAction, menuContext port.MenuContext) error {
71- f.delegateCalls++
72- f.action = action
73- f.menuContext = menuContext
74- return f.err
75-}
76-
7715 func TestExecuteContextMenuActionUseCase_CopyImageFailsWithoutResolvedData(t *testing.T) {
78- clipboard := &fakeClipboard{}
79- resolver := &fakeImageResolver{}
80- saver := &fakeResolvedImageSaver{}
81- delegator := &fakeMenuActionDelegator{}
16+ clipboard := portmocks.NewMockClipboard(t)
17+ resolver := portmocks.NewMockImageDataResolver(t)
18+ saver := portmocks.NewMockResolvedImageSaver(t)
19+ delegator := portmocks.NewMockMenuActionDelegator(t)
20+ resolver.EXPECT().
21+ ResolveImageData(mock.Anything, "https://example.com/image.png").
22+ Return(entity.ImageData{}, nil).
23+ Once()
8224 uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, saver, delegator)
8325
8426 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{
@@ -89,16 +31,22 @@ func TestExecuteContextMenuActionUseCase_CopyImageFailsWithoutResolvedData(t *te
8931 require.Error(t, err)
9032 require.Contains(t, err.Error(), "copy image:")
9133 require.Contains(t, err.Error(), "image data not available")
92- require.Zero(t, clipboard.writeImageCalls)
93- require.Zero(t, saver.saveCalls)
94- require.Zero(t, delegator.delegateCalls)
9534 }
9635
9736 func TestExecuteContextMenuActionUseCase_CopyImageWritesResolvedImage(t *testing.T) {
98- clipboard := &fakeClipboard{}
99- resolver := &fakeImageResolver{image: entity.ImageData{Bytes: []byte{1, 2, 3}, MimeType: "image/png"}}
100- saver := &fakeResolvedImageSaver{}
101- delegator := &fakeMenuActionDelegator{}
37+ clipboard := portmocks.NewMockClipboard(t)
38+ resolver := portmocks.NewMockImageDataResolver(t)
39+ saver := portmocks.NewMockResolvedImageSaver(t)
40+ delegator := portmocks.NewMockMenuActionDelegator(t)
41+ image := entity.ImageData{Bytes: []byte{1, 2, 3}, MimeType: "image/png"}
42+ resolver.EXPECT().
43+ ResolveImageData(mock.Anything, "https://example.com/image.png").
44+ Return(image, nil).
45+ Once()
46+ clipboard.EXPECT().
47+ WriteImage(mock.Anything, image).
48+ Return(nil).
49+ Once()
10250 uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, saver, delegator)
10351
10452 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{
@@ -107,18 +55,12 @@ func TestExecuteContextMenuActionUseCase_CopyImageWritesResolvedImage(t *testing
10755 })
10856
10957 require.NoError(t, err)
110- require.Equal(t, 1, resolver.resolveCalls)
111- require.Equal(t, "https://example.com/image.png", resolver.uri)
112- require.Equal(t, 1, clipboard.writeImageCalls)
113- require.Equal(t, entity.ImageData{Bytes: []byte{1, 2, 3}, MimeType: "image/png"}, clipboard.image)
114- require.Zero(t, saver.saveCalls)
115- require.Zero(t, delegator.delegateCalls)
11658 }
11759
11860 func TestExecuteContextMenuActionUseCase_CopyImageFailsFastWithoutClipboard(t *testing.T) {
119- resolver := &fakeImageResolver{}
120- saver := &fakeResolvedImageSaver{}
121- delegator := &fakeMenuActionDelegator{}
61+ resolver := portmocks.NewMockImageDataResolver(t)
62+ saver := portmocks.NewMockResolvedImageSaver(t)
63+ delegator := portmocks.NewMockMenuActionDelegator(t)
12264 uc := NewExecuteContextMenuActionUseCase(nil, resolver, saver, delegator)
12365
12466 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{
@@ -128,38 +70,37 @@ func TestExecuteContextMenuActionUseCase_CopyImageFailsFastWithoutClipboard(t *t
12870
12971 require.Error(t, err)
13072 require.Contains(t, err.Error(), "clipboard not available")
131- require.Zero(t, resolver.resolveCalls)
132- require.Zero(t, saver.saveCalls)
133- require.Zero(t, delegator.delegateCalls)
13473 }
13574
13675 func TestExecuteContextMenuActionUseCase_SaveImageDelegatesResolvedImage(t *testing.T) {
137- clipboard := &fakeClipboard{}
138- resolver := &fakeImageResolver{image: entity.ImageData{Bytes: []byte{1, 2, 3}, MimeType: "image/png"}}
139- saver := &fakeResolvedImageSaver{}
140- delegator := &fakeMenuActionDelegator{}
76+ clipboard := portmocks.NewMockClipboard(t)
77+ resolver := portmocks.NewMockImageDataResolver(t)
78+ saver := portmocks.NewMockResolvedImageSaver(t)
79+ delegator := portmocks.NewMockMenuActionDelegator(t)
80+ image := entity.ImageData{Bytes: []byte{1, 2, 3}, MimeType: "image/png"}
81+ menuContext := port.MenuContext{ImageURI: "https://example.com/image.png"}
82+ resolver.EXPECT().
83+ ResolveImageData(mock.Anything, menuContext.ImageURI).
84+ Return(image, nil).
85+ Once()
86+ saver.EXPECT().
87+ SaveResolvedImage(mock.Anything, image, menuContext).
88+ Return(nil).
89+ Once()
14190 uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, saver, delegator)
14291
14392 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{
14493 Action: port.MenuActionSaveImage,
145- Context: port.MenuContext{ImageURI: "https://example.com/image.png"},
94+ Context: menuContext,
14695 })
14796
14897 require.NoError(t, err)
149- require.Equal(t, 1, resolver.resolveCalls)
150- require.Equal(t, "https://example.com/image.png", resolver.uri)
151- require.Equal(t, 1, saver.saveCalls)
152- require.Equal(t, entity.ImageData{Bytes: []byte{1, 2, 3}, MimeType: "image/png"}, saver.image)
153- require.Equal(t, port.MenuContext{ImageURI: "https://example.com/image.png"}, saver.menuContext)
154- require.Zero(t, clipboard.writeTextCalls)
155- require.Zero(t, clipboard.writeImageCalls)
156- require.Zero(t, delegator.delegateCalls)
15798 }
15899
159100 func TestExecuteContextMenuActionUseCase_SaveImageFailsFastWithoutSaver(t *testing.T) {
160- clipboard := &fakeClipboard{}
161- resolver := &fakeImageResolver{}
162- delegator := &fakeMenuActionDelegator{}
101+ clipboard := portmocks.NewMockClipboard(t)
102+ resolver := portmocks.NewMockImageDataResolver(t)
103+ delegator := portmocks.NewMockMenuActionDelegator(t)
163104 uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, nil, delegator)
164105
165106 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{
@@ -169,42 +110,41 @@ func TestExecuteContextMenuActionUseCase_SaveImageFailsFastWithoutSaver(t *testi
169110
170111 require.Error(t, err)
171112 require.Contains(t, err.Error(), "image saver not available")
172- require.Zero(t, resolver.resolveCalls)
173- require.Zero(t, delegator.delegateCalls)
174113 }
175114
176115 func TestExecuteContextMenuActionUseCase_DelegatesInspect(t *testing.T) {
177- clipboard := &fakeClipboard{}
178- resolver := &fakeImageResolver{}
179- saver := &fakeResolvedImageSaver{}
180- delegator := &fakeMenuActionDelegator{}
181- uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, saver, delegator)
182-
183116 menuContext := port.MenuContext{
184117 PageURI: "https://example.com",
185118 X: 17,
186119 Y: 42,
187120 }
121+ clipboard := portmocks.NewMockClipboard(t)
122+ resolver := portmocks.NewMockImageDataResolver(t)
123+ saver := portmocks.NewMockResolvedImageSaver(t)
124+ delegator := portmocks.NewMockMenuActionDelegator(t)
125+ delegator.EXPECT().
126+ DelegateMenuAction(mock.Anything, port.MenuActionInspectElement, menuContext).
127+ Return(nil).
128+ Once()
129+ uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, saver, delegator)
130+
188131 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{
189132 Action: port.MenuActionInspectElement,
190133 Context: menuContext,
191134 })
192135
193136 require.NoError(t, err)
194- require.Equal(t, 1, delegator.delegateCalls)
195- require.Equal(t, port.MenuActionInspectElement, delegator.action)
196- require.Equal(t, menuContext, delegator.menuContext)
197- require.Zero(t, clipboard.writeTextCalls)
198- require.Zero(t, clipboard.writeImageCalls)
199- require.Zero(t, resolver.resolveCalls)
200- require.Zero(t, saver.saveCalls)
201137 }
202138
203139 func TestExecuteContextMenuActionUseCase_WrapsDelegateErrors(t *testing.T) {
204- clipboard := &fakeClipboard{}
205- resolver := &fakeImageResolver{}
206- saver := &fakeResolvedImageSaver{}
207- delegator := &fakeMenuActionDelegator{err: errors.New("boom")}
140+ clipboard := portmocks.NewMockClipboard(t)
141+ resolver := portmocks.NewMockImageDataResolver(t)
142+ saver := portmocks.NewMockResolvedImageSaver(t)
143+ delegator := portmocks.NewMockMenuActionDelegator(t)
144+ delegator.EXPECT().
145+ DelegateMenuAction(mock.Anything, port.MenuActionCopySelection, port.MenuContext{HasSelection: true}).
146+ Return(errors.New("boom")).
147+ Once()
208148 uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, saver, delegator)
209149
210150 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{
@@ -218,10 +158,14 @@ func TestExecuteContextMenuActionUseCase_WrapsDelegateErrors(t *testing.T) {
218158 }
219159
220160 func TestExecuteContextMenuActionUseCase_CopySelectionWritesSelectedText(t *testing.T) {
221- clipboard := &fakeClipboard{}
222- resolver := &fakeImageResolver{}
223- saver := &fakeResolvedImageSaver{}
224- delegator := &fakeMenuActionDelegator{}
161+ clipboard := portmocks.NewMockClipboard(t)
162+ resolver := portmocks.NewMockImageDataResolver(t)
163+ saver := portmocks.NewMockResolvedImageSaver(t)
164+ delegator := portmocks.NewMockMenuActionDelegator(t)
165+ clipboard.EXPECT().
166+ WriteText(mock.Anything, "selected text").
167+ Return(nil).
168+ Once()
225169 uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, saver, delegator)
226170
227171 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{
@@ -233,15 +177,19 @@ func TestExecuteContextMenuActionUseCase_CopySelectionWritesSelectedText(t *test
233177 })
234178
235179 require.NoError(t, err)
236- require.Equal(t, 1, clipboard.writeTextCalls)
237- require.Equal(t, "selected text", clipboard.text)
238- require.Zero(t, delegator.delegateCalls)
239180 }
240181
241182 func TestExecuteContextMenuActionUseCase_CopySelectionFallsBackToDelegatorWhenClipboardMissing(t *testing.T) {
242- resolver := &fakeImageResolver{}
243- saver := &fakeResolvedImageSaver{}
244- delegator := &fakeMenuActionDelegator{}
183+ resolver := portmocks.NewMockImageDataResolver(t)
184+ saver := portmocks.NewMockResolvedImageSaver(t)
185+ delegator := portmocks.NewMockMenuActionDelegator(t)
186+ delegator.EXPECT().
187+ DelegateMenuAction(mock.Anything, port.MenuActionCopySelection, port.MenuContext{
188+ HasSelection: true,
189+ SelectionText: "selected text",
190+ }).
191+ Return(nil).
192+ Once()
245193 uc := NewExecuteContextMenuActionUseCase(nil, resolver, saver, delegator)
246194
247195 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{
@@ -253,15 +201,17 @@ func TestExecuteContextMenuActionUseCase_CopySelectionFallsBackToDelegatorWhenCl
253201 })
254202
255203 require.NoError(t, err)
256- require.Equal(t, 1, delegator.delegateCalls)
257- require.Equal(t, port.MenuActionCopySelection, delegator.action)
258204 }
259205
260206 func TestExecuteContextMenuActionUseCase_CopyLinkWritesText(t *testing.T) {
261- clipboard := &fakeClipboard{}
262- resolver := &fakeImageResolver{}
263- saver := &fakeResolvedImageSaver{}
264- delegator := &fakeMenuActionDelegator{}
207+ clipboard := portmocks.NewMockClipboard(t)
208+ resolver := portmocks.NewMockImageDataResolver(t)
209+ saver := portmocks.NewMockResolvedImageSaver(t)
210+ delegator := portmocks.NewMockMenuActionDelegator(t)
211+ clipboard.EXPECT().
212+ WriteText(mock.Anything, "https://example.com/link").
213+ Return(nil).
214+ Once()
265215 uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, saver, delegator)
266216
267217 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{
@@ -270,10 +220,4 @@ func TestExecuteContextMenuActionUseCase_CopyLinkWritesText(t *testing.T) {
270220 })
271221
272222 require.NoError(t, err)
273- require.Equal(t, 1, clipboard.writeTextCalls)
274- require.Equal(t, "https://example.com/link", clipboard.text)
275- require.Zero(t, clipboard.writeImageCalls)
276- require.Zero(t, resolver.resolveCalls)
277- require.Zero(t, saver.saveCalls)
278- require.Zero(t, delegator.delegateCalls)
279223 }
@@ -6,79 +6,21 @@ import (
6 "testing"6 "testing"
7 7
8 "github.com/bnema/dumber/internal/application/port"8 "github.com/bnema/dumber/internal/application/port"
9+ portmocks "github.com/bnema/dumber/internal/application/port/mocks"
9 "github.com/bnema/dumber/internal/domain/entity"10 "github.com/bnema/dumber/internal/domain/entity"
11+ "github.com/stretchr/testify/mock"
10 "github.com/stretchr/testify/require"12 "github.com/stretchr/testify/require"
11 )13 )
12 14
13-type fakeClipboard struct {
14- writeTextCalls int
15- writeImageCalls int
16- text string
17- image entity.ImageData
18-}
19-
20-func (f *fakeClipboard) WriteText(_ context.Context, text string) error {
21- f.writeTextCalls++
22- f.text = text
23- return nil
24-}
25-
26-func (f *fakeClipboard) WriteImage(_ context.Context, image entity.ImageData) error {
27- f.writeImageCalls++
28- f.image = image
29- return nil
30-}
31-
32-func (*fakeClipboard) ReadText(context.Context) (string, error) { return "", nil }
33-func (*fakeClipboard) Clear(context.Context) error { return nil }
34-func (*fakeClipboard) HasText(context.Context) (bool, error) { return false, nil }
35-
36-type fakeImageResolver struct {
37- resolveCalls int
38- uri string
39- image entity.ImageData
40- err error
41-}
42-
43-func (f *fakeImageResolver) ResolveImageData(_ context.Context, uri string) (entity.ImageData, error) {
44- f.resolveCalls++
45- f.uri = uri
46- return f.image, f.err
47-}
48-
49-type fakeResolvedImageSaver struct {
50- saveCalls int
51- image entity.ImageData
52- menuContext port.MenuContext
53- err error
54-}
55-
56-func (f *fakeResolvedImageSaver) SaveResolvedImage(_ context.Context, image entity.ImageData, menuContext port.MenuContext) error {
57- f.saveCalls++
58- f.image = image
59- f.menuContext = menuContext
60- return f.err
61-}
62-
63-type fakeMenuActionDelegator struct {
64- delegateCalls int
65- action port.MenuAction
66- menuContext port.MenuContext
67- err error
68-}
69-
70-func (f *fakeMenuActionDelegator) DelegateMenuAction(_ context.Context, action port.MenuAction, menuContext port.MenuContext) error {
71- f.delegateCalls++
72- f.action = action
73- f.menuContext = menuContext
74- return f.err
75-}
76-
77 func TestExecuteContextMenuActionUseCase_CopyImageFailsWithoutResolvedData(t *testing.T) {15 func TestExecuteContextMenuActionUseCase_CopyImageFailsWithoutResolvedData(t *testing.T) {
78- clipboard := &fakeClipboard{}16+ clipboard := portmocks.NewMockClipboard(t)
79- resolver := &fakeImageResolver{}17+ resolver := portmocks.NewMockImageDataResolver(t)
80- saver := &fakeResolvedImageSaver{}18+ saver := portmocks.NewMockResolvedImageSaver(t)
81- delegator := &fakeMenuActionDelegator{}19+ delegator := portmocks.NewMockMenuActionDelegator(t)
20+ resolver.EXPECT().
21+ ResolveImageData(mock.Anything, "https://example.com/image.png").
22+ Return(entity.ImageData{}, nil).
23+ Once()
82 uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, saver, delegator)24 uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, saver, delegator)
83 25
84 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{26 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{
@@ -89,16 +31,22 @@ func TestExecuteContextMenuActionUseCase_CopyImageFailsWithoutResolvedData(t *te
89 require.Error(t, err)31 require.Error(t, err)
90 require.Contains(t, err.Error(), "copy image:")32 require.Contains(t, err.Error(), "copy image:")
91 require.Contains(t, err.Error(), "image data not available")33 require.Contains(t, err.Error(), "image data not available")
92- require.Zero(t, clipboard.writeImageCalls)
93- require.Zero(t, saver.saveCalls)
94- require.Zero(t, delegator.delegateCalls)
95 }34 }
96 35
97 func TestExecuteContextMenuActionUseCase_CopyImageWritesResolvedImage(t *testing.T) {36 func TestExecuteContextMenuActionUseCase_CopyImageWritesResolvedImage(t *testing.T) {
98- clipboard := &fakeClipboard{}37+ clipboard := portmocks.NewMockClipboard(t)
99- resolver := &fakeImageResolver{image: entity.ImageData{Bytes: []byte{1, 2, 3}, MimeType: "image/png"}}38+ resolver := portmocks.NewMockImageDataResolver(t)
100- saver := &fakeResolvedImageSaver{}39+ saver := portmocks.NewMockResolvedImageSaver(t)
101- delegator := &fakeMenuActionDelegator{}40+ delegator := portmocks.NewMockMenuActionDelegator(t)
41+ image := entity.ImageData{Bytes: []byte{1, 2, 3}, MimeType: "image/png"}
42+ resolver.EXPECT().
43+ ResolveImageData(mock.Anything, "https://example.com/image.png").
44+ Return(image, nil).
45+ Once()
46+ clipboard.EXPECT().
47+ WriteImage(mock.Anything, image).
48+ Return(nil).
49+ Once()
102 uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, saver, delegator)50 uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, saver, delegator)
103 51
104 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{52 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{
@@ -107,18 +55,12 @@ func TestExecuteContextMenuActionUseCase_CopyImageWritesResolvedImage(t *testing
107 })55 })
108 56
109 require.NoError(t, err)57 require.NoError(t, err)
110- require.Equal(t, 1, resolver.resolveCalls)
111- require.Equal(t, "https://example.com/image.png", resolver.uri)
112- require.Equal(t, 1, clipboard.writeImageCalls)
113- require.Equal(t, entity.ImageData{Bytes: []byte{1, 2, 3}, MimeType: "image/png"}, clipboard.image)
114- require.Zero(t, saver.saveCalls)
115- require.Zero(t, delegator.delegateCalls)
116 }58 }
117 59
118 func TestExecuteContextMenuActionUseCase_CopyImageFailsFastWithoutClipboard(t *testing.T) {60 func TestExecuteContextMenuActionUseCase_CopyImageFailsFastWithoutClipboard(t *testing.T) {
119- resolver := &fakeImageResolver{}61+ resolver := portmocks.NewMockImageDataResolver(t)
120- saver := &fakeResolvedImageSaver{}62+ saver := portmocks.NewMockResolvedImageSaver(t)
121- delegator := &fakeMenuActionDelegator{}63+ delegator := portmocks.NewMockMenuActionDelegator(t)
122 uc := NewExecuteContextMenuActionUseCase(nil, resolver, saver, delegator)64 uc := NewExecuteContextMenuActionUseCase(nil, resolver, saver, delegator)
123 65
124 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{66 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{
@@ -128,38 +70,37 @@ func TestExecuteContextMenuActionUseCase_CopyImageFailsFastWithoutClipboard(t *t
128 70
129 require.Error(t, err)71 require.Error(t, err)
130 require.Contains(t, err.Error(), "clipboard not available")72 require.Contains(t, err.Error(), "clipboard not available")
131- require.Zero(t, resolver.resolveCalls)
132- require.Zero(t, saver.saveCalls)
133- require.Zero(t, delegator.delegateCalls)
134 }73 }
135 74
136 func TestExecuteContextMenuActionUseCase_SaveImageDelegatesResolvedImage(t *testing.T) {75 func TestExecuteContextMenuActionUseCase_SaveImageDelegatesResolvedImage(t *testing.T) {
137- clipboard := &fakeClipboard{}76+ clipboard := portmocks.NewMockClipboard(t)
138- resolver := &fakeImageResolver{image: entity.ImageData{Bytes: []byte{1, 2, 3}, MimeType: "image/png"}}77+ resolver := portmocks.NewMockImageDataResolver(t)
139- saver := &fakeResolvedImageSaver{}78+ saver := portmocks.NewMockResolvedImageSaver(t)
140- delegator := &fakeMenuActionDelegator{}79+ delegator := portmocks.NewMockMenuActionDelegator(t)
80+ image := entity.ImageData{Bytes: []byte{1, 2, 3}, MimeType: "image/png"}
81+ menuContext := port.MenuContext{ImageURI: "https://example.com/image.png"}
82+ resolver.EXPECT().
83+ ResolveImageData(mock.Anything, menuContext.ImageURI).
84+ Return(image, nil).
85+ Once()
86+ saver.EXPECT().
87+ SaveResolvedImage(mock.Anything, image, menuContext).
88+ Return(nil).
89+ Once()
141 uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, saver, delegator)90 uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, saver, delegator)
142 91
143 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{92 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{
144 Action: port.MenuActionSaveImage,93 Action: port.MenuActionSaveImage,
145- Context: port.MenuContext{ImageURI: "https://example.com/image.png"},94+ Context: menuContext,
146 })95 })
147 96
148 require.NoError(t, err)97 require.NoError(t, err)
149- require.Equal(t, 1, resolver.resolveCalls)
150- require.Equal(t, "https://example.com/image.png", resolver.uri)
151- require.Equal(t, 1, saver.saveCalls)
152- require.Equal(t, entity.ImageData{Bytes: []byte{1, 2, 3}, MimeType: "image/png"}, saver.image)
153- require.Equal(t, port.MenuContext{ImageURI: "https://example.com/image.png"}, saver.menuContext)
154- require.Zero(t, clipboard.writeTextCalls)
155- require.Zero(t, clipboard.writeImageCalls)
156- require.Zero(t, delegator.delegateCalls)
157 }98 }
158 99
159 func TestExecuteContextMenuActionUseCase_SaveImageFailsFastWithoutSaver(t *testing.T) {100 func TestExecuteContextMenuActionUseCase_SaveImageFailsFastWithoutSaver(t *testing.T) {
160- clipboard := &fakeClipboard{}101+ clipboard := portmocks.NewMockClipboard(t)
161- resolver := &fakeImageResolver{}102+ resolver := portmocks.NewMockImageDataResolver(t)
162- delegator := &fakeMenuActionDelegator{}103+ delegator := portmocks.NewMockMenuActionDelegator(t)
163 uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, nil, delegator)104 uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, nil, delegator)
164 105
165 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{106 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{
@@ -169,42 +110,41 @@ func TestExecuteContextMenuActionUseCase_SaveImageFailsFastWithoutSaver(t *testi
169 110
170 require.Error(t, err)111 require.Error(t, err)
171 require.Contains(t, err.Error(), "image saver not available")112 require.Contains(t, err.Error(), "image saver not available")
172- require.Zero(t, resolver.resolveCalls)
173- require.Zero(t, delegator.delegateCalls)
174 }113 }
175 114
176 func TestExecuteContextMenuActionUseCase_DelegatesInspect(t *testing.T) {115 func TestExecuteContextMenuActionUseCase_DelegatesInspect(t *testing.T) {
177- clipboard := &fakeClipboard{}
178- resolver := &fakeImageResolver{}
179- saver := &fakeResolvedImageSaver{}
180- delegator := &fakeMenuActionDelegator{}
181- uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, saver, delegator)
182-
183 menuContext := port.MenuContext{116 menuContext := port.MenuContext{
184 PageURI: "https://example.com",117 PageURI: "https://example.com",
185 X: 17,118 X: 17,
186 Y: 42,119 Y: 42,
187 }120 }
121+ clipboard := portmocks.NewMockClipboard(t)
122+ resolver := portmocks.NewMockImageDataResolver(t)
123+ saver := portmocks.NewMockResolvedImageSaver(t)
124+ delegator := portmocks.NewMockMenuActionDelegator(t)
125+ delegator.EXPECT().
126+ DelegateMenuAction(mock.Anything, port.MenuActionInspectElement, menuContext).
127+ Return(nil).
128+ Once()
129+ uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, saver, delegator)
130+
188 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{131 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{
189 Action: port.MenuActionInspectElement,132 Action: port.MenuActionInspectElement,
190 Context: menuContext,133 Context: menuContext,
191 })134 })
192 135
193 require.NoError(t, err)136 require.NoError(t, err)
194- require.Equal(t, 1, delegator.delegateCalls)
195- require.Equal(t, port.MenuActionInspectElement, delegator.action)
196- require.Equal(t, menuContext, delegator.menuContext)
197- require.Zero(t, clipboard.writeTextCalls)
198- require.Zero(t, clipboard.writeImageCalls)
199- require.Zero(t, resolver.resolveCalls)
200- require.Zero(t, saver.saveCalls)
201 }137 }
202 138
203 func TestExecuteContextMenuActionUseCase_WrapsDelegateErrors(t *testing.T) {139 func TestExecuteContextMenuActionUseCase_WrapsDelegateErrors(t *testing.T) {
204- clipboard := &fakeClipboard{}140+ clipboard := portmocks.NewMockClipboard(t)
205- resolver := &fakeImageResolver{}141+ resolver := portmocks.NewMockImageDataResolver(t)
206- saver := &fakeResolvedImageSaver{}142+ saver := portmocks.NewMockResolvedImageSaver(t)
207- delegator := &fakeMenuActionDelegator{err: errors.New("boom")}143+ delegator := portmocks.NewMockMenuActionDelegator(t)
144+ delegator.EXPECT().
145+ DelegateMenuAction(mock.Anything, port.MenuActionCopySelection, port.MenuContext{HasSelection: true}).
146+ Return(errors.New("boom")).
147+ Once()
208 uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, saver, delegator)148 uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, saver, delegator)
209 149
210 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{150 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{
@@ -218,10 +158,14 @@ func TestExecuteContextMenuActionUseCase_WrapsDelegateErrors(t *testing.T) {
218 }158 }
219 159
220 func TestExecuteContextMenuActionUseCase_CopySelectionWritesSelectedText(t *testing.T) {160 func TestExecuteContextMenuActionUseCase_CopySelectionWritesSelectedText(t *testing.T) {
221- clipboard := &fakeClipboard{}161+ clipboard := portmocks.NewMockClipboard(t)
222- resolver := &fakeImageResolver{}162+ resolver := portmocks.NewMockImageDataResolver(t)
223- saver := &fakeResolvedImageSaver{}163+ saver := portmocks.NewMockResolvedImageSaver(t)
224- delegator := &fakeMenuActionDelegator{}164+ delegator := portmocks.NewMockMenuActionDelegator(t)
165+ clipboard.EXPECT().
166+ WriteText(mock.Anything, "selected text").
167+ Return(nil).
168+ Once()
225 uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, saver, delegator)169 uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, saver, delegator)
226 170
227 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{171 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{
@@ -233,15 +177,19 @@ func TestExecuteContextMenuActionUseCase_CopySelectionWritesSelectedText(t *test
233 })177 })
234 178
235 require.NoError(t, err)179 require.NoError(t, err)
236- require.Equal(t, 1, clipboard.writeTextCalls)
237- require.Equal(t, "selected text", clipboard.text)
238- require.Zero(t, delegator.delegateCalls)
239 }180 }
240 181
241 func TestExecuteContextMenuActionUseCase_CopySelectionFallsBackToDelegatorWhenClipboardMissing(t *testing.T) {182 func TestExecuteContextMenuActionUseCase_CopySelectionFallsBackToDelegatorWhenClipboardMissing(t *testing.T) {
242- resolver := &fakeImageResolver{}183+ resolver := portmocks.NewMockImageDataResolver(t)
243- saver := &fakeResolvedImageSaver{}184+ saver := portmocks.NewMockResolvedImageSaver(t)
244- delegator := &fakeMenuActionDelegator{}185+ delegator := portmocks.NewMockMenuActionDelegator(t)
186+ delegator.EXPECT().
187+ DelegateMenuAction(mock.Anything, port.MenuActionCopySelection, port.MenuContext{
188+ HasSelection: true,
189+ SelectionText: "selected text",
190+ }).
191+ Return(nil).
192+ Once()
245 uc := NewExecuteContextMenuActionUseCase(nil, resolver, saver, delegator)193 uc := NewExecuteContextMenuActionUseCase(nil, resolver, saver, delegator)
246 194
247 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{195 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{
@@ -253,15 +201,17 @@ func TestExecuteContextMenuActionUseCase_CopySelectionFallsBackToDelegatorWhenCl
253 })201 })
254 202
255 require.NoError(t, err)203 require.NoError(t, err)
256- require.Equal(t, 1, delegator.delegateCalls)
257- require.Equal(t, port.MenuActionCopySelection, delegator.action)
258 }204 }
259 205
260 func TestExecuteContextMenuActionUseCase_CopyLinkWritesText(t *testing.T) {206 func TestExecuteContextMenuActionUseCase_CopyLinkWritesText(t *testing.T) {
261- clipboard := &fakeClipboard{}207+ clipboard := portmocks.NewMockClipboard(t)
262- resolver := &fakeImageResolver{}208+ resolver := portmocks.NewMockImageDataResolver(t)
263- saver := &fakeResolvedImageSaver{}209+ saver := portmocks.NewMockResolvedImageSaver(t)
264- delegator := &fakeMenuActionDelegator{}210+ delegator := portmocks.NewMockMenuActionDelegator(t)
211+ clipboard.EXPECT().
212+ WriteText(mock.Anything, "https://example.com/link").
213+ Return(nil).
214+ Once()
265 uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, saver, delegator)215 uc := NewExecuteContextMenuActionUseCase(clipboard, resolver, saver, delegator)
266 216
267 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{217 err := uc.Execute(context.Background(), ExecuteContextMenuActionInput{
@@ -270,10 +220,4 @@ func TestExecuteContextMenuActionUseCase_CopyLinkWritesText(t *testing.T) {
270 })220 })
271 221
272 require.NoError(t, err)222 require.NoError(t, err)
273- require.Equal(t, 1, clipboard.writeTextCalls)
274- require.Equal(t, "https://example.com/link", clipboard.text)
275- require.Zero(t, clipboard.writeImageCalls)
276- require.Zero(t, resolver.resolveCalls)
277- require.Zero(t, saver.saveCalls)
278- require.Zero(t, delegator.delegateCalls)
279 }223 }
M internal/bootstrap/session_test.go +6 -10
@@ -5,6 +5,7 @@ import (
55 "testing"
66 "time"
77
8+ portmocks "github.com/bnema/dumber/internal/application/port/mocks"
89 "github.com/bnema/dumber/internal/application/usecase"
910 "github.com/bnema/dumber/internal/domain/entity"
1011 repomocks "github.com/bnema/dumber/internal/domain/repository/mocks"
@@ -13,20 +14,13 @@ import (
1314 "github.com/stretchr/testify/require"
1415 )
1516
16-type fakeProcessProbe struct {
17- alive map[int]bool
18-}
19-
20-func (p fakeProcessProbe) IsProcessAlive(_ context.Context, pid int) (bool, error) {
21- return p.alive[pid], nil
22-}
23-
2417 func TestRunSessionCleanup_DoesNotEndOtherActiveBrowserSessionsWithoutLivenessProof(t *testing.T) {
2518 t.Parallel()
2619
2720 ctx := context.Background()
2821 repo := repomocks.NewMockSessionRepository(t)
29- cleanupUC := usecase.NewCleanupSessionsUseCase(repo, fakeProcessProbe{alive: map[int]bool{1234: true}})
22+ probe := portmocks.NewMockSessionProcessProbe(t)
23+ cleanupUC := usecase.NewCleanupSessionsUseCase(repo, probe)
3024 cfg := config.DefaultConfig()
3125 logDir := t.TempDir()
3226
@@ -45,6 +39,7 @@ func TestRunSessionCleanup_DoesNotEndOtherActiveBrowserSessionsWithoutLivenessPr
4539 }
4640
4741 repo.EXPECT().GetRecent(mock.Anything, recentSessionsLimit).Return([]*entity.Session{current, live}, nil).Once()
42+ probe.EXPECT().IsProcessAlive(mock.Anything, livePID).Return(true, nil).Once()
4843 repo.EXPECT().DeleteExitedBefore(mock.Anything, mock.MatchedBy(func(time.Time) bool { return true })).Return(int64(0), nil).Once()
4944 repo.EXPECT().DeleteOldestExited(mock.Anything, cfg.Session.MaxExitedSessions).Return(int64(0), nil).Once()
5045
@@ -56,7 +51,8 @@ func TestRunSessionCleanup_NoPanicOnNilSessions(t *testing.T) {
5651
5752 ctx := context.Background()
5853 repo := repomocks.NewMockSessionRepository(t)
59- cleanupUC := usecase.NewCleanupSessionsUseCase(repo, fakeProcessProbe{})
54+ probe := portmocks.NewMockSessionProcessProbe(t)
55+ cleanupUC := usecase.NewCleanupSessionsUseCase(repo, probe)
6056 cfg := config.DefaultConfig()
6157 logDir := t.TempDir()
6258
@@ -5,6 +5,7 @@ import (
5 "testing"5 "testing"
6 "time"6 "time"
7 7
8+ portmocks "github.com/bnema/dumber/internal/application/port/mocks"
8 "github.com/bnema/dumber/internal/application/usecase"9 "github.com/bnema/dumber/internal/application/usecase"
9 "github.com/bnema/dumber/internal/domain/entity"10 "github.com/bnema/dumber/internal/domain/entity"
10 repomocks "github.com/bnema/dumber/internal/domain/repository/mocks"11 repomocks "github.com/bnema/dumber/internal/domain/repository/mocks"
@@ -13,20 +14,13 @@ import (
13 "github.com/stretchr/testify/require"14 "github.com/stretchr/testify/require"
14 )15 )
15 16
16-type fakeProcessProbe struct {
17- alive map[int]bool
18-}
19-
20-func (p fakeProcessProbe) IsProcessAlive(_ context.Context, pid int) (bool, error) {
21- return p.alive[pid], nil
22-}
23-
24 func TestRunSessionCleanup_DoesNotEndOtherActiveBrowserSessionsWithoutLivenessProof(t *testing.T) {17 func TestRunSessionCleanup_DoesNotEndOtherActiveBrowserSessionsWithoutLivenessProof(t *testing.T) {
25 t.Parallel()18 t.Parallel()
26 19
27 ctx := context.Background()20 ctx := context.Background()
28 repo := repomocks.NewMockSessionRepository(t)21 repo := repomocks.NewMockSessionRepository(t)
29- cleanupUC := usecase.NewCleanupSessionsUseCase(repo, fakeProcessProbe{alive: map[int]bool{1234: true}})22+ probe := portmocks.NewMockSessionProcessProbe(t)
23+ cleanupUC := usecase.NewCleanupSessionsUseCase(repo, probe)
30 cfg := config.DefaultConfig()24 cfg := config.DefaultConfig()
31 logDir := t.TempDir()25 logDir := t.TempDir()
32 26
@@ -45,6 +39,7 @@ func TestRunSessionCleanup_DoesNotEndOtherActiveBrowserSessionsWithoutLivenessPr
45 }39 }
46 40
47 repo.EXPECT().GetRecent(mock.Anything, recentSessionsLimit).Return([]*entity.Session{current, live}, nil).Once()41 repo.EXPECT().GetRecent(mock.Anything, recentSessionsLimit).Return([]*entity.Session{current, live}, nil).Once()
42+ probe.EXPECT().IsProcessAlive(mock.Anything, livePID).Return(true, nil).Once()
48 repo.EXPECT().DeleteExitedBefore(mock.Anything, mock.MatchedBy(func(time.Time) bool { return true })).Return(int64(0), nil).Once()43 repo.EXPECT().DeleteExitedBefore(mock.Anything, mock.MatchedBy(func(time.Time) bool { return true })).Return(int64(0), nil).Once()
49 repo.EXPECT().DeleteOldestExited(mock.Anything, cfg.Session.MaxExitedSessions).Return(int64(0), nil).Once()44 repo.EXPECT().DeleteOldestExited(mock.Anything, cfg.Session.MaxExitedSessions).Return(int64(0), nil).Once()
50 45
@@ -56,7 +51,8 @@ func TestRunSessionCleanup_NoPanicOnNilSessions(t *testing.T) {
56 51
57 ctx := context.Background()52 ctx := context.Background()
58 repo := repomocks.NewMockSessionRepository(t)53 repo := repomocks.NewMockSessionRepository(t)
59- cleanupUC := usecase.NewCleanupSessionsUseCase(repo, fakeProcessProbe{})54+ probe := portmocks.NewMockSessionProcessProbe(t)
55+ cleanupUC := usecase.NewCleanupSessionsUseCase(repo, probe)
60 cfg := config.DefaultConfig()56 cfg := config.DefaultConfig()
61 logDir := t.TempDir()57 logDir := t.TempDir()
62 58
M internal/infrastructure/webkit/resolved_image_saver_test.go +17 -19
@@ -7,38 +7,37 @@ import (
77 "testing"
88
99 "github.com/bnema/dumber/internal/application/port"
10+ portmocks "github.com/bnema/dumber/internal/application/port/mocks"
1011 "github.com/bnema/dumber/internal/domain/entity"
12+ "github.com/stretchr/testify/mock"
1113 "github.com/stretchr/testify/require"
1214 )
1315
14-type fakeDownloadPreparer struct {
15- calls int
16- input port.DownloadPrepareInput
17- output *port.DownloadPrepareOutput
18-}
19-
20-func (f *fakeDownloadPreparer) Execute(_ context.Context, input port.DownloadPrepareInput) *port.DownloadPrepareOutput {
21- f.calls++
22- f.input = input
23- return f.output
24-}
25-
2616 func TestResolvedImageSaver_SaveResolvedImage(t *testing.T) {
2717 downloadDir := t.TempDir()
2818 destPath := filepath.Join(downloadDir, "image.png")
29- preparer := &fakeDownloadPreparer{output: &port.DownloadPrepareOutput{Filename: "image.png", DestinationPath: destPath}}
19+ preparer := portmocks.NewMockDownloadPreparer(t)
20+ preparer.EXPECT().
21+ Execute(mock.Anything, mock.MatchedBy(func(input port.DownloadPrepareInput) bool {
22+ return input.DownloadDir == downloadDir &&
23+ input.Response.GetUri() == "https://example.com/assets/image" &&
24+ input.Response.GetMimeType() == "image/jpeg"
25+ })).
26+ Return(&port.DownloadPrepareOutput{Filename: "image.png", DestinationPath: destPath}).
27+ Once()
3028 saver := NewResolvedImageSaver(preparer, downloadDir)
3129
3230 err := saver.SaveResolvedImage(context.Background(), entity.ImageData{Bytes: []byte{1, 2, 3, 4}, MimeType: "image/jpeg"}, port.MenuContext{ImageURI: "https://example.com/assets/image"})
3331 require.NoError(t, err)
34- require.Equal(t, downloadDir, preparer.input.DownloadDir)
35- require.Equal(t, "https://example.com/assets/image", preparer.input.Response.GetUri())
36- require.Equal(t, "image/jpeg", preparer.input.Response.GetMimeType())
3732 require.Equal(t, []byte{1, 2, 3, 4}, readFile(t, destPath))
3833 }
3934
4035 func TestResolvedImageSaver_SaveResolvedImageRequiresDestination(t *testing.T) {
41- preparer := &fakeDownloadPreparer{output: &port.DownloadPrepareOutput{Filename: "image.png"}}
36+ preparer := portmocks.NewMockDownloadPreparer(t)
37+ preparer.EXPECT().
38+ Execute(mock.Anything, mock.Anything).
39+ Return(&port.DownloadPrepareOutput{Filename: "image.png"}).
40+ Once()
4241 saver := NewResolvedImageSaver(preparer, t.TempDir())
4342
4443 err := saver.SaveResolvedImage(context.Background(), entity.ImageData{Bytes: []byte{1}}, port.MenuContext{})
@@ -47,13 +46,12 @@ func TestResolvedImageSaver_SaveResolvedImageRequiresDestination(t *testing.T) {
4746 }
4847
4948 func TestResolvedImageSaver_SaveResolvedImageRejectsEmptyBytes(t *testing.T) {
50- preparer := &fakeDownloadPreparer{output: &port.DownloadPrepareOutput{Filename: "image.png", DestinationPath: filepath.Join(t.TempDir(), "image.png")}}
49+ preparer := portmocks.NewMockDownloadPreparer(t)
5150 saver := NewResolvedImageSaver(preparer, t.TempDir())
5251
5352 err := saver.SaveResolvedImage(context.Background(), entity.ImageData{}, port.MenuContext{})
5453 require.Error(t, err)
5554 require.Contains(t, err.Error(), "empty image data")
56- require.Zero(t, preparer.calls)
5755 }
5856
5957 func readFile(t *testing.T, path string) []byte {
@@ -7,38 +7,37 @@ import (
7 "testing"7 "testing"
8 8
9 "github.com/bnema/dumber/internal/application/port"9 "github.com/bnema/dumber/internal/application/port"
10+ portmocks "github.com/bnema/dumber/internal/application/port/mocks"
10 "github.com/bnema/dumber/internal/domain/entity"11 "github.com/bnema/dumber/internal/domain/entity"
12+ "github.com/stretchr/testify/mock"
11 "github.com/stretchr/testify/require"13 "github.com/stretchr/testify/require"
12 )14 )
13 15
14-type fakeDownloadPreparer struct {
15- calls int
16- input port.DownloadPrepareInput
17- output *port.DownloadPrepareOutput
18-}
19-
20-func (f *fakeDownloadPreparer) Execute(_ context.Context, input port.DownloadPrepareInput) *port.DownloadPrepareOutput {
21- f.calls++
22- f.input = input
23- return f.output
24-}
25-
26 func TestResolvedImageSaver_SaveResolvedImage(t *testing.T) {16 func TestResolvedImageSaver_SaveResolvedImage(t *testing.T) {
27 downloadDir := t.TempDir()17 downloadDir := t.TempDir()
28 destPath := filepath.Join(downloadDir, "image.png")18 destPath := filepath.Join(downloadDir, "image.png")
29- preparer := &fakeDownloadPreparer{output: &port.DownloadPrepareOutput{Filename: "image.png", DestinationPath: destPath}}19+ preparer := portmocks.NewMockDownloadPreparer(t)
20+ preparer.EXPECT().
21+ Execute(mock.Anything, mock.MatchedBy(func(input port.DownloadPrepareInput) bool {
22+ return input.DownloadDir == downloadDir &&
23+ input.Response.GetUri() == "https://example.com/assets/image" &&
24+ input.Response.GetMimeType() == "image/jpeg"
25+ })).
26+ Return(&port.DownloadPrepareOutput{Filename: "image.png", DestinationPath: destPath}).
27+ Once()
30 saver := NewResolvedImageSaver(preparer, downloadDir)28 saver := NewResolvedImageSaver(preparer, downloadDir)
31 29
32 err := saver.SaveResolvedImage(context.Background(), entity.ImageData{Bytes: []byte{1, 2, 3, 4}, MimeType: "image/jpeg"}, port.MenuContext{ImageURI: "https://example.com/assets/image"})30 err := saver.SaveResolvedImage(context.Background(), entity.ImageData{Bytes: []byte{1, 2, 3, 4}, MimeType: "image/jpeg"}, port.MenuContext{ImageURI: "https://example.com/assets/image"})
33 require.NoError(t, err)31 require.NoError(t, err)
34- require.Equal(t, downloadDir, preparer.input.DownloadDir)
35- require.Equal(t, "https://example.com/assets/image", preparer.input.Response.GetUri())
36- require.Equal(t, "image/jpeg", preparer.input.Response.GetMimeType())
37 require.Equal(t, []byte{1, 2, 3, 4}, readFile(t, destPath))32 require.Equal(t, []byte{1, 2, 3, 4}, readFile(t, destPath))
38 }33 }
39 34
40 func TestResolvedImageSaver_SaveResolvedImageRequiresDestination(t *testing.T) {35 func TestResolvedImageSaver_SaveResolvedImageRequiresDestination(t *testing.T) {
41- preparer := &fakeDownloadPreparer{output: &port.DownloadPrepareOutput{Filename: "image.png"}}36+ preparer := portmocks.NewMockDownloadPreparer(t)
37+ preparer.EXPECT().
38+ Execute(mock.Anything, mock.Anything).
39+ Return(&port.DownloadPrepareOutput{Filename: "image.png"}).
40+ Once()
42 saver := NewResolvedImageSaver(preparer, t.TempDir())41 saver := NewResolvedImageSaver(preparer, t.TempDir())
43 42
44 err := saver.SaveResolvedImage(context.Background(), entity.ImageData{Bytes: []byte{1}}, port.MenuContext{})43 err := saver.SaveResolvedImage(context.Background(), entity.ImageData{Bytes: []byte{1}}, port.MenuContext{})
@@ -47,13 +46,12 @@ func TestResolvedImageSaver_SaveResolvedImageRequiresDestination(t *testing.T) {
47 }46 }
48 47
49 func TestResolvedImageSaver_SaveResolvedImageRejectsEmptyBytes(t *testing.T) {48 func TestResolvedImageSaver_SaveResolvedImageRejectsEmptyBytes(t *testing.T) {
50- preparer := &fakeDownloadPreparer{output: &port.DownloadPrepareOutput{Filename: "image.png", DestinationPath: filepath.Join(t.TempDir(), "image.png")}}49+ preparer := portmocks.NewMockDownloadPreparer(t)
51 saver := NewResolvedImageSaver(preparer, t.TempDir())50 saver := NewResolvedImageSaver(preparer, t.TempDir())
52 51
53 err := saver.SaveResolvedImage(context.Background(), entity.ImageData{}, port.MenuContext{})52 err := saver.SaveResolvedImage(context.Background(), entity.ImageData{}, port.MenuContext{})
54 require.Error(t, err)53 require.Error(t, err)
55 require.Contains(t, err.Error(), "empty image data")54 require.Contains(t, err.Error(), "empty image data")
56- require.Zero(t, preparer.calls)
57 }55 }
58 56
59 func readFile(t *testing.T, path string) []byte {57 func readFile(t *testing.T, path string) []byte {
M internal/ui/app_browser_launch_test.go +34 -36
@@ -21,7 +21,7 @@ import (
2121 portmocks "github.com/bnema/dumber/internal/application/port/mocks"
2222 "github.com/bnema/dumber/internal/application/usecase"
2323 "github.com/bnema/dumber/internal/domain/entity"
24- "github.com/bnema/dumber/internal/domain/repository"
24+ repomocks "github.com/bnema/dumber/internal/domain/repository/mocks"
2525 "github.com/bnema/dumber/internal/infrastructure/config"
2626 "github.com/bnema/dumber/internal/shared/syncdispatch"
2727 "github.com/bnema/dumber/internal/ui/component"
@@ -35,6 +35,7 @@ import (
3535 "github.com/bnema/puregotk/v4/gdk"
3636 "github.com/bnema/puregotk/v4/gio"
3737 "github.com/bnema/puregotk/v4/gtk"
38+ "github.com/stretchr/testify/mock"
3839 "github.com/stretchr/testify/require"
3940 )
4041
@@ -62,29 +63,33 @@ func (c *testCloser) Close() error {
6263 return nil
6364 }
6465
65-type fakeSessionStateRepo struct {
66- state *entity.SessionState
67-}
68-
69-var _ repository.SessionStateRepository = (*fakeSessionStateRepo)(nil)
70-
71-func (r *fakeSessionStateRepo) SaveSnapshot(context.Context, *entity.SessionState) error { return nil }
72-
73-func (r *fakeSessionStateRepo) GetSnapshot(context.Context, entity.SessionID) (*entity.SessionState, error) {
74- return r.state, nil
66+func testPathIsSocket(path string) bool {
67+ info, err := os.Stat(path)
68+ return err == nil && info.Mode()&os.ModeSocket != 0
7569 }
7670
77-func (r *fakeSessionStateRepo) DeleteSnapshot(context.Context, entity.SessionID) error { return nil }
78-
79-func (r *fakeSessionStateRepo) GetAllSnapshots(context.Context) ([]*entity.SessionState, error) {
80- return nil, nil
71+func mockSessionStateRepoWithSnapshot(t *testing.T, sessionID entity.SessionID, state *entity.SessionState) *repomocks.MockSessionStateRepository {
72+ t.Helper()
73+ repo := repomocks.NewMockSessionStateRepository(t)
74+ repo.EXPECT().
75+ GetSnapshot(mock.Anything, sessionID).
76+ Return(state, nil).
77+ Once()
78+ return repo
8179 }
8280
83-func (r *fakeSessionStateRepo) GetTotalSnapshotsSize(context.Context) (int64, error) { return 0, nil }
84-
85-func testPathIsSocket(path string) bool {
86- info, err := os.Stat(path)
87- return err == nil && info.Mode()&os.ModeSocket != 0
81+func mockZoomRepo(t *testing.T) *repomocks.MockZoomRepository {
82+ t.Helper()
83+ repo := repomocks.NewMockZoomRepository(t)
84+ repo.EXPECT().
85+ Get(mock.Anything, mock.Anything).
86+ Return((*entity.ZoomLevel)(nil), nil).
87+ Maybe()
88+ repo.EXPECT().
89+ Set(mock.Anything, mock.Anything).
90+ Return(nil).
91+ Maybe()
92+ return repo
8893 }
8994
9095 func testGDKBackendAllows(backend string) bool {
@@ -1128,7 +1133,7 @@ func TestApp_RestoreSessionHandlesEmptyWindowSnapshotAsSuccess(t *testing.T) {
11281133 app := &App{
11291134 deps: &Dependencies{
11301135 Config: &config.Config{},
1131- SessionStateRepo: &fakeSessionStateRepo{state: state},
1136+ SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
11321137 },
11331138 tabs: entity.NewTabList(),
11341139 browserWindows: map[string]*browserWindow{staleWindow.id: staleWindow},
@@ -1169,7 +1174,7 @@ func TestApp_CreateInitialTabNoFallbackAfterEmptyWindowRestore(t *testing.T) {
11691174 Config: &config.Config{},
11701175 RestoreSessionID: string(sessionID),
11711176 InitialURL: "https://fallback.example",
1172- SessionStateRepo: &fakeSessionStateRepo{state: state},
1177+ SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
11731178 },
11741179 tabs: entity.NewTabList(),
11751180 tabsUC: tabsUC,
@@ -1209,7 +1214,7 @@ func TestApp_RestoreSessionClearsStaleUIStateBeforeApplyingRestoredTabs(t *testi
12091214 app := &App{
12101215 deps: &Dependencies{
12111216 Config: &config.Config{},
1212- SessionStateRepo: &fakeSessionStateRepo{state: entity.SnapshotFromTabList(restoredSessionID, restoredTabs)},
1217+ SessionStateRepo: mockSessionStateRepoWithSnapshot(t, restoredSessionID, entity.SnapshotFromTabList(restoredSessionID, restoredTabs)),
12131218 },
12141219 mainWindow: mainWindow,
12151220 widgetFactory: layout.NewGtkWidgetFactory(),
@@ -1276,7 +1281,7 @@ func TestApp_RestoreSessionWiresStackedPaneTitleBarCallbacks(t *testing.T) {
12761281 app := &App{
12771282 deps: &Dependencies{
12781283 Config: &config.Config{},
1279- SessionStateRepo: &fakeSessionStateRepo{state: entity.SnapshotFromTabList(stackedSessionID, stackedTabs)},
1284+ SessionStateRepo: mockSessionStateRepoWithSnapshot(t, stackedSessionID, entity.SnapshotFromTabList(stackedSessionID, stackedTabs)),
12801285 },
12811286 mainWindow: mainWindow,
12821287 widgetFactory: layout.NewGtkWidgetFactory(),
@@ -2615,13 +2620,6 @@ func TestApp_AttachPopupToTabReleasesRegistrationWhenWrapFails(t *testing.T) {
26152620 }
26162621 }
26172622
2618-type fakeZoomRepo struct{}
2619-
2620-func (f *fakeZoomRepo) Get(context.Context, string) (*entity.ZoomLevel, error) { return nil, nil }
2621-func (f *fakeZoomRepo) Set(context.Context, *entity.ZoomLevel) error { return nil }
2622-func (f *fakeZoomRepo) Delete(context.Context, string) error { return nil }
2623-func (f *fakeZoomRepo) GetAll(context.Context) ([]*entity.ZoomLevel, error) { return nil, nil }
2624-
26252623 func TestApp_BrowserWindowWebViewActionsIgnoreStaleFocusedWindow(t *testing.T) {
26262624 ctx := context.Background()
26272625
@@ -2857,7 +2855,7 @@ func TestApp_DispatchBrowserWindowActionUsesSourceWindow(t *testing.T) {
28572855 lastFocusedWindowID: first.id, // stale! should NOT be used
28582856 contentCoord: contentCoord,
28592857 navCoord: navCoord,
2860- deps: &Dependencies{ZoomUC: usecase.NewManageZoomUseCase(&fakeZoomRepo{}, 1.0, nil)},
2858+ deps: &Dependencies{ZoomUC: usecase.NewManageZoomUseCase(mockZoomRepo(t), 1.0, nil)},
28612859 workspaceViews: map[entity.TabID]*component.WorkspaceView{
28622860 tab1.ID: &component.WorkspaceView{},
28632861 tab2.ID: &component.WorkspaceView{},
@@ -2907,7 +2905,7 @@ func TestApp_DispatchBrowserWindowActionZoomInSupportsFileURLs(t *testing.T) {
29072905 tabs: entity.NewTabList(),
29082906 windowForTab: map[entity.TabID]*browserWindow{tab.ID: bw},
29092907 contentCoord: contentCoord,
2910- deps: &Dependencies{ZoomUC: usecase.NewManageZoomUseCase(&fakeZoomRepo{}, 1.0, nil)},
2908+ deps: &Dependencies{ZoomUC: usecase.NewManageZoomUseCase(mockZoomRepo(t), 1.0, nil)},
29112909 workspaceViews: map[entity.TabID]*component.WorkspaceView{
29122910 tab.ID: &component.WorkspaceView{},
29132911 },
@@ -3266,7 +3264,7 @@ func TestApp_RestoreSessionDoesNotLeakStaleWindowsIntoTabMerge(t *testing.T) {
32663264 app := &App{
32673265 deps: &Dependencies{
32683266 Config: &config.Config{},
3269- SessionStateRepo: &fakeSessionStateRepo{state: state},
3267+ SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
32703268 },
32713269 mainWindow: mainWindow,
32723270 browserWindows: map[string]*browserWindow{
@@ -3346,7 +3344,7 @@ func TestApp_RestoreSessionHonorsActiveWindowIndex(t *testing.T) {
33463344 app := &App{
33473345 deps: &Dependencies{
33483346 Config: &config.Config{},
3349- SessionStateRepo: &fakeSessionStateRepo{state: state},
3347+ SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
33503348 },
33513349 mainWindow: mainWindow,
33523350 widgetFactory: layout.NewGtkWidgetFactory(),
@@ -3413,7 +3411,7 @@ func TestApp_RestoreSessionFailsOnAdditionalWindowCreationError(t *testing.T) {
34133411 app := &App{
34143412 deps: &Dependencies{
34153413 Config: &config.Config{},
3416- SessionStateRepo: &fakeSessionStateRepo{state: state},
3414+ SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
34173415 },
34183416 mainWindow: mainWindow,
34193417 browserWindows: map[string]*browserWindow{firstBW.id: firstBW},
@@ -21,7 +21,7 @@ import (
21 portmocks "github.com/bnema/dumber/internal/application/port/mocks"21 portmocks "github.com/bnema/dumber/internal/application/port/mocks"
22 "github.com/bnema/dumber/internal/application/usecase"22 "github.com/bnema/dumber/internal/application/usecase"
23 "github.com/bnema/dumber/internal/domain/entity"23 "github.com/bnema/dumber/internal/domain/entity"
24- "github.com/bnema/dumber/internal/domain/repository"24+ repomocks "github.com/bnema/dumber/internal/domain/repository/mocks"
25 "github.com/bnema/dumber/internal/infrastructure/config"25 "github.com/bnema/dumber/internal/infrastructure/config"
26 "github.com/bnema/dumber/internal/shared/syncdispatch"26 "github.com/bnema/dumber/internal/shared/syncdispatch"
27 "github.com/bnema/dumber/internal/ui/component"27 "github.com/bnema/dumber/internal/ui/component"
@@ -35,6 +35,7 @@ import (
35 "github.com/bnema/puregotk/v4/gdk"35 "github.com/bnema/puregotk/v4/gdk"
36 "github.com/bnema/puregotk/v4/gio"36 "github.com/bnema/puregotk/v4/gio"
37 "github.com/bnema/puregotk/v4/gtk"37 "github.com/bnema/puregotk/v4/gtk"
38+ "github.com/stretchr/testify/mock"
38 "github.com/stretchr/testify/require"39 "github.com/stretchr/testify/require"
39 )40 )
40 41
@@ -62,29 +63,33 @@ func (c *testCloser) Close() error {
62 return nil63 return nil
63 }64 }
64 65
65-type fakeSessionStateRepo struct {66+func testPathIsSocket(path string) bool {
66- state *entity.SessionState67+ info, err := os.Stat(path)
67-}68+ return err == nil && info.Mode()&os.ModeSocket != 0
68-
69-var _ repository.SessionStateRepository = (*fakeSessionStateRepo)(nil)
70-
71-func (r *fakeSessionStateRepo) SaveSnapshot(context.Context, *entity.SessionState) error { return nil }
72-
73-func (r *fakeSessionStateRepo) GetSnapshot(context.Context, entity.SessionID) (*entity.SessionState, error) {
74- return r.state, nil
75 }69 }
76 70
77-func (r *fakeSessionStateRepo) DeleteSnapshot(context.Context, entity.SessionID) error { return nil }71+func mockSessionStateRepoWithSnapshot(t *testing.T, sessionID entity.SessionID, state *entity.SessionState) *repomocks.MockSessionStateRepository {
78-72+ t.Helper()
79-func (r *fakeSessionStateRepo) GetAllSnapshots(context.Context) ([]*entity.SessionState, error) {73+ repo := repomocks.NewMockSessionStateRepository(t)
80- return nil, nil74+ repo.EXPECT().
75+ GetSnapshot(mock.Anything, sessionID).
76+ Return(state, nil).
77+ Once()
78+ return repo
81 }79 }
82 80
83-func (r *fakeSessionStateRepo) GetTotalSnapshotsSize(context.Context) (int64, error) { return 0, nil }81+func mockZoomRepo(t *testing.T) *repomocks.MockZoomRepository {
84-82+ t.Helper()
85-func testPathIsSocket(path string) bool {83+ repo := repomocks.NewMockZoomRepository(t)
86- info, err := os.Stat(path)84+ repo.EXPECT().
87- return err == nil && info.Mode()&os.ModeSocket != 085+ Get(mock.Anything, mock.Anything).
86+ Return((*entity.ZoomLevel)(nil), nil).
87+ Maybe()
88+ repo.EXPECT().
89+ Set(mock.Anything, mock.Anything).
90+ Return(nil).
91+ Maybe()
92+ return repo
88 }93 }
89 94
90 func testGDKBackendAllows(backend string) bool {95 func testGDKBackendAllows(backend string) bool {
@@ -1128,7 +1133,7 @@ func TestApp_RestoreSessionHandlesEmptyWindowSnapshotAsSuccess(t *testing.T) {
1128 app := &App{1133 app := &App{
1129 deps: &Dependencies{1134 deps: &Dependencies{
1130 Config: &config.Config{},1135 Config: &config.Config{},
1131- SessionStateRepo: &fakeSessionStateRepo{state: state},1136+ SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
1132 },1137 },
1133 tabs: entity.NewTabList(),1138 tabs: entity.NewTabList(),
1134 browserWindows: map[string]*browserWindow{staleWindow.id: staleWindow},1139 browserWindows: map[string]*browserWindow{staleWindow.id: staleWindow},
@@ -1169,7 +1174,7 @@ func TestApp_CreateInitialTabNoFallbackAfterEmptyWindowRestore(t *testing.T) {
1169 Config: &config.Config{},1174 Config: &config.Config{},
1170 RestoreSessionID: string(sessionID),1175 RestoreSessionID: string(sessionID),
1171 InitialURL: "https://fallback.example",1176 InitialURL: "https://fallback.example",
1172- SessionStateRepo: &fakeSessionStateRepo{state: state},1177+ SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
1173 },1178 },
1174 tabs: entity.NewTabList(),1179 tabs: entity.NewTabList(),
1175 tabsUC: tabsUC,1180 tabsUC: tabsUC,
@@ -1209,7 +1214,7 @@ func TestApp_RestoreSessionClearsStaleUIStateBeforeApplyingRestoredTabs(t *testi
1209 app := &App{1214 app := &App{
1210 deps: &Dependencies{1215 deps: &Dependencies{
1211 Config: &config.Config{},1216 Config: &config.Config{},
1212- SessionStateRepo: &fakeSessionStateRepo{state: entity.SnapshotFromTabList(restoredSessionID, restoredTabs)},1217+ SessionStateRepo: mockSessionStateRepoWithSnapshot(t, restoredSessionID, entity.SnapshotFromTabList(restoredSessionID, restoredTabs)),
1213 },1218 },
1214 mainWindow: mainWindow,1219 mainWindow: mainWindow,
1215 widgetFactory: layout.NewGtkWidgetFactory(),1220 widgetFactory: layout.NewGtkWidgetFactory(),
@@ -1276,7 +1281,7 @@ func TestApp_RestoreSessionWiresStackedPaneTitleBarCallbacks(t *testing.T) {
1276 app := &App{1281 app := &App{
1277 deps: &Dependencies{1282 deps: &Dependencies{
1278 Config: &config.Config{},1283 Config: &config.Config{},
1279- SessionStateRepo: &fakeSessionStateRepo{state: entity.SnapshotFromTabList(stackedSessionID, stackedTabs)},1284+ SessionStateRepo: mockSessionStateRepoWithSnapshot(t, stackedSessionID, entity.SnapshotFromTabList(stackedSessionID, stackedTabs)),
1280 },1285 },
1281 mainWindow: mainWindow,1286 mainWindow: mainWindow,
1282 widgetFactory: layout.NewGtkWidgetFactory(),1287 widgetFactory: layout.NewGtkWidgetFactory(),
@@ -2615,13 +2620,6 @@ func TestApp_AttachPopupToTabReleasesRegistrationWhenWrapFails(t *testing.T) {
2615 }2620 }
2616 }2621 }
2617 2622
2618-type fakeZoomRepo struct{}
2619-
2620-func (f *fakeZoomRepo) Get(context.Context, string) (*entity.ZoomLevel, error) { return nil, nil }
2621-func (f *fakeZoomRepo) Set(context.Context, *entity.ZoomLevel) error { return nil }
2622-func (f *fakeZoomRepo) Delete(context.Context, string) error { return nil }
2623-func (f *fakeZoomRepo) GetAll(context.Context) ([]*entity.ZoomLevel, error) { return nil, nil }
2624-
2625 func TestApp_BrowserWindowWebViewActionsIgnoreStaleFocusedWindow(t *testing.T) {2623 func TestApp_BrowserWindowWebViewActionsIgnoreStaleFocusedWindow(t *testing.T) {
2626 ctx := context.Background()2624 ctx := context.Background()
2627 2625
@@ -2857,7 +2855,7 @@ func TestApp_DispatchBrowserWindowActionUsesSourceWindow(t *testing.T) {
2857 lastFocusedWindowID: first.id, // stale! should NOT be used2855 lastFocusedWindowID: first.id, // stale! should NOT be used
2858 contentCoord: contentCoord,2856 contentCoord: contentCoord,
2859 navCoord: navCoord,2857 navCoord: navCoord,
2860- deps: &Dependencies{ZoomUC: usecase.NewManageZoomUseCase(&fakeZoomRepo{}, 1.0, nil)},2858+ deps: &Dependencies{ZoomUC: usecase.NewManageZoomUseCase(mockZoomRepo(t), 1.0, nil)},
2861 workspaceViews: map[entity.TabID]*component.WorkspaceView{2859 workspaceViews: map[entity.TabID]*component.WorkspaceView{
2862 tab1.ID: &component.WorkspaceView{},2860 tab1.ID: &component.WorkspaceView{},
2863 tab2.ID: &component.WorkspaceView{},2861 tab2.ID: &component.WorkspaceView{},
@@ -2907,7 +2905,7 @@ func TestApp_DispatchBrowserWindowActionZoomInSupportsFileURLs(t *testing.T) {
2907 tabs: entity.NewTabList(),2905 tabs: entity.NewTabList(),
2908 windowForTab: map[entity.TabID]*browserWindow{tab.ID: bw},2906 windowForTab: map[entity.TabID]*browserWindow{tab.ID: bw},
2909 contentCoord: contentCoord,2907 contentCoord: contentCoord,
2910- deps: &Dependencies{ZoomUC: usecase.NewManageZoomUseCase(&fakeZoomRepo{}, 1.0, nil)},2908+ deps: &Dependencies{ZoomUC: usecase.NewManageZoomUseCase(mockZoomRepo(t), 1.0, nil)},
2911 workspaceViews: map[entity.TabID]*component.WorkspaceView{2909 workspaceViews: map[entity.TabID]*component.WorkspaceView{
2912 tab.ID: &component.WorkspaceView{},2910 tab.ID: &component.WorkspaceView{},
2913 },2911 },
@@ -3266,7 +3264,7 @@ func TestApp_RestoreSessionDoesNotLeakStaleWindowsIntoTabMerge(t *testing.T) {
3266 app := &App{3264 app := &App{
3267 deps: &Dependencies{3265 deps: &Dependencies{
3268 Config: &config.Config{},3266 Config: &config.Config{},
3269- SessionStateRepo: &fakeSessionStateRepo{state: state},3267+ SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
3270 },3268 },
3271 mainWindow: mainWindow,3269 mainWindow: mainWindow,
3272 browserWindows: map[string]*browserWindow{3270 browserWindows: map[string]*browserWindow{
@@ -3346,7 +3344,7 @@ func TestApp_RestoreSessionHonorsActiveWindowIndex(t *testing.T) {
3346 app := &App{3344 app := &App{
3347 deps: &Dependencies{3345 deps: &Dependencies{
3348 Config: &config.Config{},3346 Config: &config.Config{},
3349- SessionStateRepo: &fakeSessionStateRepo{state: state},3347+ SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
3350 },3348 },
3351 mainWindow: mainWindow,3349 mainWindow: mainWindow,
3352 widgetFactory: layout.NewGtkWidgetFactory(),3350 widgetFactory: layout.NewGtkWidgetFactory(),
@@ -3413,7 +3411,7 @@ func TestApp_RestoreSessionFailsOnAdditionalWindowCreationError(t *testing.T) {
3413 app := &App{3411 app := &App{
3414 deps: &Dependencies{3412 deps: &Dependencies{
3415 Config: &config.Config{},3413 Config: &config.Config{},
3416- SessionStateRepo: &fakeSessionStateRepo{state: state},3414+ SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
3417 },3415 },
3418 mainWindow: mainWindow,3416 mainWindow: mainWindow,
3419 browserWindows: map[string]*browserWindow{firstBW.id: firstBW},3417 browserWindows: map[string]*browserWindow{firstBW.id: firstBW},
M internal/ui/component/history_sidebar_search_test.go +26 -48
@@ -3,16 +3,17 @@ package component
33 import (
44 "context"
55 "errors"
6- "fmt"
76 "sync"
87 "testing"
98 "time"
109
1110 "github.com/bnema/dumber/internal/application/dto"
11+ portmocks "github.com/bnema/dumber/internal/application/port/mocks"
1212 "github.com/bnema/dumber/internal/domain/entity"
1313 "github.com/bnema/puregotk/v4/glib"
1414 "github.com/rs/zerolog"
1515 "github.com/stretchr/testify/assert"
16+ "github.com/stretchr/testify/mock"
1617 "github.com/stretchr/testify/require"
1718 )
1819
@@ -155,49 +156,20 @@ func TestApplyDeletedEntryLocked_RecomputesBrowseState(t *testing.T) {
155156 assert.Equal(t, historyDisplayRowEntry, hs.displayRows[2].Kind)
156157 }
157158
158-// =============================================================================
159-// Async search seam: controllable history port fake
160-// =============================================================================
161-
162-type fakeHistorySidebarHistory struct {
163- getRecentFn func(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error)
164- searchFn func(ctx context.Context, input dto.HistorySearchInput) (*dto.HistorySearchOutput, error)
165- deleteFn func(ctx context.Context, id int64) error
166-}
167-
168-func (f *fakeHistorySidebarHistory) GetRecent(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error) {
169- if f.getRecentFn != nil {
170- return f.getRecentFn(ctx, limit, offset)
171- }
172- return nil, fmt.Errorf("unexpected GetRecent call in fakeHistorySidebarHistory")
173-}
174-
175-func (f *fakeHistorySidebarHistory) Search(ctx context.Context, input dto.HistorySearchInput) (*dto.HistorySearchOutput, error) {
176- if f.searchFn != nil {
177- return f.searchFn(ctx, input)
178- }
179- return &dto.HistorySearchOutput{Matches: []entity.HistoryMatch{}}, nil
180-}
181-
182-func (f *fakeHistorySidebarHistory) Delete(ctx context.Context, id int64) error {
183- if f.deleteFn != nil {
184- return f.deleteFn(ctx, id)
185- }
186- return nil
187-}
188-
189159 func TestApplySearchResults_StaleGenerationDropsResultsAfterSearch(t *testing.T) {
190160 searchCalled := make(chan struct{}, 1)
191161 idleCalled := make(chan glib.SourceFunc, 1)
192162
193- history := &fakeHistorySidebarHistory{
194- searchFn: func(context.Context, dto.HistorySearchInput) (*dto.HistorySearchOutput, error) {
163+ history := portmocks.NewMockHistorySidebarHistory(t)
164+ history.EXPECT().
165+ Search(mock.Anything, mock.Anything).
166+ RunAndReturn(func(context.Context, dto.HistorySearchInput) (*dto.HistorySearchOutput, error) {
195167 searchCalled <- struct{}{}
196168 return &dto.HistorySearchOutput{Matches: []entity.HistoryMatch{
197169 {Entry: &entity.HistoryEntry{ID: 1, URL: "https://result.com", Title: "Result", LastVisited: time.Now()}},
198170 }}, nil
199- },
200- }
171+ }).
172+ Once()
201173
202174 hs := newTestSidebarSearchHarness()
203175 hs.ctx = t.Context()
@@ -238,14 +210,16 @@ func TestApplySearchResults_CurrentGenerationAppliedAfterSearch(t *testing.T) {
238210 searchCalled := make(chan struct{}, 1)
239211 idleCalled := make(chan glib.SourceFunc, 1)
240212
241- history := &fakeHistorySidebarHistory{
242- searchFn: func(context.Context, dto.HistorySearchInput) (*dto.HistorySearchOutput, error) {
213+ history := portmocks.NewMockHistorySidebarHistory(t)
214+ history.EXPECT().
215+ Search(mock.Anything, mock.Anything).
216+ RunAndReturn(func(context.Context, dto.HistorySearchInput) (*dto.HistorySearchOutput, error) {
243217 searchCalled <- struct{}{}
244218 return &dto.HistorySearchOutput{Matches: []entity.HistoryMatch{
245219 {Entry: &entity.HistoryEntry{ID: 1, URL: "https://live.com", Title: "Live", LastVisited: time.Now()}},
246220 }}, nil
247- },
248- }
221+ }).
222+ Once()
249223
250224 hs := newTestSidebarSearchHarness()
251225 hs.ctx = t.Context()
@@ -286,12 +260,14 @@ func TestApplySearchResults_CurrentGenerationAppliedAfterSearch(t *testing.T) {
286260 // active query while resetting browse/search state before the refreshed load.
287261 func TestHistorySidebar_ReloadPreservesQuery(t *testing.T) {
288262 searchCalled := make(chan struct{}, 1)
289- history := &fakeHistorySidebarHistory{
290- searchFn: func(context.Context, dto.HistorySearchInput) (*dto.HistorySearchOutput, error) {
263+ history := portmocks.NewMockHistorySidebarHistory(t)
264+ history.EXPECT().
265+ Search(mock.Anything, mock.Anything).
266+ RunAndReturn(func(context.Context, dto.HistorySearchInput) (*dto.HistorySearchOutput, error) {
291267 searchCalled <- struct{}{}
292268 return &dto.HistorySearchOutput{Matches: []entity.HistoryMatch{}}, nil
293- },
294- }
269+ }).
270+ Once()
295271
296272 hs := newTestSidebarSearchHarness()
297273 hs.currentQuery = "preserved"
@@ -341,13 +317,15 @@ func TestFetchPage_StaleGenerationDoesNotMutateLoadingState(t *testing.T) {
341317 getRecentCalled := make(chan struct{})
342318 proceed := make(chan struct{})
343319
344- history := &fakeHistorySidebarHistory{
345- getRecentFn: func(context.Context, int, int) ([]*entity.HistoryEntry, error) {
320+ history := portmocks.NewMockHistorySidebarHistory(t)
321+ history.EXPECT().
322+ GetRecent(mock.Anything, sidebarPageSize, 0).
323+ RunAndReturn(func(context.Context, int, int) ([]*entity.HistoryEntry, error) {
346324 close(getRecentCalled)
347325 <-proceed
348326 return []*entity.HistoryEntry{}, nil
349- },
350- }
327+ }).
328+ Once()
351329
352330 hs := newTestSidebarSearchHarness()
353331 hs.ctx = t.Context()
@@ -3,16 +3,17 @@ package component
3 import (3 import (
4 "context"4 "context"
5 "errors"5 "errors"
6- "fmt"
7 "sync"6 "sync"
8 "testing"7 "testing"
9 "time"8 "time"
10 9
11 "github.com/bnema/dumber/internal/application/dto"10 "github.com/bnema/dumber/internal/application/dto"
11+ portmocks "github.com/bnema/dumber/internal/application/port/mocks"
12 "github.com/bnema/dumber/internal/domain/entity"12 "github.com/bnema/dumber/internal/domain/entity"
13 "github.com/bnema/puregotk/v4/glib"13 "github.com/bnema/puregotk/v4/glib"
14 "github.com/rs/zerolog"14 "github.com/rs/zerolog"
15 "github.com/stretchr/testify/assert"15 "github.com/stretchr/testify/assert"
16+ "github.com/stretchr/testify/mock"
16 "github.com/stretchr/testify/require"17 "github.com/stretchr/testify/require"
17 )18 )
18 19
@@ -155,49 +156,20 @@ func TestApplyDeletedEntryLocked_RecomputesBrowseState(t *testing.T) {
155 assert.Equal(t, historyDisplayRowEntry, hs.displayRows[2].Kind)156 assert.Equal(t, historyDisplayRowEntry, hs.displayRows[2].Kind)
156 }157 }
157 158
158-// =============================================================================
159-// Async search seam: controllable history port fake
160-// =============================================================================
161-
162-type fakeHistorySidebarHistory struct {
163- getRecentFn func(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error)
164- searchFn func(ctx context.Context, input dto.HistorySearchInput) (*dto.HistorySearchOutput, error)
165- deleteFn func(ctx context.Context, id int64) error
166-}
167-
168-func (f *fakeHistorySidebarHistory) GetRecent(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error) {
169- if f.getRecentFn != nil {
170- return f.getRecentFn(ctx, limit, offset)
171- }
172- return nil, fmt.Errorf("unexpected GetRecent call in fakeHistorySidebarHistory")
173-}
174-
175-func (f *fakeHistorySidebarHistory) Search(ctx context.Context, input dto.HistorySearchInput) (*dto.HistorySearchOutput, error) {
176- if f.searchFn != nil {
177- return f.searchFn(ctx, input)
178- }
179- return &dto.HistorySearchOutput{Matches: []entity.HistoryMatch{}}, nil
180-}
181-
182-func (f *fakeHistorySidebarHistory) Delete(ctx context.Context, id int64) error {
183- if f.deleteFn != nil {
184- return f.deleteFn(ctx, id)
185- }
186- return nil
187-}
188-
189 func TestApplySearchResults_StaleGenerationDropsResultsAfterSearch(t *testing.T) {159 func TestApplySearchResults_StaleGenerationDropsResultsAfterSearch(t *testing.T) {
190 searchCalled := make(chan struct{}, 1)160 searchCalled := make(chan struct{}, 1)
191 idleCalled := make(chan glib.SourceFunc, 1)161 idleCalled := make(chan glib.SourceFunc, 1)
192 162
193- history := &fakeHistorySidebarHistory{163+ history := portmocks.NewMockHistorySidebarHistory(t)
194- searchFn: func(context.Context, dto.HistorySearchInput) (*dto.HistorySearchOutput, error) {164+ history.EXPECT().
165+ Search(mock.Anything, mock.Anything).
166+ RunAndReturn(func(context.Context, dto.HistorySearchInput) (*dto.HistorySearchOutput, error) {
195 searchCalled <- struct{}{}167 searchCalled <- struct{}{}
196 return &dto.HistorySearchOutput{Matches: []entity.HistoryMatch{168 return &dto.HistorySearchOutput{Matches: []entity.HistoryMatch{
197 {Entry: &entity.HistoryEntry{ID: 1, URL: "https://result.com", Title: "Result", LastVisited: time.Now()}},169 {Entry: &entity.HistoryEntry{ID: 1, URL: "https://result.com", Title: "Result", LastVisited: time.Now()}},
198 }}, nil170 }}, nil
199- },171+ }).
200- }172+ Once()
201 173
202 hs := newTestSidebarSearchHarness()174 hs := newTestSidebarSearchHarness()
203 hs.ctx = t.Context()175 hs.ctx = t.Context()
@@ -238,14 +210,16 @@ func TestApplySearchResults_CurrentGenerationAppliedAfterSearch(t *testing.T) {
238 searchCalled := make(chan struct{}, 1)210 searchCalled := make(chan struct{}, 1)
239 idleCalled := make(chan glib.SourceFunc, 1)211 idleCalled := make(chan glib.SourceFunc, 1)
240 212
241- history := &fakeHistorySidebarHistory{213+ history := portmocks.NewMockHistorySidebarHistory(t)
242- searchFn: func(context.Context, dto.HistorySearchInput) (*dto.HistorySearchOutput, error) {214+ history.EXPECT().
215+ Search(mock.Anything, mock.Anything).
216+ RunAndReturn(func(context.Context, dto.HistorySearchInput) (*dto.HistorySearchOutput, error) {
243 searchCalled <- struct{}{}217 searchCalled <- struct{}{}
244 return &dto.HistorySearchOutput{Matches: []entity.HistoryMatch{218 return &dto.HistorySearchOutput{Matches: []entity.HistoryMatch{
245 {Entry: &entity.HistoryEntry{ID: 1, URL: "https://live.com", Title: "Live", LastVisited: time.Now()}},219 {Entry: &entity.HistoryEntry{ID: 1, URL: "https://live.com", Title: "Live", LastVisited: time.Now()}},
246 }}, nil220 }}, nil
247- },221+ }).
248- }222+ Once()
249 223
250 hs := newTestSidebarSearchHarness()224 hs := newTestSidebarSearchHarness()
251 hs.ctx = t.Context()225 hs.ctx = t.Context()
@@ -286,12 +260,14 @@ func TestApplySearchResults_CurrentGenerationAppliedAfterSearch(t *testing.T) {
286 // active query while resetting browse/search state before the refreshed load.260 // active query while resetting browse/search state before the refreshed load.
287 func TestHistorySidebar_ReloadPreservesQuery(t *testing.T) {261 func TestHistorySidebar_ReloadPreservesQuery(t *testing.T) {
288 searchCalled := make(chan struct{}, 1)262 searchCalled := make(chan struct{}, 1)
289- history := &fakeHistorySidebarHistory{263+ history := portmocks.NewMockHistorySidebarHistory(t)
290- searchFn: func(context.Context, dto.HistorySearchInput) (*dto.HistorySearchOutput, error) {264+ history.EXPECT().
265+ Search(mock.Anything, mock.Anything).
266+ RunAndReturn(func(context.Context, dto.HistorySearchInput) (*dto.HistorySearchOutput, error) {
291 searchCalled <- struct{}{}267 searchCalled <- struct{}{}
292 return &dto.HistorySearchOutput{Matches: []entity.HistoryMatch{}}, nil268 return &dto.HistorySearchOutput{Matches: []entity.HistoryMatch{}}, nil
293- },269+ }).
294- }270+ Once()
295 271
296 hs := newTestSidebarSearchHarness()272 hs := newTestSidebarSearchHarness()
297 hs.currentQuery = "preserved"273 hs.currentQuery = "preserved"
@@ -341,13 +317,15 @@ func TestFetchPage_StaleGenerationDoesNotMutateLoadingState(t *testing.T) {
341 getRecentCalled := make(chan struct{})317 getRecentCalled := make(chan struct{})
342 proceed := make(chan struct{})318 proceed := make(chan struct{})
343 319
344- history := &fakeHistorySidebarHistory{320+ history := portmocks.NewMockHistorySidebarHistory(t)
345- getRecentFn: func(context.Context, int, int) ([]*entity.HistoryEntry, error) {321+ history.EXPECT().
322+ GetRecent(mock.Anything, sidebarPageSize, 0).
323+ RunAndReturn(func(context.Context, int, int) ([]*entity.HistoryEntry, error) {
346 close(getRecentCalled)324 close(getRecentCalled)
347 <-proceed325 <-proceed
348 return []*entity.HistoryEntry{}, nil326 return []*entity.HistoryEntry{}, nil
349- },327+ }).
350- }328+ Once()
351 329
352 hs := newTestSidebarSearchHarness()330 hs := newTestSidebarSearchHarness()
353 hs.ctx = t.Context()331 hs.ctx = t.Context()

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