v0.1 bf7c821

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}

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