1package contract
2
3import (
4 "context"
5 "database/sql"
6 "fmt"
7 "math"
8 "testing"
9
10 "github.com/bnema/dumber/services"
11 "github.com/bnema/dumber/internal/config"
12 mock_db "github.com/bnema/dumber/internal/db/mocks"
13 "github.com/golang/mock/gomock"
14)
15
16// floatMatcher implements gomock.Matcher for floating point comparison with tolerance
17type floatMatcher struct {
18 expected float64
19 tolerance float64
20}
21
22func (f floatMatcher) Matches(x interface{}) bool {
23 if actual, ok := x.(float64); ok {
24 return math.Abs(actual - f.expected) < f.tolerance
25 }
26 return false
27}
28
29func (f floatMatcher) String() string {
30 return fmt.Sprintf("is within %f of %f", f.tolerance, f.expected)
31}
32
33// floatEq creates a matcher for floating point values with tolerance
34func floatEq(expected float64) gomock.Matcher {
35 return floatMatcher{expected: expected, tolerance: 1e-10}
36}
37
38// MockWindowTitleUpdater implements the WindowTitleUpdater interface for testing
39type MockWindowTitleUpdater struct {
40 lastTitle string
41 callCount int
42}
43
44func (m *MockWindowTitleUpdater) SetTitle(title string) {
45 m.lastTitle = title
46 m.callCount++
47}
48
49// Contract Test: BrowserService Zoom Controls
50// Tests the zoom functionality contracts as specified in 002-browser-controls-ui
51func TestBrowserService_ZoomControls_Contract(t *testing.T) {
52 ctrl := gomock.NewController(t)
53 defer ctrl.Finish()
54
55 mockDB := mock_db.NewMockDatabaseQuerier(ctrl)
56 cfg := &config.Config{}
57 service := services.NewBrowserService(cfg, mockDB)
58
59 ctx := context.Background()
60 testURL := "https://example.com"
61
62 t.Run("ZoomIn increases zoom level by 0.1", func(t *testing.T) {
63 // Setup mock expectations
64 mockDB.EXPECT().
65 GetZoomLevel(ctx, testURL).
66 Return(1.0, nil)
67
68 mockDB.EXPECT().
69 SetZoomLevel(ctx, testURL, floatEq(1.1)).
70 Return(nil)
71
72 // Contract: ZoomIn should increase zoom by 0.1 and return new level
73 newZoom, err := service.ZoomIn(ctx, testURL)
74
75 // Contract assertions
76 if err != nil {
77 t.Fatalf("ZoomIn should not return error: %v", err)
78 }
79
80 expectedZoom := 1.1
81 if newZoom != expectedZoom {
82 t.Errorf("Expected zoom level %f, got %f", expectedZoom, newZoom)
83 }
84 })
85
86 t.Run("ZoomOut decreases zoom level by 0.1", func(t *testing.T) {
87 // Setup mock expectations for current zoom level
88 mockDB.EXPECT().
89 GetZoomLevel(ctx, testURL).
90 Return(1.2, nil)
91
92 mockDB.EXPECT().
93 SetZoomLevel(ctx, testURL, floatEq(1.1)).
94 Return(nil)
95
96 // Contract: ZoomOut should decrease zoom by 0.1 and return new level
97 newZoom, err := service.ZoomOut(ctx, testURL)
98
99 // Contract assertions
100 if err != nil {
101 t.Fatalf("ZoomOut should not return error: %v", err)
102 }
103
104 expectedZoom := 1.1
105 if math.Abs(newZoom - expectedZoom) > 1e-10 {
106 t.Errorf("Expected zoom level %f, got %f", expectedZoom, newZoom)
107 }
108 })
109
110 t.Run("ZoomReset sets zoom to 1.0", func(t *testing.T) {
111 // Setup mock expectations
112 mockDB.EXPECT().
113 SetZoomLevel(ctx, testURL, floatEq(1.0)).
114 Return(nil)
115
116 // Contract: ZoomReset should always return zoom to default (1.0)
117 newZoom, err := service.ResetZoom(ctx, testURL)
118
119 // Contract assertions
120 if err != nil {
121 t.Fatalf("ResetZoom should not return error: %v", err)
122 }
123
124 expectedZoom := 1.0
125 if newZoom != expectedZoom {
126 t.Errorf("ResetZoom should return 1.0, got %f", expectedZoom)
127 }
128 })
129
130 t.Run("GetZoomLevel returns current zoom level", func(t *testing.T) {
131 // Setup mock expectations
132 expectedZoom := 1.5
133 mockDB.EXPECT().
134 GetZoomLevel(ctx, testURL).
135 Return(expectedZoom, nil)
136
137 // Contract: GetZoomLevel should return current zoom level
138 currentZoom, err := service.GetZoomLevel(ctx, testURL)
139
140 // Contract assertions
141 if err != nil {
142 t.Fatalf("GetZoomLevel should not return error: %v", err)
143 }
144
145 if currentZoom != expectedZoom {
146 t.Errorf("Expected zoom level %f, got %f", expectedZoom, currentZoom)
147 }
148 })
149
150 t.Run("GetZoomLevel returns default 1.0 for new URLs", func(t *testing.T) {
151 // Setup mock expectations for non-existent URL
152 mockDB.EXPECT().
153 GetZoomLevel(ctx, testURL).
154 Return(0.0, sql.ErrNoRows)
155
156 // Contract: GetZoomLevel should return 1.0 for URLs without zoom settings
157 currentZoom, err := service.GetZoomLevel(ctx, testURL)
158
159 // Contract assertions
160 if err != nil {
161 t.Fatalf("GetZoomLevel should not return error for new URLs: %v", err)
162 }
163
164 expectedZoom := 1.0
165 if currentZoom != expectedZoom {
166 t.Errorf("Expected default zoom level %f for new URL, got %f", expectedZoom, currentZoom)
167 }
168 })
169}
170
171// Contract Test: BrowserService Navigation Controls
172func TestBrowserService_NavigationControls_Contract(t *testing.T) {
173 ctrl := gomock.NewController(t)
174 defer ctrl.Finish()
175
176 mockDB := mock_db.NewMockDatabaseQuerier(ctrl)
177 cfg := &config.Config{}
178 service := services.NewBrowserService(cfg, mockDB)
179
180 ctx := context.Background()
181
182 t.Run("GoBack handles gracefully when no history", func(t *testing.T) {
183 // Contract: GoBack should handle empty history gracefully
184 err := service.GoBack(ctx)
185
186 // May return error for empty history, which is acceptable
187 if err != nil {
188 t.Logf("GoBack returned error (acceptable for empty history): %v", err)
189 }
190 })
191
192 t.Run("GoForward handles gracefully when no forward history", func(t *testing.T) {
193 // Contract: GoForward should handle empty forward history gracefully
194 err := service.GoForward(ctx)
195
196 // May return error for empty forward history, which is acceptable
197 if err != nil {
198 t.Logf("GoForward returned error (acceptable for empty forward history): %v", err)
199 }
200 })
201}
202
203// Contract Test: WindowTitleUpdater Integration
204func TestBrowserService_WindowTitleUpdate_Contract(t *testing.T) {
205 ctrl := gomock.NewController(t)
206 defer ctrl.Finish()
207
208 mockDB := mock_db.NewMockDatabaseQuerier(ctrl)
209 cfg := &config.Config{}
210 service := services.NewBrowserService(cfg, mockDB)
211
212 // Setup mock window title updater
213 mockUpdater := &MockWindowTitleUpdater{}
214 service.SetWindowTitleUpdater(mockUpdater)
215
216 t.Run("SetWindowTitleUpdater accepts WindowTitleUpdater interface", func(t *testing.T) {
217 // Contract: Service should accept any WindowTitleUpdater implementation
218 newMockUpdater := &MockWindowTitleUpdater{}
219
220 // This should not panic or error
221 service.SetWindowTitleUpdater(newMockUpdater)
222
223 // Verify the updater was set properly
224 if newMockUpdater.callCount < 0 {
225 t.Errorf("WindowTitleUpdater should be properly initialized")
226 }
227 })
228}
229
230// Contract Test: URL Operations
231func TestBrowserService_URLOperations_Contract(t *testing.T) {
232 ctrl := gomock.NewController(t)
233 defer ctrl.Finish()
234
235 mockDB := mock_db.NewMockDatabaseQuerier(ctrl)
236 cfg := &config.Config{}
237 service := services.NewBrowserService(cfg, mockDB)
238
239 t.Run("GetCurrentURL returns current URL", func(t *testing.T) {
240 // Contract: GetCurrentURL should return the current URL string
241 ctx := context.Background()
242
243 url, err := service.GetCurrentURL(ctx)
244
245 // Contract assertions
246 if err != nil {
247 t.Fatalf("GetCurrentURL should not return error: %v", err)
248 }
249
250 // URL should be a valid string (can be empty for new browser)
251 if url != "" && len(url) < 4 { // Minimum valid URL length
252 t.Errorf("Invalid URL returned: %s", url)
253 }
254 })
255
256 t.Run("CopyCurrentURL accepts URL parameter", func(t *testing.T) {
257 // Contract: CopyCurrentURL should accept URL and handle clipboard
258 ctx := context.Background()
259 testURL := "https://example.com"
260
261 err := service.CopyCurrentURL(ctx, testURL)
262
263 // Contract assertions - should not return error for valid URL
264 if err != nil {
265 t.Fatalf("CopyCurrentURL should not return error for valid URL: %v", err)
266 }
267 })
268}
269
270// Contract Test: Error Handling
271func TestBrowserService_ErrorHandling_Contract(t *testing.T) {
272 // Test with nil config but valid mock database
273 ctrl := gomock.NewController(t)
274 defer ctrl.Finish()
275
276 mockDB := mock_db.NewMockDatabaseQuerier(ctrl)
277 service := services.NewBrowserService(nil, mockDB)
278 ctx := context.Background()
279
280 t.Run("Service handles nil config gracefully", func(t *testing.T) {
281 // Contract: Service methods should handle nil config gracefully
282
283 // Setup mock expectations for operations that might still work
284 mockDB.EXPECT().
285 GetZoomLevel(ctx, "test").
286 Return(1.0, nil).
287 AnyTimes()
288
289 mockDB.EXPECT().
290 SetZoomLevel(ctx, gomock.Any(), gomock.Any()).
291 Return(nil).
292 AnyTimes()
293
294 // These should work with valid database but nil config
295 _, err := service.ZoomIn(ctx, "test")
296 if err != nil {
297 t.Logf("ZoomIn returned error with nil config (may be expected): %v", err)
298 }
299
300 err = service.GoBack(ctx)
301 if err != nil {
302 t.Logf("GoBack returned error with nil config (may be expected): %v", err)
303 }
304 })
305}