v0.31.0 bf0756b

app_test.go RawHistoryFolder

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}

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