webview_zoom_test.go RawHistoryFolder

1package cef
2
3import (
4	"context"
5	"fmt"
6	"math"
7	"testing"
8
9	purecef "github.com/bnema/purego-cef/cef"
10	"github.com/stretchr/testify/assert"
11	"github.com/stretchr/testify/require"
12)
13
14func TestZoomConversionsRoundTrip(t *testing.T) {
15	factors := []float64{0.5, 0.8, 1.0, 1.1, 1.2, 1.4, 2.0}
16	for _, factor := range factors {
17		t.Run(fmt.Sprintf("factor_%.1f", factor), func(t *testing.T) {
18			level := cefZoomFromFactor(factor)
19			got := factorFromCEFZoom(level)
20			assert.InDelta(t, factor, got, 1e-9, "round-trip mismatch for factor %.6f: level %.6f -> factor %.12f", factor, level, got)
21		})
22	}
23}
24
25func TestZoomConversionsApplyUserZoomAndCompensation(t *testing.T) {
26	tests := []struct {
27		name         string
28		userZoom     float64
29		compensation float64
30		wantInternal float64
31	}{
32		{
33			name:         "unit_output_needs_no_compensation",
34			userZoom:     1.0,
35			compensation: 1.0,
36			wantInternal: 1.0,
37		},
38		{
39			name:         "user_zoom_on_unit_output",
40			userZoom:     1.3,
41			compensation: 1.0,
42			wantInternal: 1.3,
43		},
44		{
45			name:         "fractional_output_compensates_100_percent",
46			userZoom:     1.0,
47			compensation: 1.25,
48			wantInternal: 1.25,
49		},
50		{
51			name:         "user_zoom_times_fractional_output",
52			userZoom:     1.3,
53			compensation: 1.25,
54			wantInternal: 1.625,
55		},
56		{
57			name:         "application_scale_composed_compensation",
58			userZoom:     1.0,
59			compensation: 1.75,
60			wantInternal: 1.75,
61		},
62		{
63			name:         "integer_output_doubles_internal_factor",
64			userZoom:     2.0,
65			compensation: 2.0,
66			wantInternal: 4.0,
67		},
68		{
69			name:         "half_user_zoom_stays_compensated",
70			userZoom:     0.5,
71			compensation: 1.25,
72			wantInternal: 0.625,
73		},
74	}
75
76	for _, tt := range tests {
77		t.Run(tt.name, func(t *testing.T) {
78			level := cefZoomFromPageZoom(tt.userZoom, tt.compensation)
79			assert.InDelta(t, tt.wantInternal, factorFromCEFZoom(level), 1e-9, "internal CEF factor")
80			assert.InDelta(t, tt.userZoom, pageZoomFromCEFLevel(level, tt.compensation), 1e-9, "inverse must return user zoom")
81		})
82	}
83}
84
85func TestZoomConversionsNormalizeInvalidCompensation(t *testing.T) {
86	for _, compensation := range []float64{0, -1.5, math.NaN(), math.Inf(1)} {
87		level := cefZoomFromPageZoom(1.3, compensation)
88		assert.InDelta(t, 1.3, factorFromCEFZoom(level), 1e-9, "invalid compensation %v must behave as 1", compensation)
89		assert.InDelta(t, 1.3, pageZoomFromCEFLevel(level, compensation), 1e-9)
90	}
91}
92
93func TestZoomReapplyGuardTracksAppliedCompensation(t *testing.T) {
94	wv := &WebView{}
95
96	assert.True(t, wv.shouldReapplyZoomForCompensation(1.25), "unknown applied compensation must reapply")
97	assert.Zero(t, wv.appliedZoomCompensation())
98
99	wv.recordAppliedZoomCompensation(1.25)
100
101	assert.False(t, wv.shouldReapplyZoomForCompensation(1.25), "unchanged compensation must not reapply")
102	assert.True(t, wv.shouldReapplyZoomForCompensation(2.0), "compensation change 1.25 -> 2 must reapply")
103	assert.InDelta(t, 1.25, wv.appliedZoomCompensation(), 1e-9)
104}
105
106func TestPageZoomCompensationDefaultsToOneWithoutBridge(t *testing.T) {
107	wv := &WebView{}
108	assert.InDelta(t, 1.0, wv.pageZoomCompensation(), 1e-9)
109
110	wv.zoomCompensation = zoomCompensationStub{compensation: 1.75}
111	assert.InDelta(t, 1.75, wv.pageZoomCompensation(), 1e-9)
112
113	wv.zoomCompensation = zoomCompensationStub{compensation: math.NaN()}
114	assert.InDelta(t, 1.0, wv.pageZoomCompensation(), 1e-9)
115
116	var typedNil *Cef2gtkAdapter
117	wv.zoomCompensation = typedNil
118	assert.InDelta(t, 1.0, wv.pageZoomCompensation(), 1e-9)
119}
120
121// withSynchronousCEFTasks replaces delayed task posting with a recorder so tests
122// can inspect and run zoom tasks without a CEF message loop.
123func withSynchronousCEFTasks(t *testing.T) *[]purecef.Task {
124	t.Helper()
125	oldNewTask := cefNewTask
126	oldPostDelayedTask := cefPostDelayedTask
127	t.Cleanup(func() {
128		cefNewTask = oldNewTask
129		cefPostDelayedTask = oldPostDelayedTask
130	})
131
132	cefNewTask = func(task purecef.Task) purecef.Task { return task }
133	scheduled := &[]purecef.Task{}
134	cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, _ int64) int32 {
135		require.Equal(t, purecef.ThreadIDTidUi, threadID)
136		require.NotNil(t, task)
137		*scheduled = append(*scheduled, task)
138		return 1
139	}
140	return scheduled
141}
142
143func TestSetZoomLevelAppliesUserZoomThroughCompensation(t *testing.T) {
144	scheduled := withSynchronousCEFTasks(t)
145
146	host := &viewportSyncOrderHost{}
147	wv := &WebView{
148		ctx:              context.Background(),
149		host:             host,
150		zoomCompensation: zoomCompensationStub{compensation: 1.25},
151	}
152
153	require.NoError(t, wv.SetZoomLevel(context.Background(), 1.3))
154
155	assert.Equal(t, []string{"SetZoomLevel", "NotifyScreenInfoChanged"}, host.calls)
156	assert.InDelta(t, cefZoomFromFactor(1.625), host.zoomLevel, 1e-9, "internal CEF level must include user zoom x compensation")
157	assert.InDelta(t, 1.3, wv.GetZoomLevel(), 1e-9, "user zoom must stay user-facing")
158	assert.InDelta(t, 1.25, wv.appliedZoomCompensation(), 1e-9)
159	assert.False(t, wv.shouldReapplyZoomForCompensation(1.25))
160	assert.NotEmpty(t, *scheduled)
161
162	// The diagnostic readback decodes the same CEF level back to user zoom with
163	// the compensation of its own application.
164	for _, task := range *scheduled {
165		task.Execute()
166	}
167	assert.InDelta(t, 1.3, pageZoomFromCEFLevel(host.zoomLevel, 1.25), 1e-9)
168}
169
170func TestSetZoomLevelRejectsDestroyedViewAndMissingHost(t *testing.T) {
171	destroyed := &WebView{ctx: context.Background()}
172	destroyed.destroyed.Store(true)
173	require.ErrorIs(t, destroyed.SetZoomLevel(context.Background(), 1.2), errDestroyed)
174
175	require.ErrorIs(t, (&WebView{ctx: context.Background()}).SetZoomLevel(context.Background(), 1.2), errNoBrowser)
176}
177
178func TestScheduleZoomReadbackSkipsSupersededApplication(t *testing.T) {
179	scheduled := withSynchronousCEFTasks(t)
180	host := &viewportSyncOrderHost{}
181	wv := &WebView{ctx: context.Background(), host: host}
182
183	wv.recordAppliedZoomCompensation(1.25)
184	firstEpoch := wv.zoomApplicationEpoch.Add(1)
185	wv.scheduleZoomReadback(1.3, cefZoomFromPageZoom(1.3, 1.25), 1.25, firstEpoch)
186	require.Len(t, *scheduled, 2)
187	stale := (*scheduled)[0]
188
189	// A newer application supersedes the pending readback: decoding the newer CEF
190	// level with the older compensation would report a wrong user zoom.
191	wv.recordAppliedZoomCompensation(1.25)
192	wv.zoomApplicationEpoch.Add(1)
193	stale.Execute()
194	assert.Zero(t, host.getZoomLevelCalls, "superseded application must not be diagnosed")
195
196	wv.recordAppliedZoomCompensation(2.0)
197	currentEpoch := wv.zoomApplicationEpoch.Add(1)
198	wv.scheduleZoomReadback(1.3, cefZoomFromPageZoom(1.3, 2.0), 2.0, currentEpoch)
199	require.Len(t, *scheduled, 4)
200	(*scheduled)[2].Execute()
201	assert.Equal(t, 1, host.getZoomLevelCalls, "current application must be diagnosed")
202}

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