v0.31.0 bf0756b

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		"NotifyScreenInfoChanged",
153		"WasResized",
154		"Invalidate",
155	}, host.calls)
156}
157
158func TestSyncZoomForBackingScaleOnCEFUIThread_PostsZoomWork(t *testing.T) {
159	oldNewTask := cefNewTask
160	oldPostDelayedTask := cefPostDelayedTask
161	defer func() {
162		cefNewTask = oldNewTask
163		cefPostDelayedTask = oldPostDelayedTask
164	}()
165
166	cefNewTask = func(task purecef.Task) purecef.Task { return task }
167
168	var scheduled purecef.Task
169	cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
170		require.Equal(t, purecef.ThreadIDTidUi, threadID)
171		require.NotNil(t, task)
172		if delayMs == 0 && scheduled == nil {
173			scheduled = task
174		}
175		return 1
176	}
177
178	host := &viewportSyncOrderHost{}
179	wv := &WebView{ctx: context.Background(), host: host}
180	wv.syncZoomForBackingScaleOnCEFUIThread(host, "gtk-size-observer")
181
182	require.Empty(t, host.calls)
183	require.NotNil(t, scheduled)
184
185	scheduled.Execute()
186	require.Equal(t, []string{
187		"SetZoomLevel",
188		"NotifyScreenInfoChanged",
189	}, host.calls)
190}
191
192func TestSyncZoomForBackingScaleOnCEFUIThread_SkipsRedundantReapplyWhenBackingScaleUnchanged(t *testing.T) {
193	oldNewTask := cefNewTask
194	oldPostDelayedTask := cefPostDelayedTask
195	defer func() {
196		cefNewTask = oldNewTask
197		cefPostDelayedTask = oldPostDelayedTask
198	}()
199
200	cefNewTask = func(task purecef.Task) purecef.Task { return task }
201
202	var scheduled purecef.Task
203	cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
204		if delayMs == 0 && scheduled == nil {
205			scheduled = task
206		}
207		return 1
208	}
209
210	host := &viewportSyncOrderHost{}
211	wv := &WebView{ctx: context.Background(), host: host}
212
213	wv.syncZoomForBackingScaleOnCEFUIThread(host, "gtk-size-observer")
214	require.NotNil(t, scheduled)
215	scheduled.Execute()
216
217	host.calls = nil
218	scheduled = nil
219
220	wv.syncZoomForBackingScaleOnCEFUIThread(host, "gtk-size-observer")
221	require.Nil(t, scheduled)
222	require.Empty(t, host.calls)
223}
224
225func TestNotifyViewportSyncOnCEFUIThread_SkipsStaleHost(t *testing.T) {
226	oldNewTask := cefNewTask
227	oldPostDelayedTask := cefPostDelayedTask
228	defer func() {
229		cefNewTask = oldNewTask
230		cefPostDelayedTask = oldPostDelayedTask
231	}()
232
233	cefNewTask = func(task purecef.Task) purecef.Task { return task }
234
235	var scheduled purecef.Task
236	cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, _ int64) int32 {
237		scheduled = task
238		return 1
239	}
240
241	capturedHost := &viewportSyncOrderHost{}
242	currentHost := &viewportSyncOrderHost{}
243	wv := &WebView{ctx: context.Background(), host: currentHost}
244	wv.notifyViewportSyncOnCEFUIThread(capturedHost, true)
245
246	require.NotNil(t, scheduled)
247	scheduled.Execute()
248	require.Empty(t, capturedHost.calls)
249	require.Empty(t, currentHost.calls)
250}
251
252func TestNotifyViewportSyncOnCEFUIThread_SkipsDestroyedWebView(t *testing.T) {
253	oldNewTask := cefNewTask
254	oldPostDelayedTask := cefPostDelayedTask
255	defer func() {
256		cefNewTask = oldNewTask
257		cefPostDelayedTask = oldPostDelayedTask
258	}()
259
260	cefNewTask = func(task purecef.Task) purecef.Task { return task }
261
262	var scheduled purecef.Task
263	cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, _ int64) int32 {
264		scheduled = task
265		return 1
266	}
267
268	host := &viewportSyncOrderHost{}
269	wv := &WebView{ctx: context.Background(), host: host}
270	wv.destroyed.Store(true)
271	wv.notifyViewportSyncOnCEFUIThread(host, true)
272
273	require.NotNil(t, scheduled)
274	scheduled.Execute()
275	require.Empty(t, host.calls)
276}
277
278func TestObservedViewportSizeReady(t *testing.T) {
279	tests := []struct {
280		name            string
281		observedWidth   int32
282		observedHeight  int32
283		allocatedWidth  int32
284		allocatedHeight int32
285		want            bool
286	}{
287		{
288			name:            "exact match",
289			observedWidth:   793,
290			observedHeight:  1753,
291			allocatedWidth:  793,
292			allocatedHeight: 1753,
293			want:            true,
294		},
295		{
296			name:            "one pixel drift is accepted",
297			observedWidth:   792,
298			observedHeight:  1754,
299			allocatedWidth:  793,
300			allocatedHeight: 1753,
301			want:            true,
302		},
303		{
304			name:            "fallback observed size is rejected",
305			observedWidth:   1,
306			observedHeight:  1,
307			allocatedWidth:  793,
308			allocatedHeight: 1753,
309			want:            false,
310		},
311		{
312			name:            "missing allocation is rejected",
313			observedWidth:   793,
314			observedHeight:  1753,
315			allocatedWidth:  0,
316			allocatedHeight: 1753,
317			want:            false,
318		},
319		{
320			name:            "large mismatch is rejected",
321			observedWidth:   1200,
322			observedHeight:  1400,
323			allocatedWidth:  1587,
324			allocatedHeight: 1679,
325			want:            false,
326		},
327	}
328
329	for _, tt := range tests {
330		t.Run(tt.name, func(t *testing.T) {
331			require.Equal(t, tt.want, observedViewportSizeReady(tt.observedWidth, tt.observedHeight, tt.allocatedWidth, tt.allocatedHeight))
332		})
333	}
334}
335
336func TestPendingBrowserCreateObservedSize_RefreshesStaleFallbackSize(t *testing.T) {
337	bridge := &pendingBrowserCreateObservedSizeBridgeStub{
338		sizeWidth:       1,
339		sizeHeight:      1,
340		refreshWidth:    1055,
341		refreshHeight:   1719,
342		refreshProvided: true,
343	}
344
345	observedWidth, observedHeight, ready := pendingBrowserCreateObservedSize(bridge, 1055, 1719)
346
347	require.True(t, ready)
348	require.Equal(t, int32(1055), observedWidth)
349	require.Equal(t, int32(1719), observedHeight)
350	require.Equal(t, 1, bridge.refreshCalls)
351}
352
353func TestPendingBrowserCreateObservedSize_SkipsRefreshWhenObservedSizeAlreadyMatches(t *testing.T) {
354	bridge := &pendingBrowserCreateObservedSizeBridgeStub{sizeWidth: 1055, sizeHeight: 1719}
355
356	observedWidth, observedHeight, ready := pendingBrowserCreateObservedSize(bridge, 1055, 1719)
357
358	require.True(t, ready)
359	require.Equal(t, int32(1055), observedWidth)
360	require.Equal(t, int32(1719), observedHeight)
361	require.Equal(t, 0, bridge.refreshCalls)
362}
363
364func TestShouldDeferPendingBrowserCreateFromViewportSync_WaitsForNativePopupAttach(t *testing.T) {
365	wv := &WebView{pendingCreate: &pendingBrowserCreate{}, nativePopupCandidate: true}
366
367	require.True(t, wv.shouldDeferPendingBrowserCreateFromViewportSync())
368
369	wv.nativePopupFallbackStarted = true
370	require.False(t, wv.shouldDeferPendingBrowserCreateFromViewportSync())
371}
372
373func TestPreparePendingBrowserCreateObservedSizeRetry_CoalescesScheduling(t *testing.T) {
374	oldScheduleAfter := cefScheduleAfter
375	defer func() { cefScheduleAfter = oldScheduleAfter }()
376
377	scheduleCalls := 0
378	cefScheduleAfter = func(delay time.Duration, fn func()) {
379		require.Equal(t, pendingBrowserCreateObservedSizeRetryDelay, delay)
380		require.NotNil(t, fn)
381		scheduleCalls++
382	}
383
384	wv := &WebView{pendingCreate: &pendingBrowserCreate{}}
385
386	attempt, action := wv.preparePendingBrowserCreateObservedSizeRetry(context.Background(), "test")
387	require.Equal(t, 1, attempt)
388	require.Equal(t, pendingBrowserCreateObservedSizeRetryScheduled, action)
389	require.Equal(t, 1, scheduleCalls)
390
391	attempt, action = wv.preparePendingBrowserCreateObservedSizeRetry(context.Background(), "test")
392	require.Equal(t, 1, attempt)
393	require.Equal(t, pendingBrowserCreateObservedSizeRetryAlreadyScheduled, action)
394	require.Equal(t, 1, scheduleCalls)
395}
396
397func TestPreparePendingBrowserCreateObservedSizeRetry_MaxRetriesProceedsWithoutScheduling(t *testing.T) {
398	oldScheduleAfter := cefScheduleAfter
399	defer func() { cefScheduleAfter = oldScheduleAfter }()
400
401	scheduleCalls := 0
402	cefScheduleAfter = func(delay time.Duration, fn func()) {
403		require.Equal(t, pendingBrowserCreateObservedSizeRetryDelay, delay)
404		require.NotNil(t, fn)
405		scheduleCalls++
406	}
407
408	wv := &WebView{pendingCreate: &pendingBrowserCreate{observedSizeRetries: pendingBrowserCreateObservedSizeMaxRetries}}
409
410	attempt, action := wv.preparePendingBrowserCreateObservedSizeRetry(context.Background(), "test")
411	require.Equal(t, pendingBrowserCreateObservedSizeMaxRetries+1, attempt)
412	require.Equal(t, pendingBrowserCreateObservedSizeRetryProceedWithoutDelay, action)
413	require.Equal(t, 0, scheduleCalls)
414	require.False(t, wv.pendingCreate.observedSizeRetryScheduled)
415}
416
417func TestScheduleResizeRepaintPulse_CoalescesToLatestSequence(t *testing.T) {
418	oldNewTask := cefNewTask
419	oldPostDelayedTask := cefPostDelayedTask
420	defer func() {
421		cefNewTask = oldNewTask
422		cefPostDelayedTask = oldPostDelayedTask
423	}()
424
425	cefNewTask = func(task purecef.Task) purecef.Task { return task }
426
427	var scheduled []purecef.Task
428	var delays []int64
429	cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
430		require.Equal(t, purecef.ThreadIDTidUi, threadID)
431		require.NotNil(t, task)
432		scheduled = append(scheduled, task)
433		delays = append(delays, delayMs)
434		return 1
435	}
436
437	host := &viewportSyncOrderHost{}
438	wv := &WebView{ctx: context.Background(), host: host}
439
440	wv.scheduleResizeRepaintPulse(context.Background(), "first")
441	wv.scheduleResizeRepaintPulse(context.Background(), "second")
442
443	require.Equal(t, []int64{16, 48, 16, 48}, delays)
444	require.Len(t, scheduled, 4)
445
446	scheduled[0].Execute()
447	scheduled[1].Execute()
448	require.Empty(t, host.calls)
449
450	scheduled[2].Execute()
451	scheduled[3].Execute()
452	require.Equal(t, []string{
453		"NotifyScreenInfoChanged",
454		"WasResized",
455		"Invalidate",
456		"NotifyScreenInfoChanged",
457		"WasResized",
458		"Invalidate",
459	}, host.calls)
460}
461
462func TestScheduleResizeRepaintPulse_SkipsStaleHost(t *testing.T) {
463	oldNewTask := cefNewTask
464	oldPostDelayedTask := cefPostDelayedTask
465	defer func() {
466		cefNewTask = oldNewTask
467		cefPostDelayedTask = oldPostDelayedTask
468	}()
469
470	cefNewTask = func(task purecef.Task) purecef.Task { return task }
471
472	var scheduled []purecef.Task
473	cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, _ int64) int32 {
474		require.Equal(t, purecef.ThreadIDTidUi, threadID)
475		require.NotNil(t, task)
476		scheduled = append(scheduled, task)
477		return 1
478	}
479
480	capturedHost := &viewportSyncOrderHost{}
481	currentHost := &viewportSyncOrderHost{}
482	wv := &WebView{ctx: context.Background(), host: capturedHost}
483
484	wv.scheduleResizeRepaintPulse(context.Background(), "stale-host")
485	wv.host = currentHost
486
487	require.Len(t, scheduled, 2)
488	scheduled[0].Execute()
489	scheduled[1].Execute()
490
491	require.Empty(t, capturedHost.calls)
492	require.Empty(t, currentHost.calls)
493}
494
495func TestScheduleResizeRepaintPulse_SkipsWhenHostNilAtScheduleTime(t *testing.T) {
496	oldNewTask := cefNewTask
497	oldPostDelayedTask := cefPostDelayedTask
498	defer func() {
499		cefNewTask = oldNewTask
500		cefPostDelayedTask = oldPostDelayedTask
501	}()
502
503	cefNewTask = func(task purecef.Task) purecef.Task { return task }
504
505	var scheduled []purecef.Task
506	cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, _ int64) int32 {
507		scheduled = append(scheduled, task)
508		return 1
509	}
510
511	wv := &WebView{ctx: context.Background()}
512
513	wv.scheduleResizeRepaintPulse(context.Background(), "nil-host")
514
515	require.Empty(t, scheduled)
516}
517
518func TestScheduleResizeRepaintPulse_SkipsWhenHostClearedBeforeExecution(t *testing.T) {
519	oldNewTask := cefNewTask
520	oldPostDelayedTask := cefPostDelayedTask
521	defer func() {
522		cefNewTask = oldNewTask
523		cefPostDelayedTask = oldPostDelayedTask
524	}()
525
526	cefNewTask = func(task purecef.Task) purecef.Task { return task }
527
528	var scheduled []purecef.Task
529	cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, _ int64) int32 {
530		require.Equal(t, purecef.ThreadIDTidUi, threadID)
531		require.NotNil(t, task)
532		scheduled = append(scheduled, task)
533		return 1
534	}
535
536	host := &viewportSyncOrderHost{}
537	wv := &WebView{ctx: context.Background(), host: host}
538
539	wv.scheduleResizeRepaintPulse(context.Background(), "nil-host-before-execute")
540	wv.host = nil
541
542	require.Len(t, scheduled, 2)
543	scheduled[0].Execute()
544	scheduled[1].Execute()
545
546	require.Empty(t, host.calls)
547}

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