v0.32.0 0617278

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 idle residency timeout disables",
202			mutate: func(cfg *Config) {
203				cfg.Engine.CEF.IdleRuntimeTimeoutMs = 0
204			},
205			wantErr: false,
206		},
207		{
208			name: "maximum idle residency timeout accepted",
209			mutate: func(cfg *Config) {
210				cfg.Engine.CEF.IdleRuntimeTimeoutMs = 300000
211			},
212			wantErr: false,
213		},
214		{
215			name: "negative idle residency timeout",
216			mutate: func(cfg *Config) {
217				cfg.Engine.CEF.IdleRuntimeTimeoutMs = -1
218			},
219			wantErr:  true,
220			wantText: "engine.cef.idle_runtime_timeout_ms",
221		},
222		{
223			name: "oversized idle residency timeout",
224			mutate: func(cfg *Config) {
225				cfg.Engine.CEF.IdleRuntimeTimeoutMs = 300001
226			},
227			wantErr:  true,
228			wantText: "engine.cef.idle_runtime_timeout_ms",
229		},
230		{
231			name: "zero wheel multiplier",
232			mutate: func(cfg *Config) {
233				cfg.Engine.CEF.Input.ScrollWheelMultiplier = 0
234			},
235			wantErr:  true,
236			wantText: "engine.cef.input.scroll_wheel_multiplier",
237		},
238		{
239			name: "negative precise multiplier",
240			mutate: func(cfg *Config) {
241				cfg.Engine.CEF.Input.ScrollPreciseMultiplier = -0.1
242			},
243			wantErr:  true,
244			wantText: "engine.cef.input.scroll_precise_multiplier",
245		},
246		{
247			name: "zero horizontal multiplier",
248			mutate: func(cfg *Config) {
249				cfg.Engine.CEF.Input.ScrollHorizontalMultiplier = 0
250			},
251			wantErr:  true,
252			wantText: "engine.cef.input.scroll_horizontal_multiplier",
253		},
254		{
255			name: "negative vertical multiplier",
256			mutate: func(cfg *Config) {
257				cfg.Engine.CEF.Input.ScrollVerticalMultiplier = -0.1
258			},
259			wantErr:  true,
260			wantText: "engine.cef.input.scroll_vertical_multiplier",
261		},
262		{
263			name: "nan horizontal multiplier",
264			mutate: func(cfg *Config) {
265				cfg.Engine.CEF.Input.ScrollHorizontalMultiplier = math.NaN()
266			},
267			wantErr:  true,
268			wantText: "engine.cef.input.scroll_horizontal_multiplier",
269		},
270		{
271			name: "infinite vertical multiplier",
272			mutate: func(cfg *Config) {
273				cfg.Engine.CEF.Input.ScrollVerticalMultiplier = math.Inf(1)
274			},
275			wantErr:  true,
276			wantText: "engine.cef.input.scroll_vertical_multiplier",
277		},
278		{
279			name: "zero max delta disables clamp",
280			mutate: func(cfg *Config) {
281				cfg.Engine.CEF.Input.ScrollMaxDelta = 0
282			},
283			wantErr: false,
284		},
285		{
286			name: "negative max delta",
287			mutate: func(cfg *Config) {
288				cfg.Engine.CEF.Input.ScrollMaxDelta = -1
289			},
290			wantErr:  true,
291			wantText: "engine.cef.input.scroll_max_delta",
292		},
293		{
294			name: "zero touchpad navigation min delta",
295			mutate: func(cfg *Config) {
296				cfg.Engine.CEF.Input.TouchpadNavigationMinDelta = 0
297			},
298			wantErr:  true,
299			wantText: "engine.cef.input.touchpad_navigation_min_delta",
300		},
301		{
302			name: "nan touchpad navigation max vertical ratio",
303			mutate: func(cfg *Config) {
304				cfg.Engine.CEF.Input.TouchpadNavigationMaxVerticalRatio = math.NaN()
305			},
306			wantErr:  true,
307			wantText: "engine.cef.input.touchpad_navigation_max_vertical_ratio",
308		},
309	}
310
311	for _, tt := range tests {
312		t.Run(tt.name, func(t *testing.T) {
313			cfg := DefaultConfig()
314			tt.mutate(cfg)
315
316			err := validateConfig(cfg)
317			if tt.wantErr {
318				require.Error(t, err)
319				assert.Contains(t, err.Error(), tt.wantText)
320				return
321			}
322			require.NoError(t, err)
323		})
324	}
325}
326
327func TestValidateConfig_ExternalLinksAcceptsSupportedValues(t *testing.T) {
328	for _, behavior := range []ExternalLinkBehavior{
329		ExternalLinkBehaviorWindowed,
330		ExternalLinkBehaviorTabbed,
331		ExternalLinkBehaviorSplit,
332		ExternalLinkBehaviorStacked,
333	} {
334		for _, placement := range []ExternalLinkPlacement{
335			ExternalLinkPlacementRight,
336			ExternalLinkPlacementLeft,
337			ExternalLinkPlacementTop,
338			ExternalLinkPlacementBottom,
339		} {
340			cfg := DefaultConfig()
341			cfg.Workspace.ExternalLinks.Behavior = behavior
342			cfg.Workspace.ExternalLinks.Placement = placement
343			require.NoError(t, validateConfig(cfg))
344		}
345	}
346}
347
348func TestValidateConfig_ExternalLinksRejectsUnsupportedValues(t *testing.T) {
349	cfg := DefaultConfig()
350	cfg.Workspace.ExternalLinks.Behavior = "invalid"
351	cfg.Workspace.ExternalLinks.Placement = "invalid"
352
353	err := validateConfig(cfg)
354	require.Error(t, err)
355	assert.Contains(t, err.Error(), "workspace.external_links.behavior")
356	assert.Contains(t, err.Error(), "workspace.external_links.placement")
357}
358
359func TestValidateConfig_WorkspaceNewPaneURLAllowsExistingAbsoluteLocalPath(t *testing.T) {
360	tmpDir := t.TempDir()
361	path := filepath.Join(tmpDir, "page.html")
362	require.NoError(t, os.WriteFile(path, []byte("ok"), 0644))
363
364	cfg := DefaultConfig()
365	cfg.Workspace.NewPaneURL = path
366
367	require.NoError(t, validateConfig(cfg))
368}
369
370func TestValidateConfig_WorkspaceNewPaneURLRejectsRelativeLocalPathLikeValues(t *testing.T) {
371	// This test mutates process CWD; do not add t.Parallel here.
372	tmpDir := t.TempDir()
373	oldWD, err := os.Getwd()
374	require.NoError(t, err)
375	childDir := filepath.Join(tmpDir, "child")
376	require.NoError(t, os.Mkdir(childDir, 0755))
377	require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "page.html"), []byte("ok"), 0644))
378	require.NoError(t, os.WriteFile(filepath.Join(childDir, "page.html"), []byte("ok"), 0644))
379	require.NoError(t, os.Chdir(childDir))
380	t.Cleanup(func() { _ = os.Chdir(oldWD) })
381
382	for _, value := range []string{"./page.html", "../page.html"} {
383		t.Run(value, func(t *testing.T) {
384			cfg := DefaultConfig()
385			cfg.Workspace.NewPaneURL = value
386
387			err := validateConfig(cfg)
388			require.Error(t, err)
389			assert.Contains(t, err.Error(), "workspace.new_pane_url relative local path cannot be resolved")
390		})
391	}
392}
393
394func TestValidateConfig_WorkspaceNewPaneURLRejectsMissingLocalPathLikeValues(t *testing.T) {
395	for _, value := range []string{"/missing/dumber/page.html", "~/missing-dumber-page.html"} {
396		t.Run(value, func(t *testing.T) {
397			cfg := DefaultConfig()
398			cfg.Workspace.NewPaneURL = value
399
400			err := validateConfig(cfg)
401			require.Error(t, err)
402			assert.Contains(t, err.Error(), "workspace.new_pane_url local path must exist")
403		})
404	}
405}
406
407func TestValidateConfig_WorkspaceNewPaneURLRejectsExistingBareRelativeFile(t *testing.T) {
408	// This test mutates process CWD; do not add t.Parallel here.
409	tmpDir := t.TempDir()
410	oldWD, err := os.Getwd()
411	require.NoError(t, err)
412	require.NoError(t, os.Chdir(tmpDir))
413	t.Cleanup(func() { _ = os.Chdir(oldWD) })
414	require.NoError(t, os.WriteFile("README", []byte("ok"), 0644))
415	require.NoError(t, os.WriteFile("index.html", []byte("ok"), 0644))
416
417	for _, value := range []string{"README", "index.html"} {
418		t.Run(value, func(t *testing.T) {
419			cfg := DefaultConfig()
420			cfg.Workspace.NewPaneURL = value
421
422			validationErr := validateConfig(cfg)
423			require.Error(t, validationErr)
424			assert.Contains(
425				t,
426				validationErr.Error(),
427				"workspace.new_pane_url relative local path cannot be resolved",
428			)
429		})
430	}
431}
432
433func TestValidateConfig_WorkspaceNewPaneURLRejectsMissingBareRelativeValue(t *testing.T) {
434	// This test mutates process CWD; do not add t.Parallel here.
435	tmpDir := t.TempDir()
436	oldWD, err := os.Getwd()
437	require.NoError(t, err)
438	require.NoError(t, os.Chdir(tmpDir))
439	t.Cleanup(func() { _ = os.Chdir(oldWD) })
440
441	cfg := DefaultConfig()
442	cfg.Workspace.NewPaneURL = "missing-local-file"
443
444	validationErr := validateConfig(cfg)
445	require.Error(t, validationErr)
446	assert.Contains(t, validationErr.Error(), "workspace.new_pane_url")
447	assert.Contains(t, validationErr.Error(), "relative local path cannot be resolved without config file context")
448}
449
450func TestValidateConfig_WebKitDefaultProfileIgnoresZeroGPUThreads(t *testing.T) {
451	cfg := DefaultConfig()
452	cfg.Engine.Profile = ProfileDefault
453	cfg.Engine.WebKit.SkiaGPUPaintingThreads = 0
454
455	err := validateConfig(cfg)
456	require.NoError(t, err)
457}

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