v0.33.0 62a09f5

webview_viewport_test.go RawHistoryFolder

1package cef
2
3import (
4	"context"
5	"testing"
6	"time"
7
8	purecef "github.com/bnema/purego-cef/cef"
9	"github.com/stretchr/testify/require"
10)
11
12type viewportSyncOrderHost struct {
13	purecef.BrowserHost
14	calls             []string
15	zoomLevel         float64
16	getZoomLevelCalls int
17}
18
19type pendingBrowserCreateObservedSizeBridgeStub struct {
20	sizeWidth       int32
21	sizeHeight      int32
22	refreshWidth    int32
23	refreshHeight   int32
24	refreshCalls    int
25	refreshProvided bool
26}
27
28func (s *pendingBrowserCreateObservedSizeBridgeStub) Size() (int32, int32) {
29	return s.sizeWidth, s.sizeHeight
30}
31
32func (s *pendingBrowserCreateObservedSizeBridgeStub) RefreshObservedSizeOnGTKThread() (int32, int32) {
33	s.refreshCalls++
34	if s.refreshProvided {
35		return s.refreshWidth, s.refreshHeight
36	}
37	return s.sizeWidth, s.sizeHeight
38}
39
40func (h *viewportSyncOrderHost) WasHidden(state int32) {
41	h.calls = append(h.calls, "WasHidden")
42	if state != 0 {
43		h.calls = append(h.calls, "WasHidden(nonzero)")
44	}
45}
46
47func (h *viewportSyncOrderHost) NotifyScreenInfoChanged() {
48	h.calls = append(h.calls, "NotifyScreenInfoChanged")
49}
50
51func (h *viewportSyncOrderHost) WasResized() {
52	h.calls = append(h.calls, "WasResized")
53}
54
55func (h *viewportSyncOrderHost) Invalidate(_ purecef.PaintElementType) {
56	h.calls = append(h.calls, "Invalidate")
57}
58
59func (h *viewportSyncOrderHost) SetZoomLevel(level float64) {
60	h.calls = append(h.calls, "SetZoomLevel")
61	h.zoomLevel = level
62}
63
64func (h *viewportSyncOrderHost) GetZoomLevel() float64 {
65	h.getZoomLevelCalls++
66	return h.zoomLevel
67}
68
69func TestNotifyBrowserViewportSync_VisibleCallsFullSequence(t *testing.T) {
70	host := &viewportSyncOrderHost{}
71
72	notifyBrowserViewportSync(host, true)
73
74	require.Equal(t, []string{"WasHidden", "NotifyScreenInfoChanged", "WasResized", "Invalidate"}, host.calls)
75}
76
77func TestNotifyBrowserViewportSync_HiddenSkipsWasHidden(t *testing.T) {
78	host := &viewportSyncOrderHost{}
79
80	notifyBrowserViewportSync(host, false)
81
82	require.Equal(t, []string{"NotifyScreenInfoChanged", "WasResized", "Invalidate"}, host.calls)
83}
84
85func TestNotifyViewportSyncOnCEFUIThread_PostsViewportWorkOnly(t *testing.T) {
86	oldNewTask := cefNewTask
87	oldPostDelayedTask := cefPostDelayedTask
88	defer func() {
89		cefNewTask = oldNewTask
90		cefPostDelayedTask = oldPostDelayedTask
91	}()
92
93	cefNewTask = func(task purecef.Task) purecef.Task { return task }
94
95	var scheduled purecef.Task
96	cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
97		require.Equal(t, purecef.ThreadIDTidUi, threadID)
98		require.NotNil(t, task)
99		if delayMs == 0 && scheduled == nil {
100			scheduled = task
101		}
102		return 1
103	}
104
105	host := &viewportSyncOrderHost{}
106	wv := &WebView{ctx: context.Background(), host: host}
107	wv.notifyViewportSyncOnCEFUIThread(host, true)
108
109	require.Empty(t, host.calls)
110	require.NotNil(t, scheduled)
111
112	scheduled.Execute()
113	require.Equal(t, []string{
114		"WasHidden",
115		"NotifyScreenInfoChanged",
116		"WasResized",
117		"Invalidate",
118	}, host.calls)
119}
120
121func TestNotifyViewportSyncOnCEFUIThread_RemainsViewportOnlyAcrossRepeatedResizes(t *testing.T) {
122	oldNewTask := cefNewTask
123	oldPostDelayedTask := cefPostDelayedTask
124	defer func() {
125		cefNewTask = oldNewTask
126		cefPostDelayedTask = oldPostDelayedTask
127	}()
128
129	cefNewTask = func(task purecef.Task) purecef.Task { return task }
130
131	var scheduled purecef.Task
132	cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
133		if delayMs == 0 && scheduled == nil {
134			scheduled = task
135		}
136		return 1
137	}
138
139	host := &viewportSyncOrderHost{}
140	wv := &WebView{ctx: context.Background(), host: host}
141
142	wv.notifyViewportSyncOnCEFUIThread(host, true)
143	require.NotNil(t, scheduled)
144	scheduled.Execute()
145
146	host.calls = nil
147	scheduled = nil
148
149	wv.notifyViewportSyncOnCEFUIThread(host, true)
150	require.NotNil(t, scheduled)
151	scheduled.Execute()
152
153	require.Equal(t, []string{
154		"NotifyScreenInfoChanged",
155		"WasResized",
156		"Invalidate",
157	}, host.calls)
158}
159
160func TestSyncZoomForBackingScaleOnCEFUIThread_PostsZoomWork(t *testing.T) {
161	oldNewTask := cefNewTask
162	oldPostDelayedTask := cefPostDelayedTask
163	defer func() {
164		cefNewTask = oldNewTask
165		cefPostDelayedTask = oldPostDelayedTask
166	}()
167
168	cefNewTask = func(task purecef.Task) purecef.Task { return task }
169
170	var scheduled purecef.Task
171	cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
172		require.Equal(t, purecef.ThreadIDTidUi, threadID)
173		require.NotNil(t, task)
174		if delayMs == 0 && scheduled == nil {
175			scheduled = task
176		}
177		return 1
178	}
179
180	host := &viewportSyncOrderHost{}
181	wv := &WebView{ctx: context.Background(), host: host}
182	wv.syncZoomForBackingScaleOnCEFUIThread(host, "gtk-size-observer")
183
184	require.Empty(t, host.calls)
185	require.NotNil(t, scheduled)
186
187	scheduled.Execute()
188	require.Equal(t, []string{
189		"SetZoomLevel",
190		"NotifyScreenInfoChanged",
191	}, host.calls)
192}
193
194func TestSyncZoomForBackingScaleOnCEFUIThread_SkipsRedundantReapplyWhenBackingScaleUnchanged(t *testing.T) {
195	oldNewTask := cefNewTask
196	oldPostDelayedTask := cefPostDelayedTask
197	defer func() {
198		cefNewTask = oldNewTask
199		cefPostDelayedTask = oldPostDelayedTask
200	}()
201
202	cefNewTask = func(task purecef.Task) purecef.Task { return task }
203
204	var scheduled purecef.Task
205	cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
206		if delayMs == 0 && scheduled == nil {
207			scheduled = task
208		}
209		return 1
210	}
211
212	host := &viewportSyncOrderHost{}
213	wv := &WebView{ctx: context.Background(), host: host, zoomCompensation: zoomCompensationStub{compensation: 1.25}}
214
215	wv.syncZoomForBackingScaleOnCEFUIThread(host, "gtk-size-observer")
216	require.NotNil(t, scheduled)
217	scheduled.Execute()
218
219	host.calls = nil
220	scheduled = nil
221
222	wv.syncZoomForBackingScaleOnCEFUIThread(host, "gtk-size-observer")
223	require.Nil(t, scheduled)
224	require.Empty(t, host.calls)
225}
226
227func TestSyncZoomForBackingScaleOnCEFUIThread_ReappliesWhenCompensationChangesWithoutTouchingUserZoom(t *testing.T) {
228	oldNewTask := cefNewTask
229	oldPostDelayedTask := cefPostDelayedTask
230	defer func() {
231		cefNewTask = oldNewTask
232		cefPostDelayedTask = oldPostDelayedTask
233	}()
234
235	cefNewTask = func(task purecef.Task) purecef.Task { return task }
236
237	var scheduled []purecef.Task
238	cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
239		require.Equal(t, purecef.ThreadIDTidUi, threadID)
240		if delayMs == 0 {
241			scheduled = append(scheduled, task)
242		}
243		return 1
244	}
245
246	host := &viewportSyncOrderHost{}
247	wv := &WebView{ctx: context.Background(), host: host, zoomCompensation: zoomCompensationStub{compensation: 1.25}}
248	wv.zoomFactor.Store(1.3)
249
250	wv.syncZoomForBackingScaleOnCEFUIThread(host, "gtk-size-observer")
251	require.Len(t, scheduled, 1)
252	scheduled[0].Execute()
253	require.InDelta(t, cefZoomFromFactor(1.625), host.zoomLevel, 1e-9)
254
255	// Moving to a different output scale must reapply the same user zoom under the
256	// new compensation and must not mutate the persisted user zoom.
257	host.calls = nil
258	scheduled = nil
259	wv.zoomCompensation = zoomCompensationStub{compensation: 2.0}
260
261	wv.syncZoomForBackingScaleOnCEFUIThread(host, "gtk-size-observer")
262	require.Len(t, scheduled, 1)
263	scheduled[0].Execute()
264	require.InDelta(t, cefZoomFromFactor(2.6), host.zoomLevel, 1e-9)
265	require.InDelta(t, 1.3, wv.GetZoomLevel(), 1e-9, "user zoom must survive an output transition")
266	require.Equal(t, []string{"SetZoomLevel", "NotifyScreenInfoChanged"}, host.calls)
267}
268
269func TestSyncZoomForBackingScaleOnCEFUIThread_SkipsStaleHostAndDestroyedView(t *testing.T) {
270	oldNewTask := cefNewTask
271	oldPostDelayedTask := cefPostDelayedTask
272	defer func() {
273		cefNewTask = oldNewTask
274		cefPostDelayedTask = oldPostDelayedTask
275	}()
276
277	cefNewTask = func(task purecef.Task) purecef.Task { return task }
278
279	var scheduled []purecef.Task
280	cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, _ int64) int32 {
281		scheduled = append(scheduled, task)
282		return 1
283	}
284
285	capturedHost := &viewportSyncOrderHost{}
286	currentHost := &viewportSyncOrderHost{}
287	wv := &WebView{ctx: context.Background(), host: currentHost, zoomCompensation: zoomCompensationStub{compensation: 1.25}}
288
289	wv.syncZoomForBackingScaleOnCEFUIThread(capturedHost, "host-replaced")
290	require.Len(t, scheduled, 1)
291	scheduled[0].Execute()
292	require.Empty(t, capturedHost.calls)
293	require.Empty(t, currentHost.calls)
294
295	destroyed := &WebView{ctx: context.Background(), host: currentHost, zoomCompensation: zoomCompensationStub{compensation: 1.25}}
296	destroyed.destroyed.Store(true)
297	scheduled = nil
298	destroyed.syncZoomForBackingScaleOnCEFUIThread(currentHost, "destroyed")
299	require.Len(t, scheduled, 1)
300	scheduled[0].Execute()
301	require.Empty(t, currentHost.calls)
302}
303
304func TestSyncZoomForBackingScaleOnCEFUIThread_SkipsMissingHost(t *testing.T) {
305	wv := &WebView{ctx: context.Background(), zoomCompensation: zoomCompensationStub{compensation: 1.25}}
306	wv.syncZoomForBackingScaleOnCEFUIThread(nil, "missing-host")
307	require.Zero(t, wv.appliedZoomCompensation())
308}
309
310func TestNotifyViewportSyncOnCEFUIThread_SkipsStaleHost(t *testing.T) {
311	oldNewTask := cefNewTask
312	oldPostDelayedTask := cefPostDelayedTask
313	defer func() {
314		cefNewTask = oldNewTask
315		cefPostDelayedTask = oldPostDelayedTask
316	}()
317
318	cefNewTask = func(task purecef.Task) purecef.Task { return task }
319
320	var scheduled purecef.Task
321	cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, _ int64) int32 {
322		scheduled = task
323		return 1
324	}
325
326	capturedHost := &viewportSyncOrderHost{}
327	currentHost := &viewportSyncOrderHost{}
328	wv := &WebView{ctx: context.Background(), host: currentHost}
329	wv.notifyViewportSyncOnCEFUIThread(capturedHost, true)
330
331	require.NotNil(t, scheduled)
332	scheduled.Execute()
333	require.Empty(t, capturedHost.calls)
334	require.Empty(t, currentHost.calls)
335}
336
337func TestNotifyViewportSyncOnCEFUIThread_SkipsDestroyedWebView(t *testing.T) {
338	oldNewTask := cefNewTask
339	oldPostDelayedTask := cefPostDelayedTask
340	defer func() {
341		cefNewTask = oldNewTask
342		cefPostDelayedTask = oldPostDelayedTask
343	}()
344
345	cefNewTask = func(task purecef.Task) purecef.Task { return task }
346
347	var scheduled purecef.Task
348	cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, _ int64) int32 {
349		scheduled = task
350		return 1
351	}
352
353	host := &viewportSyncOrderHost{}
354	wv := &WebView{ctx: context.Background(), host: host}
355	wv.destroyed.Store(true)
356	wv.notifyViewportSyncOnCEFUIThread(host, true)
357
358	require.NotNil(t, scheduled)
359	scheduled.Execute()
360	require.Empty(t, host.calls)
361}
362
363func TestObservedViewportSizeReady(t *testing.T) {
364	tests := []struct {
365		name            string
366		observedWidth   int32
367		observedHeight  int32
368		allocatedWidth  int32
369		allocatedHeight int32
370		want            bool
371	}{
372		{
373			name:            "exact match",
374			observedWidth:   793,
375			observedHeight:  1753,
376			allocatedWidth:  793,
377			allocatedHeight: 1753,
378			want:            true,
379		},
380		{
381			name:            "one pixel drift is accepted",
382			observedWidth:   792,
383			observedHeight:  1754,
384			allocatedWidth:  793,
385			allocatedHeight: 1753,
386			want:            true,
387		},
388		{
389			name:            "fallback observed size is rejected",
390			observedWidth:   1,
391			observedHeight:  1,
392			allocatedWidth:  793,
393			allocatedHeight: 1753,
394			want:            false,
395		},
396		{
397			name:            "missing allocation is rejected",
398			observedWidth:   793,
399			observedHeight:  1753,
400			allocatedWidth:  0,
401			allocatedHeight: 1753,
402			want:            false,
403		},
404		{
405			name:            "large mismatch is rejected",
406			observedWidth:   1200,
407			observedHeight:  1400,
408			allocatedWidth:  1587,
409			allocatedHeight: 1679,
410			want:            false,
411		},
412	}
413
414	for _, tt := range tests {
415		t.Run(tt.name, func(t *testing.T) {
416			require.Equal(t, tt.want, observedViewportSizeReady(tt.observedWidth, tt.observedHeight, tt.allocatedWidth, tt.allocatedHeight))
417		})
418	}
419}
420
421func TestPendingBrowserCreateObservedSize_RefreshesStaleFallbackSize(t *testing.T) {
422	bridge := &pendingBrowserCreateObservedSizeBridgeStub{
423		sizeWidth:       1,
424		sizeHeight:      1,
425		refreshWidth:    1055,
426		refreshHeight:   1719,
427		refreshProvided: true,
428	}
429
430	observedWidth, observedHeight, ready := pendingBrowserCreateObservedSize(bridge, 1055, 1719)
431
432	require.True(t, ready)
433	require.Equal(t, int32(1055), observedWidth)
434	require.Equal(t, int32(1719), observedHeight)
435	require.Equal(t, 1, bridge.refreshCalls)
436}
437
438func TestPendingBrowserCreateObservedSize_SkipsRefreshWhenObservedSizeAlreadyMatches(t *testing.T) {
439	bridge := &pendingBrowserCreateObservedSizeBridgeStub{sizeWidth: 1055, sizeHeight: 1719}
440
441	observedWidth, observedHeight, ready := pendingBrowserCreateObservedSize(bridge, 1055, 1719)
442
443	require.True(t, ready)
444	require.Equal(t, int32(1055), observedWidth)
445	require.Equal(t, int32(1719), observedHeight)
446	require.Equal(t, 0, bridge.refreshCalls)
447}
448
449func TestShouldDeferPendingBrowserCreateFromViewportSync_WaitsForNativePopupAttach(t *testing.T) {
450	wv := &WebView{pendingCreate: &pendingBrowserCreate{}, nativePopupCandidate: true}
451
452	require.True(t, wv.shouldDeferPendingBrowserCreateFromViewportSync())
453
454	wv.nativePopupFallbackStarted = true
455	require.False(t, wv.shouldDeferPendingBrowserCreateFromViewportSync())
456}
457
458func TestPreparePendingBrowserCreateObservedSizeRetry_CoalescesScheduling(t *testing.T) {
459	oldScheduleAfter := cefScheduleAfter
460	defer func() { cefScheduleAfter = oldScheduleAfter }()
461
462	scheduleCalls := 0
463	cefScheduleAfter = func(delay time.Duration, fn func()) {
464		require.Equal(t, pendingBrowserCreateObservedSizeRetryDelay, delay)
465		require.NotNil(t, fn)
466		scheduleCalls++
467	}
468
469	wv := &WebView{pendingCreate: &pendingBrowserCreate{}}
470
471	attempt, action := wv.preparePendingBrowserCreateObservedSizeRetry(context.Background(), "test")
472	require.Equal(t, 1, attempt)
473	require.Equal(t, pendingBrowserCreateObservedSizeRetryScheduled, action)
474	require.Equal(t, 1, scheduleCalls)
475
476	attempt, action = wv.preparePendingBrowserCreateObservedSizeRetry(context.Background(), "test")
477	require.Equal(t, 1, attempt)
478	require.Equal(t, pendingBrowserCreateObservedSizeRetryAlreadyScheduled, action)
479	require.Equal(t, 1, scheduleCalls)
480}
481
482func TestPreparePendingBrowserCreateObservedSizeRetry_MaxRetriesProceedsWithoutScheduling(t *testing.T) {
483	oldScheduleAfter := cefScheduleAfter
484	defer func() { cefScheduleAfter = oldScheduleAfter }()
485
486	scheduleCalls := 0
487	cefScheduleAfter = func(delay time.Duration, fn func()) {
488		require.Equal(t, pendingBrowserCreateObservedSizeRetryDelay, delay)
489		require.NotNil(t, fn)
490		scheduleCalls++
491	}
492
493	wv := &WebView{pendingCreate: &pendingBrowserCreate{observedSizeRetries: pendingBrowserCreateObservedSizeMaxRetries}}
494
495	attempt, action := wv.preparePendingBrowserCreateObservedSizeRetry(context.Background(), "test")
496	require.Equal(t, pendingBrowserCreateObservedSizeMaxRetries+1, attempt)
497	require.Equal(t, pendingBrowserCreateObservedSizeRetryProceedWithoutDelay, action)
498	require.Equal(t, 0, scheduleCalls)
499	require.False(t, wv.pendingCreate.observedSizeRetryScheduled)
500}
501
502func TestScheduleResizeRepaintPulse_CoalescesToLatestSequence(t *testing.T) {
503	oldNewTask := cefNewTask
504	oldPostDelayedTask := cefPostDelayedTask
505	defer func() {
506		cefNewTask = oldNewTask
507		cefPostDelayedTask = oldPostDelayedTask
508	}()
509
510	cefNewTask = func(task purecef.Task) purecef.Task { return task }
511
512	var scheduled []purecef.Task
513	var delays []int64
514	cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
515		require.Equal(t, purecef.ThreadIDTidUi, threadID)
516		require.NotNil(t, task)
517		scheduled = append(scheduled, task)
518		delays = append(delays, delayMs)
519		return 1
520	}
521
522	host := &viewportSyncOrderHost{}
523	wv := &WebView{ctx: context.Background(), host: host}
524
525	wv.scheduleResizeRepaintPulse(context.Background(), "first")
526	wv.scheduleResizeRepaintPulse(context.Background(), "second")
527
528	const pulses = 6
529	require.Equal(t, []int64{16, 48, 96, 160, 250, 400, 16, 48, 96, 160, 250, 400}, delays)
530	require.Len(t, scheduled, 2*pulses)
531
532	for _, task := range scheduled[:pulses] {
533		task.Execute()
534	}
535	require.Empty(t, host.calls)
536
537	for _, task := range scheduled[pulses:] {
538		task.Execute()
539	}
540	// Only the first two pulses resync the viewport; later ones only request a
541	// refresh frame so the surface ID is not reallocated repeatedly.
542	require.Equal(t, []string{
543		"NotifyScreenInfoChanged", "WasResized", "Invalidate",
544		"NotifyScreenInfoChanged", "WasResized", "Invalidate",
545		"Invalidate",
546		"Invalidate",
547		"Invalidate",
548		"Invalidate",
549	}, host.calls)
550}
551
552func TestScheduleResizeRepaintPulse_SkipsStaleHost(t *testing.T) {
553	oldNewTask := cefNewTask
554	oldPostDelayedTask := cefPostDelayedTask
555	defer func() {
556		cefNewTask = oldNewTask
557		cefPostDelayedTask = oldPostDelayedTask
558	}()
559
560	cefNewTask = func(task purecef.Task) purecef.Task { return task }
561
562	var scheduled []purecef.Task
563	cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, _ int64) int32 {
564		require.Equal(t, purecef.ThreadIDTidUi, threadID)
565		require.NotNil(t, task)
566		scheduled = append(scheduled, task)
567		return 1
568	}
569
570	capturedHost := &viewportSyncOrderHost{}
571	currentHost := &viewportSyncOrderHost{}
572	wv := &WebView{ctx: context.Background(), host: capturedHost}
573
574	wv.scheduleResizeRepaintPulse(context.Background(), "stale-host")
575	wv.host = currentHost
576
577	require.Len(t, scheduled, 6)
578	for _, task := range scheduled {
579		task.Execute()
580	}
581
582	require.Empty(t, capturedHost.calls)
583	require.Empty(t, currentHost.calls)
584}
585
586func TestScheduleResizeRepaintPulse_SkipsWhenHostNilAtScheduleTime(t *testing.T) {
587	oldNewTask := cefNewTask
588	oldPostDelayedTask := cefPostDelayedTask
589	defer func() {
590		cefNewTask = oldNewTask
591		cefPostDelayedTask = oldPostDelayedTask
592	}()
593
594	cefNewTask = func(task purecef.Task) purecef.Task { return task }
595
596	var scheduled []purecef.Task
597	cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, _ int64) int32 {
598		scheduled = append(scheduled, task)
599		return 1
600	}
601
602	wv := &WebView{ctx: context.Background()}
603
604	wv.scheduleResizeRepaintPulse(context.Background(), "nil-host")
605
606	require.Empty(t, scheduled)
607}
608
609func TestScheduleResizeRepaintPulse_SkipsWhenHostClearedBeforeExecution(t *testing.T) {
610	oldNewTask := cefNewTask
611	oldPostDelayedTask := cefPostDelayedTask
612	defer func() {
613		cefNewTask = oldNewTask
614		cefPostDelayedTask = oldPostDelayedTask
615	}()
616
617	cefNewTask = func(task purecef.Task) purecef.Task { return task }
618
619	var scheduled []purecef.Task
620	cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, _ int64) int32 {
621		require.Equal(t, purecef.ThreadIDTidUi, threadID)
622		require.NotNil(t, task)
623		scheduled = append(scheduled, task)
624		return 1
625	}
626
627	host := &viewportSyncOrderHost{}
628	wv := &WebView{ctx: context.Background(), host: host}
629
630	wv.scheduleResizeRepaintPulse(context.Background(), "nil-host-before-execute")
631	wv.host = nil
632
633	require.Len(t, scheduled, 6)
634	for _, task := range scheduled {
635		task.Execute()
636	}
637
638	require.Empty(t, host.calls)
639}

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