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}