v0.14.1 0da30ba

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		DefaultZoom: 1.0, // Set default zoom level
98	}
99
100	service := services.NewBrowserService(cfg, queries)
101
102	// Load zoom cache (simulates startup behavior)
103	if err := service.LoadZoomCacheFromDB(context.Background()); err != nil {
104		t.Fatalf("Failed to load zoom cache: %v", err)
105	}
106
107	return service, cleanup
108}
109
110// Integration Test: Zoom Persistence
111func TestBrowserControls_ZoomPersistence_Integration(t *testing.T) {
112	service, cleanup := setupTestBrowserService(t)
113	defer cleanup()
114
115	ctx := context.Background()
116	testURL := testExampleURL
117
118	t.Run("Zoom level persists across service operations", func(t *testing.T) {
119		// Set initial zoom level
120		zoomLevel, err := service.ZoomIn(ctx, testURL)
121		if err != nil {
122			t.Fatalf("Failed to zoom in: %v", err)
123		}
124
125		expectedZoom := 1.1
126		if zoomLevel != expectedZoom {
127			t.Errorf("Expected zoom %f, got %f", expectedZoom, zoomLevel)
128		}
129
130		// Zoom in again
131		zoomLevel, err = service.ZoomIn(ctx, testURL)
132		if err != nil {
133			t.Fatalf("Failed to zoom in again: %v", err)
134		}
135
136		expectedZoom = 1.2
137		if zoomLevel != expectedZoom {
138			t.Errorf("Expected zoom %f, got %f", expectedZoom, zoomLevel)
139		}
140
141		// Test that zoom persists for the same URL
142		// Simulate getting zoom level (this would typically happen on page load)
143		currentZoom, err := service.GetZoomLevel(ctx, testURL)
144		if err != nil {
145			t.Fatalf("Failed to get zoom level: %v", err)
146		}
147
148		if currentZoom != expectedZoom {
149			t.Errorf("Zoom level not persisted. Expected %f, got %f", expectedZoom, currentZoom)
150		}
151	})
152
153	t.Run("Different URLs have independent zoom levels", func(t *testing.T) {
154		url1 := testExampleURL
155		url2 := "https://google.com"
156
157		// Set different zoom levels for different URLs
158		zoom1, err := service.ZoomIn(ctx, url1)
159		if err != nil {
160			t.Fatalf("Failed to zoom in for url1: %v", err)
161		}
162
163		zoom2, err := service.ZoomOut(ctx, url2)
164		if err != nil {
165			t.Fatalf("Failed to zoom out for url2: %v", err)
166		}
167
168		// Verify they are different
169		if zoom1 == zoom2 {
170			t.Errorf("Expected different zoom levels for different URLs, both got %f", zoom1)
171		}
172
173		// Verify persistence for each URL
174		persistedZoom1, err := service.GetZoomLevel(ctx, url1)
175		if err != nil {
176			t.Fatalf("Failed to get zoom level for url1: %v", err)
177		}
178
179		persistedZoom2, err := service.GetZoomLevel(ctx, url2)
180		if err != nil {
181			t.Fatalf("Failed to get zoom level for url2: %v", err)
182		}
183
184		if persistedZoom1 != zoom1 {
185			t.Errorf("URL1 zoom not persisted. Expected %f, got %f", zoom1, persistedZoom1)
186		}
187
188		if persistedZoom2 != zoom2 {
189			t.Errorf("URL2 zoom not persisted. Expected %f, got %f", zoom2, persistedZoom2)
190		}
191	})
192
193	t.Run("Zoom reset works correctly", func(t *testing.T) {
194		testURL := "https://test.com"
195
196		// Change zoom level
197		if _, err := service.ZoomIn(ctx, testURL); err != nil {
198			t.Logf("Warning: ZoomIn failed: %v", err)
199		}
200		if _, err := service.ZoomIn(ctx, testURL); err != nil {
201			t.Logf("Warning: ZoomIn failed: %v", err)
202		}
203
204		// Reset zoom
205		resetZoom, err := service.ResetZoom(ctx, testURL)
206		if err != nil {
207			t.Fatalf("Failed to reset zoom: %v", err)
208		}
209
210		expectedZoom := 1.0
211		if resetZoom != expectedZoom {
212			t.Errorf("Expected reset zoom %f, got %f", expectedZoom, resetZoom)
213		}
214
215		// Verify persistence of reset
216		persistedZoom, err := service.GetZoomLevel(ctx, testURL)
217		if err != nil {
218			t.Fatalf("Failed to get zoom level after reset: %v", err)
219		}
220
221		if persistedZoom != expectedZoom {
222			t.Errorf("Reset zoom not persisted. Expected %f, got %f", expectedZoom, persistedZoom)
223		}
224	})
225}
226
227// Integration Test: Navigation with History
228func TestBrowserControls_NavigationHistory_Integration(t *testing.T) {
229	_, cleanup := setupTestBrowserService(t)
230	defer cleanup()
231
232	ctx := context.Background()
233
234	t.Run("Navigation builds history correctly", func(t *testing.T) {
235		// This test assumes the service has methods to add history entries
236		// and that navigation uses real history data
237
238		// Add some test history entries directly to database for testing
239		database, queries, dbCleanup := setupTestDB(t)
240		defer dbCleanup()
241
242		// Insert test history entries
243		testURLs := []string{
244			testExampleURL,
245			"https://google.com",
246			"https://github.com",
247		}
248
249		for _, url := range testURLs {
250			_, err := database.Exec(
251				`INSERT INTO history (url, title, visit_count) VALUES (?, ?, ?)`,
252				url, "Test Title", 1,
253			)
254			if err != nil {
255				t.Fatalf("Failed to insert test history: %v", err)
256			}
257		}
258
259		// Create service with populated database
260		serviceWithHistory := services.NewBrowserService(&config.Config{}, queries)
261
262		// Test navigation operations
263		err := serviceWithHistory.GoBack(ctx)
264		if err != nil {
265			t.Logf("GoBack returned error (acceptable for test): %v", err)
266		}
267
268		err = serviceWithHistory.GoForward(ctx)
269		if err != nil {
270			t.Logf("GoForward returned error (acceptable for test): %v", err)
271		}
272	})
273}
274
275// Integration Test: WindowTitleUpdater Integration
276func TestBrowserControls_WindowTitleUpdater_Integration(t *testing.T) {
277	service, cleanup := setupTestBrowserService(t)
278	defer cleanup()
279
280	// Create mock window title updater
281	mockUpdater := &MockWindowUpdater{
282		titles: make([]string, 0),
283	}
284
285	// This test would need the actual WindowTitleUpdater interface to be implemented
286	// For now, we test that the service accepts the updater
287	t.Run("Service integrates with WindowTitleUpdater", func(t *testing.T) {
288		// This is a placeholder test - would need actual implementation
289		// to verify title updates during navigation operations
290
291		service.SetWindowTitleUpdater(mockUpdater)
292
293		// Simulate navigation that should trigger title update
294		ctx := context.Background()
295		if err := service.GoBack(ctx); err != nil {
296			t.Logf("Warning: GoBack failed: %v", err)
297		}
298
299		// In a full integration test, we would verify that:
300		// 1. Title updates were called
301		// 2. Correct titles were set
302		// 3. Updates happened at appropriate times
303
304		// For now, just verify no panic occurred and titles were set
305		if mockUpdater == nil {
306			t.Errorf("WindowTitleUpdater integration failed: mockUpdater is nil")
307		}
308	})
309}
310
311// Integration Test: URL Copying with System Integration
312func TestBrowserControls_URLCopying_Integration(t *testing.T) {
313	service, cleanup := setupTestBrowserService(t)
314	defer cleanup()
315
316	t.Run("URL copying returns current browser URL", func(t *testing.T) {
317		ctx := context.Background()
318
319		// Test URL copying functionality
320		testURL := testExampleURL
321		err := service.CopyCurrentURL(ctx, testURL)
322		if err != nil {
323			t.Fatalf("CopyCurrentURL failed: %v", err)
324		}
325
326		// In a full integration test, we would also verify:
327		// 1. System clipboard is updated
328		// 2. Correct URL format
329		// 3. Special handling for local/file URLs
330	})
331}
332
333// Integration Test: Performance Requirements
334func TestBrowserControls_Performance_Integration(t *testing.T) {
335	service, cleanup := setupTestBrowserService(t)
336	defer cleanup()
337
338	ctx := context.Background()
339	testURL := testExampleURL
340
341	t.Run("Zoom operations complete within performance requirements", func(t *testing.T) {
342		// Spec requires <50ms for history search, similar expectation for zoom
343		start := time.Now()
344
345		_, err := service.ZoomIn(ctx, testURL)
346		if err != nil {
347			t.Fatalf("ZoomIn failed: %v", err)
348		}
349
350		duration := time.Since(start)
351
352		// Zoom operations should be fast (<10ms typically)
353		maxDuration := 50 * time.Millisecond
354		if duration > maxDuration {
355			t.Errorf("Zoom operation too slow: %v (max: %v)", duration, maxDuration)
356		}
357	})
358
359	t.Run("Navigation operations complete within reasonable time", func(t *testing.T) {
360		start := time.Now()
361
362		err := service.GoBack(ctx)
363		if err != nil {
364			t.Fatalf("GoBack failed: %v", err)
365		}
366
367		duration := time.Since(start)
368
369		// Navigation should be reasonably fast
370		maxDuration := 100 * time.Millisecond
371		if duration > maxDuration {
372			t.Errorf("Navigation operation too slow: %v (max: %v)", duration, maxDuration)
373		}
374	})
375}
376
377// Helper function to verify database state
378func verifyZoomInDatabase(t *testing.T, queries *db.Queries, url string, expectedZoom float64) {
379	ctx := context.Background()
380
381	// Extract domain from URL (zoom is stored per domain, not full URL)
382	domain := extractDomain(url)
383
384	// Get zoom level from database
385	zoom, err := queries.GetZoomLevel(ctx, domain)
386	if err != nil {
387		t.Fatalf("Failed to get zoom level from database for domain %s: %v", domain, err)
388	}
389
390	if zoom != expectedZoom {
391		t.Errorf("Database zoom mismatch for domain %s. Expected %f, got %f", domain, expectedZoom, zoom)
392	}
393}
394
395// extractDomain extracts domain from URL for zoom persistence
396func extractDomain(url string) string {
397	// This is a simplified version - in real code you'd use net/url
398	// For test purposes, assuming test URLs are simple
399	return "example.com" // Placeholder - would extract actual domain
400}

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