v0.30.0 eb3068f

validation_test.go RawHistoryFolder

1package config
2
3import (
4	"math"
5	"os"
6	"path/filepath"
7	"testing"
8
9	"github.com/stretchr/testify/assert"
10	"github.com/stretchr/testify/require"
11)
12
13func TestValidateConfig_EngineType(t *testing.T) {
14	t.Setenv("DUMBER_ENGINE", "")
15
16	tests := []struct {
17		name       string
18		engineType string
19		wantErr    bool
20	}{
21		{name: "cef", engineType: EngineTypeCEF, wantErr: false},
22		{name: "webkit", engineType: EngineTypeWebKit, wantErr: false},
23		{name: "empty is invalid", engineType: "", wantErr: true},
24		{name: "invalid", engineType: "netscape", wantErr: true},
25	}
26
27	for _, tt := range tests {
28		t.Run(tt.name, func(t *testing.T) {
29			cfg := DefaultConfig()
30			cfg.Engine.Type = tt.engineType
31
32			err := validateConfig(cfg)
33			if tt.wantErr {
34				require.Error(t, err)
35				assert.Contains(t, err.Error(), "engine.type")
36				return
37			}
38			require.NoError(t, err)
39		})
40	}
41}
42
43func TestValidateConfig_EngineTypeIgnoresEnvOverride(t *testing.T) {
44	t.Run("invalid configured type is still rejected when env override is valid", func(t *testing.T) {
45		t.Setenv("DUMBER_ENGINE", EngineTypeWebKit)
46		cfg := DefaultConfig()
47		cfg.Engine.Type = "netscape"
48
49		err := validateConfig(cfg)
50
51		require.Error(t, err)
52		assert.Contains(t, err.Error(), "engine.type")
53	})
54
55	t.Run("valid configured type is not rejected when env override is invalid", func(t *testing.T) {
56		t.Setenv("DUMBER_ENGINE", "netscape")
57		cfg := DefaultConfig()
58		cfg.Engine.Type = EngineTypeCEF
59
60		err := validateConfig(cfg)
61
62		require.NoError(t, err)
63	})
64}
65
66func TestValidateConfig_EngineCookiePolicy(t *testing.T) {
67	tests := []struct {
68		name         string
69		cookiePolicy CookiePolicy
70		wantErr      bool
71	}{
72		{name: "always", cookiePolicy: CookiePolicyAlways, wantErr: false},
73		{name: "no_third_party", cookiePolicy: CookiePolicyNoThirdParty, wantErr: false},
74		{name: "never", cookiePolicy: CookiePolicyNever, wantErr: false},
75		{name: "invalid", cookiePolicy: CookiePolicy("bad_policy"), wantErr: true},
76	}
77
78	for _, tt := range tests {
79		t.Run(tt.name, func(t *testing.T) {
80			cfg := DefaultConfig()
81			cfg.Engine.CookiePolicy = tt.cookiePolicy
82
83			err := validateConfig(cfg)
84			if tt.wantErr {
85				require.Error(t, err)
86				assert.Contains(t, err.Error(), "engine.cookie_policy")
87				return
88			}
89			require.NoError(t, err)
90		})
91	}
92}
93
94func TestValidateConfig_WebKitMediaSettings(t *testing.T) {
95	tests := []struct {
96		name      string
97		mutate    func(*Config)
98		wantErr   bool
99		wantField string
100	}{
101		{
102			name: "valid gl rendering modes",
103			mutate: func(cfg *Config) {
104				cfg.Engine.WebKit.GLRenderingMode = GLRenderingModeGLES2
105			},
106			wantErr: false,
107		},
108		{
109			name: "invalid gl rendering mode",
110			mutate: func(cfg *Config) {
111				cfg.Engine.WebKit.GLRenderingMode = GLRenderingMode("metal")
112			},
113			wantErr:   true,
114			wantField: "engine.webkit.gl_rendering_mode",
115		},
116		{
117			name: "gstreamer debug level lower bound",
118			mutate: func(cfg *Config) {
119				cfg.Engine.WebKit.GStreamerDebugLevel = 0
120			},
121			wantErr: false,
122		},
123		{
124			name: "gstreamer debug level upper bound",
125			mutate: func(cfg *Config) {
126				cfg.Engine.WebKit.GStreamerDebugLevel = 5
127			},
128			wantErr: false,
129		},
130		{
131			name: "negative gstreamer debug level",
132			mutate: func(cfg *Config) {
133				cfg.Engine.WebKit.GStreamerDebugLevel = -1
134			},
135			wantErr:   true,
136			wantField: "engine.webkit.gstreamer_debug_level",
137		},
138		{
139			name: "too high gstreamer debug level",
140			mutate: func(cfg *Config) {
141				cfg.Engine.WebKit.GStreamerDebugLevel = 6
142			},
143			wantErr:   true,
144			wantField: "engine.webkit.gstreamer_debug_level",
145		},
146	}
147
148	for _, tt := range tests {
149		t.Run(tt.name, func(t *testing.T) {
150			cfg := DefaultConfig()
151			tt.mutate(cfg)
152
153			err := validateConfig(cfg)
154			if tt.wantErr {
155				require.Error(t, err)
156				assert.Contains(t, err.Error(), tt.wantField)
157				return
158			}
159			require.NoError(t, err)
160		})
161	}
162}
163
164func TestValidateConfig_CEFConfig(t *testing.T) {
165	tests := []struct {
166		name     string
167		mutate   func(*Config)
168		wantErr  bool
169		wantText string
170	}{
171		{
172			name: "valid defaults",
173			mutate: func(_ *Config) {
174			},
175			wantErr: false,
176		},
177		{
178			name: "invalid log severity",
179			mutate: func(cfg *Config) {
180				cfg.Engine.CEF.LogSeverity = 7
181			},
182			wantErr:  true,
183			wantText: "engine.cef.log_severity",
184		},
185		{
186			name: "zero frame rate uses default",
187			mutate: func(cfg *Config) {
188				cfg.Engine.CEF.WindowlessFrameRate = 0
189			},
190			wantErr: false,
191		},
192		{
193			name: "negative frame rate",
194			mutate: func(cfg *Config) {
195				cfg.Engine.CEF.WindowlessFrameRate = -1
196			},
197			wantErr:  true,
198			wantText: "engine.cef.windowless_frame_rate",
199		},
200		{
201			name: "zero wheel multiplier",
202			mutate: func(cfg *Config) {
203				cfg.Engine.CEF.Input.ScrollWheelMultiplier = 0
204			},
205			wantErr:  true,
206			wantText: "engine.cef.input.scroll_wheel_multiplier",
207		},
208		{
209			name: "negative precise multiplier",
210			mutate: func(cfg *Config) {
211				cfg.Engine.CEF.Input.ScrollPreciseMultiplier = -0.1
212			},
213			wantErr:  true,
214			wantText: "engine.cef.input.scroll_precise_multiplier",
215		},
216		{
217			name: "zero horizontal multiplier",
218			mutate: func(cfg *Config) {
219				cfg.Engine.CEF.Input.ScrollHorizontalMultiplier = 0
220			},
221			wantErr:  true,
222			wantText: "engine.cef.input.scroll_horizontal_multiplier",
223		},
224		{
225			name: "negative vertical multiplier",
226			mutate: func(cfg *Config) {
227				cfg.Engine.CEF.Input.ScrollVerticalMultiplier = -0.1
228			},
229			wantErr:  true,
230			wantText: "engine.cef.input.scroll_vertical_multiplier",
231		},
232		{
233			name: "nan horizontal multiplier",
234			mutate: func(cfg *Config) {
235				cfg.Engine.CEF.Input.ScrollHorizontalMultiplier = math.NaN()
236			},
237			wantErr:  true,
238			wantText: "engine.cef.input.scroll_horizontal_multiplier",
239		},
240		{
241			name: "infinite vertical multiplier",
242			mutate: func(cfg *Config) {
243				cfg.Engine.CEF.Input.ScrollVerticalMultiplier = math.Inf(1)
244			},
245			wantErr:  true,
246			wantText: "engine.cef.input.scroll_vertical_multiplier",
247		},
248		{
249			name: "zero max delta disables clamp",
250			mutate: func(cfg *Config) {
251				cfg.Engine.CEF.Input.ScrollMaxDelta = 0
252			},
253			wantErr: false,
254		},
255		{
256			name: "negative max delta",
257			mutate: func(cfg *Config) {
258				cfg.Engine.CEF.Input.ScrollMaxDelta = -1
259			},
260			wantErr:  true,
261			wantText: "engine.cef.input.scroll_max_delta",
262		},
263		{
264			name: "zero touchpad navigation min delta",
265			mutate: func(cfg *Config) {
266				cfg.Engine.CEF.Input.TouchpadNavigationMinDelta = 0
267			},
268			wantErr:  true,
269			wantText: "engine.cef.input.touchpad_navigation_min_delta",
270		},
271		{
272			name: "nan touchpad navigation max vertical ratio",
273			mutate: func(cfg *Config) {
274				cfg.Engine.CEF.Input.TouchpadNavigationMaxVerticalRatio = math.NaN()
275			},
276			wantErr:  true,
277			wantText: "engine.cef.input.touchpad_navigation_max_vertical_ratio",
278		},
279	}
280
281	for _, tt := range tests {
282		t.Run(tt.name, func(t *testing.T) {
283			cfg := DefaultConfig()
284			tt.mutate(cfg)
285
286			err := validateConfig(cfg)
287			if tt.wantErr {
288				require.Error(t, err)
289				assert.Contains(t, err.Error(), tt.wantText)
290				return
291			}
292			require.NoError(t, err)
293		})
294	}
295}
296
297func TestValidateConfig_WorkspaceNewPaneURLAllowsExistingAbsoluteLocalPath(t *testing.T) {
298	tmpDir := t.TempDir()
299	path := filepath.Join(tmpDir, "page.html")
300	require.NoError(t, os.WriteFile(path, []byte("ok"), 0644))
301
302	cfg := DefaultConfig()
303	cfg.Workspace.NewPaneURL = path
304
305	require.NoError(t, validateConfig(cfg))
306}
307
308func TestValidateConfig_WorkspaceNewPaneURLRejectsRelativeLocalPathLikeValues(t *testing.T) {
309	// This test mutates process CWD; do not add t.Parallel here.
310	tmpDir := t.TempDir()
311	oldWD, err := os.Getwd()
312	require.NoError(t, err)
313	childDir := filepath.Join(tmpDir, "child")
314	require.NoError(t, os.Mkdir(childDir, 0755))
315	require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "page.html"), []byte("ok"), 0644))
316	require.NoError(t, os.WriteFile(filepath.Join(childDir, "page.html"), []byte("ok"), 0644))
317	require.NoError(t, os.Chdir(childDir))
318	t.Cleanup(func() { _ = os.Chdir(oldWD) })
319
320	for _, value := range []string{"./page.html", "../page.html"} {
321		t.Run(value, func(t *testing.T) {
322			cfg := DefaultConfig()
323			cfg.Workspace.NewPaneURL = value
324
325			err := validateConfig(cfg)
326			require.Error(t, err)
327			assert.Contains(t, err.Error(), "workspace.new_pane_url relative local path cannot be resolved")
328		})
329	}
330}
331
332func TestValidateConfig_WorkspaceNewPaneURLRejectsMissingLocalPathLikeValues(t *testing.T) {
333	for _, value := range []string{"/missing/dumber/page.html", "~/missing-dumber-page.html"} {
334		t.Run(value, func(t *testing.T) {
335			cfg := DefaultConfig()
336			cfg.Workspace.NewPaneURL = value
337
338			err := validateConfig(cfg)
339			require.Error(t, err)
340			assert.Contains(t, err.Error(), "workspace.new_pane_url local path must exist")
341		})
342	}
343}
344
345func TestValidateConfig_WorkspaceNewPaneURLRejectsExistingBareRelativeFile(t *testing.T) {
346	// This test mutates process CWD; do not add t.Parallel here.
347	tmpDir := t.TempDir()
348	oldWD, err := os.Getwd()
349	require.NoError(t, err)
350	require.NoError(t, os.Chdir(tmpDir))
351	t.Cleanup(func() { _ = os.Chdir(oldWD) })
352	require.NoError(t, os.WriteFile("README", []byte("ok"), 0644))
353	require.NoError(t, os.WriteFile("index.html", []byte("ok"), 0644))
354
355	for _, value := range []string{"README", "index.html"} {
356		t.Run(value, func(t *testing.T) {
357			cfg := DefaultConfig()
358			cfg.Workspace.NewPaneURL = value
359
360			validationErr := validateConfig(cfg)
361			require.Error(t, validationErr)
362			assert.Contains(
363				t,
364				validationErr.Error(),
365				"workspace.new_pane_url relative local path cannot be resolved",
366			)
367		})
368	}
369}
370
371func TestValidateConfig_WorkspaceNewPaneURLRejectsMissingBareRelativeValue(t *testing.T) {
372	// This test mutates process CWD; do not add t.Parallel here.
373	tmpDir := t.TempDir()
374	oldWD, err := os.Getwd()
375	require.NoError(t, err)
376	require.NoError(t, os.Chdir(tmpDir))
377	t.Cleanup(func() { _ = os.Chdir(oldWD) })
378
379	cfg := DefaultConfig()
380	cfg.Workspace.NewPaneURL = "missing-local-file"
381
382	validationErr := validateConfig(cfg)
383	require.Error(t, validationErr)
384	assert.Contains(t, validationErr.Error(), "workspace.new_pane_url")
385	assert.Contains(t, validationErr.Error(), "relative local path cannot be resolved without config file context")
386}
387
388func TestValidateConfig_WebKitDefaultProfileIgnoresZeroGPUThreads(t *testing.T) {
389	cfg := DefaultConfig()
390	cfg.Engine.Profile = ProfileDefault
391	cfg.Engine.WebKit.SkiaGPUPaintingThreads = 0
392
393	err := validateConfig(cfg)
394	require.NoError(t, err)
395}

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