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_ExternalLinksAcceptsSupportedValues(t *testing.T) {
298 for _, behavior := range []ExternalLinkBehavior{
299 ExternalLinkBehaviorWindowed,
300 ExternalLinkBehaviorTabbed,
301 ExternalLinkBehaviorSplit,
302 ExternalLinkBehaviorStacked,
303 } {
304 for _, placement := range []ExternalLinkPlacement{
305 ExternalLinkPlacementRight,
306 ExternalLinkPlacementLeft,
307 ExternalLinkPlacementTop,
308 ExternalLinkPlacementBottom,
309 } {
310 cfg := DefaultConfig()
311 cfg.Workspace.ExternalLinks.Behavior = behavior
312 cfg.Workspace.ExternalLinks.Placement = placement
313 require.NoError(t, validateConfig(cfg))
314 }
315 }
316}
317
318func TestValidateConfig_ExternalLinksRejectsUnsupportedValues(t *testing.T) {
319 cfg := DefaultConfig()
320 cfg.Workspace.ExternalLinks.Behavior = "invalid"
321 cfg.Workspace.ExternalLinks.Placement = "invalid"
322
323 err := validateConfig(cfg)
324 require.Error(t, err)
325 assert.Contains(t, err.Error(), "workspace.external_links.behavior")
326 assert.Contains(t, err.Error(), "workspace.external_links.placement")
327}
328
329func TestValidateConfig_WorkspaceNewPaneURLAllowsExistingAbsoluteLocalPath(t *testing.T) {
330 tmpDir := t.TempDir()
331 path := filepath.Join(tmpDir, "page.html")
332 require.NoError(t, os.WriteFile(path, []byte("ok"), 0644))
333
334 cfg := DefaultConfig()
335 cfg.Workspace.NewPaneURL = path
336
337 require.NoError(t, validateConfig(cfg))
338}
339
340func TestValidateConfig_WorkspaceNewPaneURLRejectsRelativeLocalPathLikeValues(t *testing.T) {
341 // This test mutates process CWD; do not add t.Parallel here.
342 tmpDir := t.TempDir()
343 oldWD, err := os.Getwd()
344 require.NoError(t, err)
345 childDir := filepath.Join(tmpDir, "child")
346 require.NoError(t, os.Mkdir(childDir, 0755))
347 require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "page.html"), []byte("ok"), 0644))
348 require.NoError(t, os.WriteFile(filepath.Join(childDir, "page.html"), []byte("ok"), 0644))
349 require.NoError(t, os.Chdir(childDir))
350 t.Cleanup(func() { _ = os.Chdir(oldWD) })
351
352 for _, value := range []string{"./page.html", "../page.html"} {
353 t.Run(value, func(t *testing.T) {
354 cfg := DefaultConfig()
355 cfg.Workspace.NewPaneURL = value
356
357 err := validateConfig(cfg)
358 require.Error(t, err)
359 assert.Contains(t, err.Error(), "workspace.new_pane_url relative local path cannot be resolved")
360 })
361 }
362}
363
364func TestValidateConfig_WorkspaceNewPaneURLRejectsMissingLocalPathLikeValues(t *testing.T) {
365 for _, value := range []string{"/missing/dumber/page.html", "~/missing-dumber-page.html"} {
366 t.Run(value, func(t *testing.T) {
367 cfg := DefaultConfig()
368 cfg.Workspace.NewPaneURL = value
369
370 err := validateConfig(cfg)
371 require.Error(t, err)
372 assert.Contains(t, err.Error(), "workspace.new_pane_url local path must exist")
373 })
374 }
375}
376
377func TestValidateConfig_WorkspaceNewPaneURLRejectsExistingBareRelativeFile(t *testing.T) {
378 // This test mutates process CWD; do not add t.Parallel here.
379 tmpDir := t.TempDir()
380 oldWD, err := os.Getwd()
381 require.NoError(t, err)
382 require.NoError(t, os.Chdir(tmpDir))
383 t.Cleanup(func() { _ = os.Chdir(oldWD) })
384 require.NoError(t, os.WriteFile("README", []byte("ok"), 0644))
385 require.NoError(t, os.WriteFile("index.html", []byte("ok"), 0644))
386
387 for _, value := range []string{"README", "index.html"} {
388 t.Run(value, func(t *testing.T) {
389 cfg := DefaultConfig()
390 cfg.Workspace.NewPaneURL = value
391
392 validationErr := validateConfig(cfg)
393 require.Error(t, validationErr)
394 assert.Contains(
395 t,
396 validationErr.Error(),
397 "workspace.new_pane_url relative local path cannot be resolved",
398 )
399 })
400 }
401}
402
403func TestValidateConfig_WorkspaceNewPaneURLRejectsMissingBareRelativeValue(t *testing.T) {
404 // This test mutates process CWD; do not add t.Parallel here.
405 tmpDir := t.TempDir()
406 oldWD, err := os.Getwd()
407 require.NoError(t, err)
408 require.NoError(t, os.Chdir(tmpDir))
409 t.Cleanup(func() { _ = os.Chdir(oldWD) })
410
411 cfg := DefaultConfig()
412 cfg.Workspace.NewPaneURL = "missing-local-file"
413
414 validationErr := validateConfig(cfg)
415 require.Error(t, validationErr)
416 assert.Contains(t, validationErr.Error(), "workspace.new_pane_url")
417 assert.Contains(t, validationErr.Error(), "relative local path cannot be resolved without config file context")
418}
419
420func TestValidateConfig_WebKitDefaultProfileIgnoresZeroGPUThreads(t *testing.T) {
421 cfg := DefaultConfig()
422 cfg.Engine.Profile = ProfileDefault
423 cfg.Engine.WebKit.SkiaGPUPaintingThreads = 0
424
425 err := validateConfig(cfg)
426 require.NoError(t, err)
427}