1package cef
2
3import (
4 "context"
5 "strings"
6 "testing"
7 "unsafe"
8
9 purecef "github.com/bnema/purego-cef/cef"
10)
11
12type schemeRegistrarStub struct {
13 scheme string
14 options int32
15 calls int
16}
17
18func (s *schemeRegistrarStub) AddCustomScheme(schemeName string, options int32) int32 {
19 s.calls++
20 s.scheme = schemeName
21 s.options = options
22 return 1
23}
24
25type relaunchCommandLineStub struct {
26 commandLineString string
27}
28
29func (c relaunchCommandLineStub) IsValid() bool { return true }
30func (c relaunchCommandLineStub) IsReadOnly() bool { return true }
31func (c relaunchCommandLineStub) Copy() purecef.CommandLine { return c }
32func (c relaunchCommandLineStub) InitFromArgv(int32, unsafe.Pointer) {}
33func (c relaunchCommandLineStub) InitFromString(string) {}
34func (c relaunchCommandLineStub) Reset() {}
35func (c relaunchCommandLineStub) GetArgv(purecef.StringList) {}
36func (c relaunchCommandLineStub) GetCommandLineString() string { return c.commandLineString }
37func (c relaunchCommandLineStub) GetProgram() string { return "dumber" }
38func (c relaunchCommandLineStub) SetProgram(string) {}
39func (c relaunchCommandLineStub) HasSwitches() bool { return false }
40func (c relaunchCommandLineStub) HasSwitch(string) bool { return false }
41func (c relaunchCommandLineStub) GetSwitchValue(string) string { return "" }
42func (c relaunchCommandLineStub) GetSwitches(purecef.StringMap) {}
43func (c relaunchCommandLineStub) AppendSwitch(string) {}
44func (c relaunchCommandLineStub) AppendSwitchWithValue(string, string) {
45}
46func (c relaunchCommandLineStub) HasArguments() bool { return true }
47func (c relaunchCommandLineStub) GetArguments(purecef.StringList) {}
48func (c relaunchCommandLineStub) AppendArgument(string) {
49}
50func (c relaunchCommandLineStub) PrependWrapper(string) {}
51func (c relaunchCommandLineStub) RemoveSwitch(string) {}
52
53type mutableCommandLineStub struct {
54 relaunchCommandLineStub
55 switches map[string]string
56}
57
58func newMutableCommandLineStub() *mutableCommandLineStub {
59 return &mutableCommandLineStub{switches: make(map[string]string)}
60}
61
62func (c *mutableCommandLineStub) IsReadOnly() bool { return false }
63func (c *mutableCommandLineStub) Copy() purecef.CommandLine { return c }
64func (c *mutableCommandLineStub) Reset() { c.switches = make(map[string]string) }
65func (c *mutableCommandLineStub) HasSwitches() bool { return len(c.switches) > 0 }
66func (c *mutableCommandLineStub) HasSwitch(name string) bool {
67 _, ok := c.switches[name]
68 return ok
69}
70func (c *mutableCommandLineStub) GetSwitchValue(name string) string { return c.switches[name] }
71func (c *mutableCommandLineStub) AppendSwitch(name string) { c.switches[name] = "" }
72func (c *mutableCommandLineStub) AppendSwitchWithValue(name, value string) {
73 c.switches[name] = value
74}
75func (c *mutableCommandLineStub) HasArguments() bool { return false }
76func (c *mutableCommandLineStub) RemoveSwitch(name string) {
77 delete(c.switches, name)
78}
79
80func TestRegisterDumbScheme(t *testing.T) {
81 t.Parallel()
82
83 stub := &schemeRegistrarStub{}
84 registerDumbScheme(stub)
85
86 wantOptions := purecef.SchemeOptionsSchemeOptionStandard |
87 purecef.SchemeOptionsSchemeOptionSecure |
88 purecef.SchemeOptionsSchemeOptionCorsEnabled |
89 purecef.SchemeOptionsSchemeOptionCspBypassing |
90 purecef.SchemeOptionsSchemeOptionFetchEnabled
91
92 if stub.calls != 1 {
93 t.Fatalf("AddCustomScheme call count = %d, want 1", stub.calls)
94 }
95 if stub.scheme != dumbSchemeName {
96 t.Fatalf("scheme = %q, want %q", stub.scheme, dumbSchemeName)
97 }
98 if stub.options != wantOptions {
99 t.Fatalf("options = %d, want %d", stub.options, wantOptions)
100 }
101}
102
103func TestParseBrowseURLFromRelaunchCommandLine(t *testing.T) {
104 t.Parallel()
105
106 got := parseBrowseURLFromRelaunchCommandLine(relaunchCommandLineStub{
107 commandLineString: "dumber browse https://example.com",
108 })
109
110 if got != "https://example.com" {
111 t.Fatalf("parsed browse url = %q, want %q", got, "https://example.com")
112 }
113}
114
115func TestDumberBPH_OnAlreadyRunningAppRelaunch_ForwardsBrowseURLAndReturns1(t *testing.T) {
116 t.Parallel()
117
118 gotURL := ""
119 eng := &Engine{}
120 eng.SetAlreadyRunningAppRelaunchHandler(func(url string) {
121 gotURL = url
122 })
123
124 ret := (&dumberBPH{engine: eng}).OnAlreadyRunningAppRelaunch(relaunchCommandLineStub{
125 commandLineString: "dumber browse https://example.com",
126 }, "")
127
128 if ret != 1 {
129 t.Fatalf("OnAlreadyRunningAppRelaunch returned %d, want 1", ret)
130 }
131 if gotURL != "https://example.com" {
132 t.Fatalf("forwarded browse url = %q, want %q", gotURL, "https://example.com")
133 }
134}
135
136func TestDumberBPH_OnAlreadyRunningAppRelaunch_DoesNotForwardEmptyBrowseURLAndReturns1(t *testing.T) {
137 t.Parallel()
138
139 gotURL := "sentinel"
140 eng := &Engine{}
141 eng.SetAlreadyRunningAppRelaunchHandler(func(url string) {
142 gotURL = url
143 })
144
145 ret := (&dumberBPH{engine: eng}).OnAlreadyRunningAppRelaunch(relaunchCommandLineStub{
146 commandLineString: "dumber browse",
147 }, "")
148
149 if ret != 1 {
150 t.Fatalf("OnAlreadyRunningAppRelaunch returned %d, want 1", ret)
151 }
152 if gotURL != "sentinel" {
153 t.Fatalf("forwarded browse url = %q, want %q", gotURL, "sentinel")
154 }
155}
156
157func TestDumberBPH_OnAlreadyRunningAppRelaunch_DoesNotForwardNonBrowse(t *testing.T) {
158 t.Parallel()
159
160 called := false
161 eng := &Engine{}
162 eng.SetAlreadyRunningAppRelaunchHandler(func(string) {
163 called = true
164 })
165
166 ret := (&dumberBPH{engine: eng}).OnAlreadyRunningAppRelaunch(relaunchCommandLineStub{
167 commandLineString: "dumber",
168 }, "")
169
170 if ret != 0 {
171 t.Fatalf("OnAlreadyRunningAppRelaunch returned %d, want 0", ret)
172 }
173 if called {
174 t.Fatal("handler should not be called for non-browse relaunch")
175 }
176}
177
178func TestConfigureCommandLine_AppendsExpectedSwitches(t *testing.T) {
179 commandLine := newMutableCommandLineStub()
180 configureCommandLine(commandLine)
181
182 if !commandLine.HasSwitch("enable-smooth-scrolling") {
183 t.Fatal("expected enable-smooth-scrolling switch to be appended")
184 }
185 if got := commandLine.GetSwitchValue("autoplay-policy"); got != "no-user-gesture-required" {
186 t.Fatalf("autoplay-policy = %q, want %q", got, "no-user-gesture-required")
187 }
188}
189
190func TestConfigureCommandLine_DisablesWebAuthnByDefault(t *testing.T) {
191 t.Setenv(cefEnableWebAuthnUnsafeEnvVar, "")
192
193 commandLine := newMutableCommandLineStub()
194 configureCommandLine(commandLine)
195
196 want := "WebAuth"
197 if got := commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch); got != want {
198 t.Fatalf("disable-features = %q, want %q", got, want)
199 }
200 if got := commandLine.GetSwitchValue(chromiumDisableBlinkFeaturesSwitch); got != want {
201 t.Fatalf("disable-blink-features = %q, want %q", got, want)
202 }
203}
204
205func TestConfigureCommandLine_WebAuthnUnsafeOptInDoesNotDisableWebAuth(t *testing.T) {
206 t.Setenv(cefEnableWebAuthnUnsafeEnvVar, "1")
207
208 commandLine := newMutableCommandLineStub()
209 configureCommandLine(commandLine)
210
211 if got := commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch); strings.Contains(got, "WebAuth") {
212 t.Fatalf("disable-features = %q, should not contain WebAuth", got)
213 }
214 if got := commandLine.GetSwitchValue(chromiumDisableBlinkFeaturesSwitch); strings.Contains(got, "WebAuth") {
215 t.Fatalf("disable-blink-features = %q, should not contain WebAuth", got)
216 }
217}
218
219func TestConfigureCommandLine_WebAuthnDisablePreservesExistingDisableFeatures(t *testing.T) {
220 commandLine := newMutableCommandLineStub()
221 commandLine.AppendSwitchWithValue(chromiumDisableFeaturesSwitch, "ExistingFeature,WebAuth")
222 commandLine.AppendSwitchWithValue(chromiumDisableBlinkFeaturesSwitch, "ExistingBlinkFeature,WebAuth")
223
224 configureCommandLine(commandLine)
225
226 if got := commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch); got != "ExistingFeature,WebAuth" {
227 t.Fatalf("disable-features = %q, want %q", got, "ExistingFeature,WebAuth")
228 }
229 if got := commandLine.GetSwitchValue(chromiumDisableBlinkFeaturesSwitch); got != "ExistingBlinkFeature,WebAuth" {
230 t.Fatalf("disable-blink-features = %q, want %q", got, "ExistingBlinkFeature,WebAuth")
231 }
232}
233
234func TestConfigureCommandLine_WebAuthnUnsafeOverrideRemovesExistingWebAuthTokens(t *testing.T) {
235 t.Setenv(cefEnableWebAuthnUnsafeEnvVar, "1")
236
237 commandLine := newMutableCommandLineStub()
238 commandLine.AppendSwitchWithValue(chromiumDisableFeaturesSwitch, "ExistingFeature,WebAuth,AnotherFeature")
239 commandLine.AppendSwitchWithValue(chromiumDisableBlinkFeaturesSwitch, "WebAuth,ExistingBlinkFeature")
240
241 configureCommandLine(commandLine)
242
243 if got := commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch); got != "ExistingFeature,AnotherFeature" {
244 t.Fatalf("disable-features = %q, want %q", got, "ExistingFeature,AnotherFeature")
245 }
246 if got := commandLine.GetSwitchValue(chromiumDisableBlinkFeaturesSwitch); got != "ExistingBlinkFeature" {
247 t.Fatalf("disable-blink-features = %q, want %q", got, "ExistingBlinkFeature")
248 }
249}
250
251func TestConfigureCommandLine_WebAuthnUnsafeOverrideRemovesSwitchWhenOnlyWebAuth(t *testing.T) {
252 t.Setenv(cefEnableWebAuthnUnsafeEnvVar, "1")
253
254 commandLine := newMutableCommandLineStub()
255 commandLine.AppendSwitchWithValue(chromiumDisableFeaturesSwitch, "WebAuth")
256
257 configureCommandLine(commandLine)
258
259 if commandLine.HasSwitch(chromiumDisableFeaturesSwitch) {
260 t.Fatalf("disable-features switch should have been removed entirely, but has value %q", commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch))
261 }
262}
263
264func TestConfigureCommandLine_SetsOzonePlatformToWayland(t *testing.T) {
265 commandLine := newMutableCommandLineStub()
266 configureCommandLine(commandLine)
267
268 if got := commandLine.GetSwitchValue("ozone-platform"); got != "wayland" {
269 t.Fatalf("ozone-platform = %q, want %q", got, "wayland")
270 }
271
272 // Dumber-specific switches must remain present.
273 if !commandLine.HasSwitch("enable-smooth-scrolling") {
274 t.Fatal("expected enable-smooth-scrolling switch to be present")
275 }
276 if got := commandLine.GetSwitchValue("autoplay-policy"); got != "no-user-gesture-required" {
277 t.Fatalf("autoplay-policy = %q, want %q", got, "no-user-gesture-required")
278 }
279}
280
281func TestConfigureCommandLine_PreservesExistingOzonePlatform(t *testing.T) {
282 commandLine := newMutableCommandLineStub()
283 commandLine.AppendSwitchWithValue("ozone-platform", "x11")
284
285 configureCommandLine(commandLine)
286
287 if got := commandLine.GetSwitchValue("ozone-platform"); got != "x11" {
288 t.Fatalf("ozone-platform = %q, want %q (bridge preserves existing value)", got, "x11")
289 }
290
291 // Dumber-specific switches must remain present.
292 if !commandLine.HasSwitch("enable-smooth-scrolling") {
293 t.Fatal("expected enable-smooth-scrolling switch to be present")
294 }
295 if got := commandLine.GetSwitchValue("autoplay-policy"); got != "no-user-gesture-required" {
296 t.Fatalf("autoplay-policy = %q, want %q", got, "no-user-gesture-required")
297 }
298}
299
300func TestConfigureCommandLine_PreservesInheritedANGLEBackend(t *testing.T) {
301 commandLine := newMutableCommandLineStub()
302 commandLine.AppendSwitchWithValue("use-angle", "gl-egl")
303
304 configureCommandLine(commandLine)
305
306 if got := commandLine.GetSwitchValue("use-angle"); got != "gl-egl" {
307 t.Fatalf("use-angle = %q, want inherited gl-egl", got)
308 }
309}
310
311func TestConfigureCommandLine_EnableVAAPIEnvAppendsHardwareDecodeFlags(t *testing.T) {
312 t.Setenv(cefEnableVAAPIEnvVar, "1")
313
314 commandLine := newMutableCommandLineStub()
315 commandLine.AppendSwitchWithValue(chromiumEnableFeaturesSwitch, "ExistingFeature")
316
317 configureCommandLine(commandLine)
318
319 gotFeatures := commandLine.GetSwitchValue(chromiumEnableFeaturesSwitch)
320 for _, feature := range []string{
321 "ExistingFeature",
322 "AcceleratedVideoDecoder",
323 "AcceleratedVideoEncoder",
324 "AcceleratedVideoDecodeLinuxGL",
325 "AcceleratedVideoDecodeLinuxZeroCopyGL",
326 "VaapiIgnoreDriverChecks",
327 } {
328 if !strings.Contains(gotFeatures, feature) {
329 t.Fatalf("enable-features = %q, missing %q", gotFeatures, feature)
330 }
331 }
332 if !commandLine.HasSwitch("ignore-gpu-blocklist") {
333 t.Fatal("expected ignore-gpu-blocklist switch")
334 }
335 if !commandLine.HasSwitch("enable-zero-copy") {
336 t.Fatal("expected enable-zero-copy switch")
337 }
338 if !commandLine.HasSwitch("disable-gpu-driver-bug-workaround") {
339 t.Fatal("expected disable-gpu-driver-bug-workaround switch")
340 }
341}
342
343func TestConfigureCommandLine_ChromiumFlagsEnvAppendsSwitches(t *testing.T) {
344 t.Setenv(cefChromiumFlagsEnvVar, `--enable-features=VaapiVideoDecoder,VaapiIgnoreDriverChecks --ignore-gpu-blocklist --autoplay-policy=document-user-activation-required ignored-arg`)
345
346 commandLine := newMutableCommandLineStub()
347 commandLine.AppendSwitchWithValue(chromiumEnableFeaturesSwitch, "ExistingFeature")
348
349 configureCommandLine(commandLine)
350
351 if got := commandLine.GetSwitchValue(chromiumEnableFeaturesSwitch); got != "ExistingFeature,Vulkan,DefaultANGLEVulkan,VulkanFromANGLE,VaapiVideoDecoder,VaapiIgnoreDriverChecks" {
352 t.Fatalf("enable-features = %q, want render stack and env features appended", got)
353 }
354 if !commandLine.HasSwitch("ignore-gpu-blocklist") {
355 t.Fatal("expected ignore-gpu-blocklist switch")
356 }
357 if got := commandLine.GetSwitchValue("autoplay-policy"); got != "document-user-activation-required" {
358 t.Fatalf("autoplay-policy = %q, want env override", got)
359 }
360 if commandLine.HasSwitch("ignored-arg") {
361 t.Fatal("non-switch env token should be ignored")
362 }
363}
364
365func TestParseChromiumSwitchToken(t *testing.T) {
366 t.Parallel()
367
368 tests := []struct {
369 name string
370 token string
371 wantName string
372 wantValue string
373 wantOK bool
374 }{
375 {name: "long switch with value", token: "--enable-features=VaapiVideoDecoder", wantName: "enable-features", wantValue: "VaapiVideoDecoder", wantOK: true},
376 {name: "long switch without value", token: "--ignore-gpu-blocklist", wantName: "ignore-gpu-blocklist", wantOK: true},
377 {name: "single dash switch", token: "-enable-zero-copy", wantName: "enable-zero-copy", wantOK: true},
378 {name: "empty value", token: "--flag=", wantName: "flag", wantOK: true},
379 {name: "embedded equals", token: "--flag=a=b", wantName: "flag", wantValue: "a=b", wantOK: true},
380 {name: "non switch", token: "youtube.com", wantOK: false},
381 {name: "sentinel", token: "--", wantOK: false},
382 {name: "empty", token: " ", wantOK: false},
383 }
384
385 for _, tt := range tests {
386 t.Run(tt.name, func(t *testing.T) {
387 gotName, gotValue, gotOK := parseChromiumSwitchToken(tt.token)
388 if gotName != tt.wantName || gotValue != tt.wantValue || gotOK != tt.wantOK {
389 t.Fatalf("parseChromiumSwitchToken(%q) = (%q, %q, %v), want (%q, %q, %v)", tt.token, gotName, gotValue, gotOK, tt.wantName, tt.wantValue, tt.wantOK)
390 }
391 })
392 }
393}
394
395func TestAppendUniqueCommaSeparatedSwitchValues(t *testing.T) {
396 t.Run("trims whitespace and skips empty existing values", func(t *testing.T) {
397 commandLine := newMutableCommandLineStub()
398 commandLine.AppendSwitchWithValue("disable-features", " , ExistingFeature , ")
399
400 appendUniqueCommaSeparatedSwitchValues(commandLine, "disable-features", "FeatureA")
401
402 want := "ExistingFeature,FeatureA"
403 if got := commandLine.GetSwitchValue("disable-features"); got != want {
404 t.Fatalf("disable-features = %q, want %q", got, want)
405 }
406 })
407
408 t.Run("deduplicates existing and appended values", func(t *testing.T) {
409 commandLine := newMutableCommandLineStub()
410 commandLine.AppendSwitchWithValue("disable-features", "FeatureA,FeatureA")
411
412 appendUniqueCommaSeparatedSwitchValues(commandLine, "disable-features", "FeatureA", "FeatureB")
413
414 want := "FeatureA,FeatureB"
415 if got := commandLine.GetSwitchValue("disable-features"); got != want {
416 t.Fatalf("disable-features = %q, want %q", got, want)
417 }
418 })
419
420 t.Run("empty appended values are no-op", func(t *testing.T) {
421 commandLine := newMutableCommandLineStub()
422 commandLine.AppendSwitchWithValue("disable-features", "ExistingFeature")
423
424 appendUniqueCommaSeparatedSwitchValues(commandLine, "disable-features")
425
426 if got := commandLine.GetSwitchValue("disable-features"); got != "ExistingFeature" {
427 t.Fatalf("disable-features = %q, want ExistingFeature", got)
428 }
429 })
430
431 t.Run("nil command line is no-op", func(_ *testing.T) {
432 appendUniqueCommaSeparatedSwitchValues(nil, "disable-features", "FeatureA")
433 })
434}
435
436func TestDumberBPH_OnBeforeChildProcessLaunch_AppendsNoZygote(t *testing.T) {
437 commandLine := newMutableCommandLineStub()
438 commandLine.AppendSwitchWithValue("type", "renderer")
439
440 (&dumberBPH{engine: &Engine{ctx: context.Background()}}).OnBeforeChildProcessLaunch(commandLine)
441
442 if !commandLine.HasSwitch("no-zygote") {
443 t.Fatal("expected no-zygote switch to be appended for child processes")
444 }
445}
446
447func TestConfigureCommandLine_CEFRenderNodeEnvOverridesExistingRenderNode(t *testing.T) {
448 t.Setenv(cefRenderNodeEnvVar, "/dev/dri/renderD129")
449
450 commandLine := newMutableCommandLineStub()
451 commandLine.AppendSwitchWithValue(chromiumRenderNodeOverrideSwitch, "/dev/dri/renderD128")
452
453 configureCommandLine(commandLine)
454
455 if got := commandLine.GetSwitchValue(chromiumRenderNodeOverrideSwitch); got != "/dev/dri/renderD129" {
456 t.Fatalf("render node override = %q, want /dev/dri/renderD129", got)
457 }
458}
459
460func TestConfigureCommandLine_CEFRenderNodeEnvSentinelsRemoveExistingRenderNode(t *testing.T) {
461 for _, value := range []string{"auto", "default", "none", "off", "disable", "disabled"} {
462 t.Run(value, func(t *testing.T) {
463 t.Setenv(cefRenderNodeEnvVar, value)
464
465 commandLine := newMutableCommandLineStub()
466 commandLine.AppendSwitchWithValue(chromiumRenderNodeOverrideSwitch, "/dev/dri/renderD128")
467
468 configureCommandLine(commandLine)
469
470 if commandLine.HasSwitch(chromiumRenderNodeOverrideSwitch) {
471 t.Fatalf("render node override still present: %q", commandLine.GetSwitchValue(chromiumRenderNodeOverrideSwitch))
472 }
473 })
474 }
475}
476
477func TestDumberBPH_OnBeforeChildProcessLaunch_AppliesRenderingAndHardwareDecodeFlags(t *testing.T) {
478 t.Setenv(cef2gtkAngleBackendVar, "vulkan")
479 t.Setenv(cefEnableVAAPIEnvVar, "1")
480
481 commandLine := newMutableCommandLineStub()
482 commandLine.AppendSwitchWithValue("type", "gpu-process")
483 commandLine.AppendSwitchWithValue("use-angle", "gl-egl")
484
485 (&dumberBPH{engine: &Engine{ctx: context.Background()}}).OnBeforeChildProcessLaunch(commandLine)
486
487 if got := commandLine.GetSwitchValue("use-angle"); got != "vulkan" {
488 t.Fatalf("use-angle = %q, want vulkan", got)
489 }
490 if got := commandLine.GetSwitchValue(chromiumEnableFeaturesSwitch); !strings.Contains(got, "AcceleratedVideoDecoder") || !strings.Contains(got, "VulkanFromANGLE") {
491 t.Fatalf("enable-features = %q, want hardware decode and Vulkan ANGLE features", got)
492 }
493 if !commandLine.HasSwitch("ignore-gpu-blocklist") {
494 t.Fatal("expected ignore-gpu-blocklist switch")
495 }
496 if !commandLine.HasSwitch("enable-zero-copy") {
497 t.Fatal("expected enable-zero-copy switch")
498 }
499 if !commandLine.HasSwitch("no-zygote") {
500 t.Fatal("expected no-zygote switch")
501 }
502}
503
504func TestDumberBPH_OnBeforeChildProcessLaunch_DisablesWebAuthnByDefault(t *testing.T) {
505 t.Setenv(cefEnableWebAuthnUnsafeEnvVar, "")
506
507 commandLine := newMutableCommandLineStub()
508 commandLine.AppendSwitchWithValue("type", "renderer")
509
510 (&dumberBPH{engine: &Engine{ctx: context.Background()}}).OnBeforeChildProcessLaunch(commandLine)
511
512 want := "WebAuth"
513 if got := commandLine.GetSwitchValue(chromiumDisableFeaturesSwitch); got != want {
514 t.Fatalf("disable-features = %q, want %q", got, want)
515 }
516 if got := commandLine.GetSwitchValue(chromiumDisableBlinkFeaturesSwitch); got != want {
517 t.Fatalf("disable-blink-features = %q, want %q", got, want)
518 }
519 if !commandLine.HasSwitch("no-zygote") {
520 t.Fatal("expected no-zygote switch to be appended")
521 }
522}