v0.1 bf7c821

browser_controls_test.go RawHistoryFolder

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}

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