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}