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