browser_controls_integration_test.go RawHistoryFolder
1package integration
2
3import (
4 "context"
5 "database/sql"
6 "os"
7 "path/filepath"
8 "sync"
9 "testing"
10 "time"
11
12 "github.com/bnema/dumber/internal/config"
13 "github.com/bnema/dumber/internal/db"
14 "github.com/bnema/dumber/services"
15 _ "github.com/ncruces/go-sqlite3/driver" // SQLite driver
16 _ "github.com/ncruces/go-sqlite3/embed" // Embed SQLite
17)
18
19// MockWindowUpdater for integration testing
20type MockWindowUpdater struct {
21 titles []string
22 mutex sync.Mutex
23}
24
25// Implement the WindowTitleUpdater interface method
26func (m *MockWindowUpdater) SetTitle(title string) {
27 m.mutex.Lock()
28 defer m.mutex.Unlock()
29 m.titles = append(m.titles, title)
30}
31
32// setupTestDB creates a temporary SQLite database for integration testing
33func setupTestDB(t *testing.T) (*sql.DB, *db.Queries, func()) {
34 // Create temporary database file
35 tmpDir := t.TempDir()
36 dbPath := filepath.Join(tmpDir, "test_browser.db")
37
38 database, err := sql.Open("sqlite3", dbPath)
39 if err != nil {
40 t.Fatalf("Failed to open test database: %v", err)
41 }
42
43 // Create tables using the migration schema
44 createTablesSQL := `
45 CREATE TABLE IF NOT EXISTS history (
46 id INTEGER PRIMARY KEY AUTOINCREMENT,
47 url TEXT NOT NULL,
48 title TEXT,
49 visit_count INTEGER DEFAULT 1,
50 last_visited DATETIME DEFAULT CURRENT_TIMESTAMP,
51 created_at DATETIME DEFAULT CURRENT_TIMESTAMP
52 );
53
54 CREATE TABLE IF NOT EXISTS shortcuts (
55 id INTEGER PRIMARY KEY AUTOINCREMENT,
56 shortcut TEXT NOT NULL UNIQUE,
57 url_template TEXT NOT NULL,
58 description TEXT,
59 created_at DATETIME DEFAULT CURRENT_TIMESTAMP
60 );
61
62 CREATE TABLE IF NOT EXISTS zoom_levels (
63 domain TEXT NOT NULL UNIQUE,
64 zoom_factor REAL NOT NULL DEFAULT 1.0,
65 updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
66 );
67 `
68
69 if _, err := database.Exec(createTablesSQL); err != nil {
70 t.Fatalf("Failed to create test tables: %v", err)
71 }
72
73 queries := db.New(database)
74
75 cleanup := func() {
76 database.Close()
77 os.Remove(dbPath)
78 }
79
80 return database, queries, cleanup
81}
82
83// setupTestBrowserService creates a BrowserService with test database
84func setupTestBrowserService(t *testing.T) (*services.BrowserService, func()) {
85 _, queries, cleanup := setupTestDB(t)
86
87 cfg := &config.Config{}
88
89 service := services.NewBrowserService(cfg, queries)
90
91 return service, cleanup
92}
93
94// Integration Test: Zoom Persistence
95func TestBrowserControls_ZoomPersistence_Integration(t *testing.T) {
96 service, cleanup := setupTestBrowserService(t)
97 defer cleanup()
98
99 ctx := context.Background()
100 testURL := "https://example.com"
101
102 t.Run("Zoom level persists across service operations", func(t *testing.T) {
103 // Set initial zoom level
104 zoomLevel, err := service.ZoomIn(ctx, testURL)
105 if err != nil {
106 t.Fatalf("Failed to zoom in: %v", err)
107 }
108
109 expectedZoom := 1.1
110 if zoomLevel != expectedZoom {
111 t.Errorf("Expected zoom %f, got %f", expectedZoom, zoomLevel)
112 }
113
114 // Zoom in again
115 zoomLevel, err = service.ZoomIn(ctx, testURL)
116 if err != nil {
117 t.Fatalf("Failed to zoom in again: %v", err)
118 }
119
120 expectedZoom = 1.2
121 if zoomLevel != expectedZoom {
122 t.Errorf("Expected zoom %f, got %f", expectedZoom, zoomLevel)
123 }
124
125 // Test that zoom persists for the same URL
126 // Simulate getting zoom level (this would typically happen on page load)
127 currentZoom, err := service.GetZoomLevel(ctx, testURL)
128 if err != nil {
129 t.Fatalf("Failed to get zoom level: %v", err)
130 }
131
132 if currentZoom != expectedZoom {
133 t.Errorf("Zoom level not persisted. Expected %f, got %f", expectedZoom, currentZoom)
134 }
135 })
136
137 t.Run("Different URLs have independent zoom levels", func(t *testing.T) {
138 url1 := "https://example.com"
139 url2 := "https://google.com"
140
141 // Set different zoom levels for different URLs
142 zoom1, err := service.ZoomIn(ctx, url1)
143 if err != nil {
144 t.Fatalf("Failed to zoom in for url1: %v", err)
145 }
146
147 zoom2, err := service.ZoomOut(ctx, url2)
148 if err != nil {
149 t.Fatalf("Failed to zoom out for url2: %v", err)
150 }
151
152 // Verify they are different
153 if zoom1 == zoom2 {
154 t.Errorf("Expected different zoom levels for different URLs, both got %f", zoom1)
155 }
156
157 // Verify persistence for each URL
158 persistedZoom1, err := service.GetZoomLevel(ctx, url1)
159 if err != nil {
160 t.Fatalf("Failed to get zoom level for url1: %v", err)
161 }
162
163 persistedZoom2, err := service.GetZoomLevel(ctx, url2)
164 if err != nil {
165 t.Fatalf("Failed to get zoom level for url2: %v", err)
166 }
167
168 if persistedZoom1 != zoom1 {
169 t.Errorf("URL1 zoom not persisted. Expected %f, got %f", zoom1, persistedZoom1)
170 }
171
172 if persistedZoom2 != zoom2 {
173 t.Errorf("URL2 zoom not persisted. Expected %f, got %f", zoom2, persistedZoom2)
174 }
175 })
176
177 t.Run("Zoom reset works correctly", func(t *testing.T) {
178 testURL := "https://test.com"
179
180 // Change zoom level
181 service.ZoomIn(ctx, testURL)
182 service.ZoomIn(ctx, testURL)
183
184 // Reset zoom
185 resetZoom, err := service.ResetZoom(ctx, testURL)
186 if err != nil {
187 t.Fatalf("Failed to reset zoom: %v", err)
188 }
189
190 expectedZoom := 1.0
191 if resetZoom != expectedZoom {
192 t.Errorf("Expected reset zoom %f, got %f", expectedZoom, resetZoom)
193 }
194
195 // Verify persistence of reset
196 persistedZoom, err := service.GetZoomLevel(ctx, testURL)
197 if err != nil {
198 t.Fatalf("Failed to get zoom level after reset: %v", err)
199 }
200
201 if persistedZoom != expectedZoom {
202 t.Errorf("Reset zoom not persisted. Expected %f, got %f", expectedZoom, persistedZoom)
203 }
204 })
205}
206
207// Integration Test: Navigation with History
208func TestBrowserControls_NavigationHistory_Integration(t *testing.T) {
209 _, cleanup := setupTestBrowserService(t)
210 defer cleanup()
211
212 ctx := context.Background()
213
214 t.Run("Navigation builds history correctly", func(t *testing.T) {
215 // This test assumes the service has methods to add history entries
216 // and that navigation uses real history data
217
218 // Add some test history entries directly to database for testing
219 database, queries, dbCleanup := setupTestDB(t)
220 defer dbCleanup()
221
222 // Insert test history entries
223 testURLs := []string{
224 "https://example.com",
225 "https://google.com",
226 "https://github.com",
227 }
228
229 for _, url := range testURLs {
230 _, err := database.Exec(
231 `INSERT INTO history (url, title, visit_count) VALUES (?, ?, ?)`,
232 url, "Test Title", 1,
233 )
234 if err != nil {
235 t.Fatalf("Failed to insert test history: %v", err)
236 }
237 }
238
239 // Create service with populated database
240 serviceWithHistory := services.NewBrowserService(&config.Config{}, queries)
241
242 // Test navigation operations
243 err := serviceWithHistory.GoBack(ctx)
244 if err != nil {
245 t.Logf("GoBack returned error (acceptable for test): %v", err)
246 }
247
248 err = serviceWithHistory.GoForward(ctx)
249 if err != nil {
250 t.Logf("GoForward returned error (acceptable for test): %v", err)
251 }
252 })
253}
254
255// Integration Test: WindowTitleUpdater Integration
256func TestBrowserControls_WindowTitleUpdater_Integration(t *testing.T) {
257 service, cleanup := setupTestBrowserService(t)
258 defer cleanup()
259
260 // Create mock window title updater
261 mockUpdater := &MockWindowUpdater{
262 titles: make([]string, 0),
263 }
264
265 // This test would need the actual WindowTitleUpdater interface to be implemented
266 // For now, we test that the service accepts the updater
267 t.Run("Service integrates with WindowTitleUpdater", func(t *testing.T) {
268 // This is a placeholder test - would need actual implementation
269 // to verify title updates during navigation operations
270
271 service.SetWindowTitleUpdater(mockUpdater)
272
273 // Simulate navigation that should trigger title update
274 ctx := context.Background()
275 service.GoBack(ctx)
276
277 // In a full integration test, we would verify that:
278 // 1. Title updates were called
279 // 2. Correct titles were set
280 // 3. Updates happened at appropriate times
281
282 // For now, just verify no panic occurred
283 if len(mockUpdater.titles) < 0 {
284 t.Errorf("WindowTitleUpdater integration failed")
285 }
286 })
287}
288
289// Integration Test: URL Copying with System Integration
290func TestBrowserControls_URLCopying_Integration(t *testing.T) {
291 service, cleanup := setupTestBrowserService(t)
292 defer cleanup()
293
294 t.Run("URL copying returns current browser URL", func(t *testing.T) {
295 ctx := context.Background()
296
297 // Test URL copying functionality
298 testURL := "https://example.com"
299 err := service.CopyCurrentURL(ctx, testURL)
300 if err != nil {
301 t.Fatalf("CopyCurrentURL failed: %v", err)
302 }
303
304 // In a full integration test, we would also verify:
305 // 1. System clipboard is updated
306 // 2. Correct URL format
307 // 3. Special handling for local/file URLs
308 })
309}
310
311// Integration Test: Performance Requirements
312func TestBrowserControls_Performance_Integration(t *testing.T) {
313 service, cleanup := setupTestBrowserService(t)
314 defer cleanup()
315
316 ctx := context.Background()
317 testURL := "https://example.com"
318
319 t.Run("Zoom operations complete within performance requirements", func(t *testing.T) {
320 // Spec requires <50ms for history search, similar expectation for zoom
321 start := time.Now()
322
323 _, err := service.ZoomIn(ctx, testURL)
324 if err != nil {
325 t.Fatalf("ZoomIn failed: %v", err)
326 }
327
328 duration := time.Since(start)
329
330 // Zoom operations should be fast (<10ms typically)
331 maxDuration := 50 * time.Millisecond
332 if duration > maxDuration {
333 t.Errorf("Zoom operation too slow: %v (max: %v)", duration, maxDuration)
334 }
335 })
336
337 t.Run("Navigation operations complete within reasonable time", func(t *testing.T) {
338 start := time.Now()
339
340 err := service.GoBack(ctx)
341 if err != nil {
342 t.Fatalf("GoBack failed: %v", err)
343 }
344
345 duration := time.Since(start)
346
347 // Navigation should be reasonably fast
348 maxDuration := 100 * time.Millisecond
349 if duration > maxDuration {
350 t.Errorf("Navigation operation too slow: %v (max: %v)", duration, maxDuration)
351 }
352 })
353}
354
355// Helper function to verify database state
356func verifyZoomInDatabase(t *testing.T, queries *db.Queries, url string, expectedZoom float64) {
357 _ = context.Background() // Test placeholder
358
359 // This would use the actual SQLC generated methods
360 // For now, it's a placeholder for the verification logic
361
362 // zoom, err := queries.GetZoomLevel(ctx, url)
363 // if err != nil {
364 // t.Fatalf("Failed to get zoom from database: %v", err)
365 // }
366 //
367 // if zoom != expectedZoom {
368 // t.Errorf("Database zoom mismatch. Expected %f, got %f", expectedZoom, zoom)
369 // }
370}