v0.29.0 21b3150

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}
17
18type pendingBrowserCreateObservedSizeBridgeStub struct {
19	sizeWidth       int32
20	sizeHeight      int32
21	refreshWidth    int32
22	refreshHeight   int32
23	refreshCalls    int
24	refreshProvided bool
25}
26
27func (s *pendingBrowserCreateObservedSizeBridgeStub) Size() (int32, int32) {
28	return s.sizeWidth, s.sizeHeight
29}
30
31func (s *pendingBrowserCreateObservedSizeBridgeStub) RefreshObservedSizeOnGTKThread() (int32, int32) {
32	s.refreshCalls++
33	if s.refreshProvided {
34		return s.refreshWidth, s.refreshHeight
35	}
36	return s.sizeWidth, s.sizeHeight
37}
38
39func (h *viewportSyncOrderHost) WasHidden(state int32) {
40	h.calls = append(h.calls, "WasHidden")
41	if state != 0 {
42		h.calls = append(h.calls, "WasHidden(nonzero)")
43	}
44}
45
46func (h *viewportSyncOrderHost) NotifyScreenInfoChanged() {
47	h.calls = append(h.calls, "NotifyScreenInfoChanged")
48}
49
50func (h *viewportSyncOrderHost) WasResized() {
51	h.calls = append(h.calls, "WasResized")
52}
53
54func (h *viewportSyncOrderHost) Invalidate(_ purecef.PaintElementType) {
55	h.calls = append(h.calls, "Invalidate")
56}
57
58func (h *viewportSyncOrderHost) SetZoomLevel(level float64) {
59	h.calls = append(h.calls, "SetZoomLevel")
60	h.zoomLevel = level
61}
62
63func (h *viewportSyncOrderHost) GetZoomLevel() float64 {
64	return h.zoomLevel
65}
66
67func TestNotifyBrowserViewportSync_VisibleCallsFullSequence(t *testing.T) {
68	host := &viewportSyncOrderHost{}
69
70	notifyBrowserViewportSync(host, true)
71
72	require.Equal(t, []string{"WasHidden", "NotifyScreenInfoChanged", "WasResized", "Invalidate"}, host.calls)
73}
74
75func TestNotifyBrowserViewportSync_HiddenSkipsWasHidden(t *testing.T) {
76	host := &viewportSyncOrderHost{}
77
78	notifyBrowserViewportSync(host, false)
79
80	require.Equal(t, []string{"NotifyScreenInfoChanged", "WasResized", "Invalidate"}, host.calls)
81}
82
83func TestNotifyViewportSyncOnCEFUIThread_PostsViewportWorkOnly(t *testing.T) {
84	oldNewTask := cefNewTask
85	oldPostDelayedTask := cefPostDelayedTask
86	defer func() {
87		cefNewTask = oldNewTask
88		cefPostDelayedTask = oldPostDelayedTask
89	}()
90
91	cefNewTask = func(task purecef.Task) purecef.Task { return task }
92
93	var scheduled purecef.Task
94	cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
95		require.Equal(t, purecef.ThreadIDTidUi, threadID)
96		require.NotNil(t, task)
97		if delayMs == 0 && scheduled == nil {
98			scheduled = task
99		}
100		return 1
101	}
102
103	host := &viewportSyncOrderHost{}
104	wv := &WebView{ctx: context.Background(), host: host}
105	wv.notifyViewportSyncOnCEFUIThread(host, true)
106
107	require.Empty(t, host.calls)
108	require.NotNil(t, scheduled)
109
110	scheduled.Execute()
111	require.Equal(t, []string{
112		"WasHidden",
113		"NotifyScreenInfoChanged",
114		"WasResized",
115		"Invalidate",
116	}, host.calls)
117}
118
119func TestNotifyViewportSyncOnCEFUIThread_RemainsViewportOnlyAcrossRepeatedResizes(t *testing.T) {
120	oldNewTask := cefNewTask
121	oldPostDelayedTask := cefPostDelayedTask
122	defer func() {
123		cefNewTask = oldNewTask
124		cefPostDelayedTask = oldPostDelayedTask
125	}()
126
127	cefNewTask = func(task purecef.Task) purecef.Task { return task }
128
129	var scheduled purecef.Task
130	cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
131		if delayMs == 0 && scheduled == nil {
132			scheduled = task
133		}
134		return 1
135	}
136
137	host := &viewportSyncOrderHost{}
138	wv := &WebView{ctx: context.Background(), host: host}
139
140	wv.notifyViewportSyncOnCEFUIThread(host, true)
141	require.NotNil(t, scheduled)
142	scheduled.Execute()
143
144	host.calls = nil
145	scheduled = nil
146
147	wv.notifyViewportSyncOnCEFUIThread(host, true)
148	require.NotNil(t, scheduled)
149	scheduled.Execute()
150
151	require.Equal(t, []string{
152		"WasHidden",
153		"NotifyScreenInfoChanged",
154		"WasResized",
155		"Invalidate",
156	}, host.calls)
157}
158
159func TestSyncZoomForBackingScaleOnCEFUIThread_PostsZoomWork(t *testing.T) {
160	oldNewTask := cefNewTask
161	oldPostDelayedTask := cefPostDelayedTask
162	defer func() {
163		cefNewTask = oldNewTask
164		cefPostDelayedTask = oldPostDelayedTask
165	}()
166
167	cefNewTask = func(task purecef.Task) purecef.Task { return task }
168
169	var scheduled purecef.Task
170	cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
171		require.Equal(t, purecef.ThreadIDTidUi, threadID)
172		require.NotNil(t, task)
173		if delayMs == 0 && scheduled == nil {
174			scheduled = task
175		}
176		return 1
177	}
178
179	host := &viewportSyncOrderHost{}
180	wv := &WebView{ctx: context.Background(), host: host}
181	wv.syncZoomForBackingScaleOnCEFUIThread(host, "gtk-size-observer")
182
183	require.Empty(t, host.calls)
184	require.NotNil(t, scheduled)
185
186	scheduled.Execute()
187	require.Equal(t, []string{
188		"SetZoomLevel",
189		"NotifyScreenInfoChanged",
190	}, host.calls)
191}
192
193func TestSyncZoomForBackingScaleOnCEFUIThread_SkipsRedundantReapplyWhenBackingScaleUnchanged(t *testing.T) {
194	oldNewTask := cefNewTask
195	oldPostDelayedTask := cefPostDelayedTask
196	defer func() {
197		cefNewTask = oldNewTask
198		cefPostDelayedTask = oldPostDelayedTask
199	}()
200
201	cefNewTask = func(task purecef.Task) purecef.Task { return task }
202
203	var scheduled purecef.Task
204	cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
205		if delayMs == 0 && scheduled == nil {
206			scheduled = task
207		}
208		return 1
209	}
210
211	host := &viewportSyncOrderHost{}
212	wv := &WebView{ctx: context.Background(), host: host}
213
214	wv.syncZoomForBackingScaleOnCEFUIThread(host, "gtk-size-observer")
215	require.NotNil(t, scheduled)
216	scheduled.Execute()
217
218	host.calls = nil
219	scheduled = nil
220
221	wv.syncZoomForBackingScaleOnCEFUIThread(host, "gtk-size-observer")
222	require.Nil(t, scheduled)
223	require.Empty(t, host.calls)
224}
225
226func TestNotifyViewportSyncOnCEFUIThread_SkipsStaleHost(t *testing.T) {
227	oldNewTask := cefNewTask
228	oldPostDelayedTask := cefPostDelayedTask
229	defer func() {
230		cefNewTask = oldNewTask
231		cefPostDelayedTask = oldPostDelayedTask
232	}()
233
234	cefNewTask = func(task purecef.Task) purecef.Task { return task }
235
236	var scheduled purecef.Task
237	cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, _ int64) int32 {
238		scheduled = task
239		return 1
240	}
241
242	capturedHost := &viewportSyncOrderHost{}
243	currentHost := &viewportSyncOrderHost{}
244	wv := &WebView{ctx: context.Background(), host: currentHost}
245	wv.notifyViewportSyncOnCEFUIThread(capturedHost, true)
246
247	require.NotNil(t, scheduled)
248	scheduled.Execute()
249	require.Empty(t, capturedHost.calls)
250	require.Empty(t, currentHost.calls)
251}
252
253func TestNotifyViewportSyncOnCEFUIThread_SkipsDestroyedWebView(t *testing.T) {
254	oldNewTask := cefNewTask
255	oldPostDelayedTask := cefPostDelayedTask
256	defer func() {
257		cefNewTask = oldNewTask
258		cefPostDelayedTask = oldPostDelayedTask
259	}()
260
261	cefNewTask = func(task purecef.Task) purecef.Task { return task }
262
263	var scheduled purecef.Task
264	cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, _ int64) int32 {
265		scheduled = task
266		return 1
267	}
268
269	host := &viewportSyncOrderHost{}
270	wv := &WebView{ctx: context.Background(), host: host}
271	wv.destroyed.Store(true)
272	wv.notifyViewportSyncOnCEFUIThread(host, true)
273
274	require.NotNil(t, scheduled)
275	scheduled.Execute()
276	require.Empty(t, host.calls)
277}
278
279func TestObservedViewportSizeReady(t *testing.T) {
280	tests := []struct {
281		name            string
282		observedWidth   int32
283		observedHeight  int32
284		allocatedWidth  int32
285		allocatedHeight int32
286		want            bool
287	}{
288		{
289			name:            "exact match",
290			observedWidth:   793,
291			observedHeight:  1753,
292			allocatedWidth:  793,
293			allocatedHeight: 1753,
294			want:            true,
295		},
296		{
297			name:            "one pixel drift is accepted",
298			observedWidth:   792,
299			observedHeight:  1754,
300			allocatedWidth:  793,
301			allocatedHeight: 1753,
302			want:            true,
303		},
304		{
305			name:            "fallback observed size is rejected",
306			observedWidth:   1,
307			observedHeight:  1,
308			allocatedWidth:  793,
309			allocatedHeight: 1753,
310			want:            false,
311		},
312		{
313			name:            "missing allocation is rejected",
314			observedWidth:   793,
315			observedHeight:  1753,
316			allocatedWidth:  0,
317			allocatedHeight: 1753,
318			want:            false,
319		},
320		{
321			name:            "large mismatch is rejected",
322			observedWidth:   1200,
323			observedHeight:  1400,
324			allocatedWidth:  1587,
325			allocatedHeight: 1679,
326			want:            false,
327		},
328	}
329
330	for _, tt := range tests {
331		t.Run(tt.name, func(t *testing.T) {
332			require.Equal(t, tt.want, observedViewportSizeReady(tt.observedWidth, tt.observedHeight, tt.allocatedWidth, tt.allocatedHeight))
333		})
334	}
335}
336
337func TestPendingBrowserCreateObservedSize_RefreshesStaleFallbackSize(t *testing.T) {
338	bridge := &pendingBrowserCreateObservedSizeBridgeStub{
339		sizeWidth:       1,
340		sizeHeight:      1,
341		refreshWidth:    1055,
342		refreshHeight:   1719,
343		refreshProvided: true,
344	}
345
346	observedWidth, observedHeight, ready := pendingBrowserCreateObservedSize(bridge, 1055, 1719)
347
348	require.True(t, ready)
349	require.Equal(t, int32(1055), observedWidth)
350	require.Equal(t, int32(1719), observedHeight)
351	require.Equal(t, 1, bridge.refreshCalls)
352}
353
354func TestPendingBrowserCreateObservedSize_SkipsRefreshWhenObservedSizeAlreadyMatches(t *testing.T) {
355	bridge := &pendingBrowserCreateObservedSizeBridgeStub{sizeWidth: 1055, sizeHeight: 1719}
356
357	observedWidth, observedHeight, ready := pendingBrowserCreateObservedSize(bridge, 1055, 1719)
358
359	require.True(t, ready)
360	require.Equal(t, int32(1055), observedWidth)
361	require.Equal(t, int32(1719), observedHeight)
362	require.Equal(t, 0, bridge.refreshCalls)
363}
364
365func TestShouldDeferPendingBrowserCreateFromViewportSync_WaitsForNativePopupAttach(t *testing.T) {
366	wv := &WebView{pendingCreate: &pendingBrowserCreate{}, nativePopupCandidate: true}
367
368	require.True(t, wv.shouldDeferPendingBrowserCreateFromViewportSync())
369
370	wv.nativePopupFallbackStarted = true
371	require.False(t, wv.shouldDeferPendingBrowserCreateFromViewportSync())
372}
373
374func TestPreparePendingBrowserCreateObservedSizeRetry_CoalescesScheduling(t *testing.T) {
375	oldScheduleAfter := cefScheduleAfter
376	defer func() { cefScheduleAfter = oldScheduleAfter }()
377
378	scheduleCalls := 0
379	cefScheduleAfter = func(delay time.Duration, fn func()) {
380		require.Equal(t, pendingBrowserCreateObservedSizeRetryDelay, delay)
381		require.NotNil(t, fn)
382		scheduleCalls++
383	}
384
385	wv := &WebView{pendingCreate: &pendingBrowserCreate{}}
386
387	attempt, action := wv.preparePendingBrowserCreateObservedSizeRetry(context.Background(), "test")
388	require.Equal(t, 1, attempt)
389	require.Equal(t, pendingBrowserCreateObservedSizeRetryScheduled, action)
390	require.Equal(t, 1, scheduleCalls)
391
392	attempt, action = wv.preparePendingBrowserCreateObservedSizeRetry(context.Background(), "test")
393	require.Equal(t, 1, attempt)
394	require.Equal(t, pendingBrowserCreateObservedSizeRetryAlreadyScheduled, action)
395	require.Equal(t, 1, scheduleCalls)
396}
397
398func TestPreparePendingBrowserCreateObservedSizeRetry_MaxRetriesProceedsWithoutScheduling(t *testing.T) {
399	oldScheduleAfter := cefScheduleAfter
400	defer func() { cefScheduleAfter = oldScheduleAfter }()
401
402	scheduleCalls := 0
403	cefScheduleAfter = func(delay time.Duration, fn func()) {
404		require.Equal(t, pendingBrowserCreateObservedSizeRetryDelay, delay)
405		require.NotNil(t, fn)
406		scheduleCalls++
407	}
408
409	wv := &WebView{pendingCreate: &pendingBrowserCreate{observedSizeRetries: pendingBrowserCreateObservedSizeMaxRetries}}
410
411	attempt, action := wv.preparePendingBrowserCreateObservedSizeRetry(context.Background(), "test")
412	require.Equal(t, pendingBrowserCreateObservedSizeMaxRetries+1, attempt)
413	require.Equal(t, pendingBrowserCreateObservedSizeRetryProceedWithoutDelay, action)
414	require.Equal(t, 0, scheduleCalls)
415	require.False(t, wv.pendingCreate.observedSizeRetryScheduled)
416}
417
418func TestScheduleResizeRepaintPulse_CoalescesToLatestSequence(t *testing.T) {
419	oldNewTask := cefNewTask
420	oldPostDelayedTask := cefPostDelayedTask
421	defer func() {
422		cefNewTask = oldNewTask
423		cefPostDelayedTask = oldPostDelayedTask
424	}()
425
426	cefNewTask = func(task purecef.Task) purecef.Task { return task }
427
428	var scheduled []purecef.Task
429	var delays []int64
430	cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
431		require.Equal(t, purecef.ThreadIDTidUi, threadID)
432		require.NotNil(t, task)
433		scheduled = append(scheduled, task)
434		delays = append(delays, delayMs)
435		return 1
436	}
437
438	host := &viewportSyncOrderHost{}
439	wv := &WebView{ctx: context.Background(), host: host}
440
441	wv.scheduleResizeRepaintPulse(context.Background(), "first")
442	wv.scheduleResizeRepaintPulse(context.Background(), "second")
443
444	require.Equal(t, []int64{16, 48, 16, 48}, delays)
445	require.Len(t, scheduled, 4)
446
447	scheduled[0].Execute()
448	scheduled[1].Execute()
449	require.Empty(t, host.calls)
450
451	scheduled[2].Execute()
452	scheduled[3].Execute()
453	require.Equal(t, []string{"Invalidate", "Invalidate"}, host.calls)
454}

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