FazBrowse GitHub Viewer
|
Trending
|
URL:
|
Home
Tools:
[Download Repo ZIP]
[View Raw Code]
[Original HTTPS Page]
pythonnative/tests/test_gestures.py at v0.41.0 · pythonnative/pythonnative · GitHub
pythonnative
pythonnative
Repository navigation
Code
Issues
11
(11)
Pull requests
4
(4)
Actions
Projects
Security and quality
Insights
Expand file tree
Breadcrumbs
pythonnative
/
tests
/
test_gestures.py
Copy path
More file actions
More file actions
Latest commit
History
History
History
362 lines (273 loc) · 12.6 KB
Breadcrumbs
pythonnative
/
tests
/
test_gestures.py
Copy path
File metadata and controls
362 lines (273 loc) · 12.6 KB
Raw
Copy raw file
Download raw file
Open symbols panel
Edit and raw actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
"""Tests for the gesture system: descriptors, serialization, and the
pure-Python `GestureArbiter` used by the browser preview (and as the reference semantics).
The arbiter tests drive scripted pointer streams (positions in points,
times in seconds) and assert on the emitted payloads, which is exactly
the contract the platform handlers rely on.
"""
from
__future__
import
annotations
import
json
import
math
from
typing
import
Any
,
Dict
,
List
,
Tuple
import
pytest
from
pythonnative
.
gestures
import
(
GestureArbiter
,
GestureEvent
,
GestureState
,
Pan
,
Tap
,
serialize_gestures
,
)
Emitted
=
Tuple
[
int
,
Dict
[
str
,
Any
]]
def
_arbiter
(
*
specs
:
Dict
[
str
,
Any
])
->
Tuple
[
GestureArbiter
,
List
[
Emitted
]]:
emitted
:
List
[
Emitted
]
=
[]
arbiter
=
GestureArbiter
(
list
(
specs
),
lambda
i
,
payload
:
emitted
.
append
((
i
,
payload
)))
return
arbiter
,
emitted
def
_states
(
emitted
:
List
[
Emitted
])
->
List
[
str
]:
return
[
payload
[
"state"
]
for
_i
,
payload
in
emitted
]
# ======================================================================
# Descriptors and serialization
# ======================================================================
def
test_serialize_gestures_emits_config_and_routers
()
->
None
:
pans
:
List
[
GestureEvent
]
=
[]
specs
,
events
=
serialize_gestures
(
[
Tap
(
on_tap
=
lambda
e
:
None
),
Pan
(
on_change
=
pans
.
append
,
min_distance
=
4.0
),
]
)
assert
specs
==
[
{
"kind"
:
"tap"
,
"n_taps"
:
1
,
"max_distance"
:
12.0
,
"simultaneous"
: [
1
],
"wait_for"
: []},
{
"kind"
:
"pan"
,
"min_distance"
:
4.0
,
"min_pointers"
:
1
,
"simultaneous"
: [
0
],
"wait_for"
: []},
]
assert
set
(
events
)
==
{
"gesture:0"
,
"gesture:1"
}
events
[
"gesture:1"
]({
"kind"
:
"pan"
,
"state"
:
"changed"
,
"translation_x"
:
9.0
})
assert
len
(
pans
)
==
1
and
pans
[
0
].
translation_x
==
9.0
def
test_serialize_passes_plain_dicts_through
()
->
None
:
specs
,
events
=
serialize_gestures
([{
"kind"
:
"tap"
,
"n_taps"
:
3
}])
assert
specs
==
[{
"kind"
:
"tap"
,
"n_taps"
:
3
,
"simultaneous"
: [],
"wait_for"
: []}]
assert
events
==
{}
def
test_router_drops_unknown_payload_keys
()
->
None
:
seen
:
List
[
GestureEvent
]
=
[]
_specs
,
events
=
serialize_gestures
([
Tap
(
on_tap
=
seen
.
append
)])
events
[
"gesture:0"
]({
"kind"
:
"tap"
,
"state"
:
"ended"
,
"x"
:
1.0
,
"_debug"
:
"extra"
})
assert
seen
[
0
].
x
==
1.0
def
test_gesture_state_is_a_str_enum_coerced_from_wire_strings
()
->
None
:
event
=
GestureEvent
(
kind
=
"tap"
,
state
=
"ended"
)
# type: ignore[arg-type]
assert
event
.
state
is
GestureState
.
ENDED
assert
event
.
state
==
"ended"
assert
str
(
GestureState
.
BEGAN
)
==
"began"
assert
json
.
dumps
({
"state"
:
GestureState
.
CANCELLED
})
==
'{"state": "cancelled"}'
with
pytest
.
raises
(
ValueError
):
GestureEvent
(
kind
=
"tap"
,
state
=
"exploded"
)
# type: ignore[arg-type]
def
test_arbiter_emits_plain_string_states
()
->
None
:
arbiter
,
emitted
=
_arbiter
({
"kind"
:
"tap"
})
arbiter
.
pointer_down
(
0
,
10.0
,
10.0
,
0.0
)
arbiter
.
pointer_up
(
0
,
10.0
,
10.0
,
0.05
)
assert
emitted
and
all
(
type
(
p
[
"state"
])
is
str
for
_i
,
p
in
emitted
)
def
test_descriptor_callback_routing_by_state
()
->
None
:
log
:
List
[
str
]
=
[]
pan
=
Pan
(
on_begin
=
lambda
e
:
log
.
append
(
"begin"
),
on_change
=
lambda
e
:
log
.
append
(
"change"
),
on_end
=
lambda
e
:
log
.
append
(
"end"
),
)
_specs
,
events
=
serialize_gestures
([
pan
])
router
=
events
[
"gesture:0"
]
router
({
"kind"
:
"pan"
,
"state"
:
"began"
})
router
({
"kind"
:
"pan"
,
"state"
:
"changed"
})
router
({
"kind"
:
"pan"
,
"state"
:
"ended"
})
router
({
"kind"
:
"pan"
,
"state"
:
"cancelled"
})
assert
log
==
[
"begin"
,
"change"
,
"end"
,
"end"
]
# ======================================================================
# Tap recognition
# ======================================================================
def
test_tap_recognized_on_quick_release
()
->
None
:
arbiter
,
emitted
=
_arbiter
({
"kind"
:
"tap"
})
arbiter
.
pointer_down
(
1
,
10.0
,
20.0
,
t
=
0.0
)
arbiter
.
pointer_up
(
1
,
10.0
,
20.0
,
t
=
0.1
)
assert
len
(
emitted
)
==
1
index
,
payload
=
emitted
[
0
]
assert
index
==
0
assert
payload
[
"state"
]
==
"ended"
assert
payload
[
"x"
]
==
10.0
and
payload
[
"y"
]
==
20.0
def
test_tap_fails_when_pointer_travels_beyond_slop
()
->
None
:
arbiter
,
emitted
=
_arbiter
({
"kind"
:
"tap"
,
"max_distance"
:
12.0
})
arbiter
.
pointer_down
(
1
,
0.0
,
0.0
,
t
=
0.0
)
arbiter
.
pointer_move
(
1
,
30.0
,
0.0
,
t
=
0.05
)
arbiter
.
pointer_up
(
1
,
30.0
,
0.0
,
t
=
0.1
)
assert
emitted
==
[]
def
test_tap_fails_when_held_too_long
()
->
None
:
arbiter
,
emitted
=
_arbiter
({
"kind"
:
"tap"
})
arbiter
.
pointer_down
(
1
,
0.0
,
0.0
,
t
=
0.0
)
arbiter
.
pointer_up
(
1
,
0.0
,
0.0
,
t
=
1.0
)
assert
emitted
==
[]
def
test_double_tap_requires_two_quick_taps
()
->
None
:
arbiter
,
emitted
=
_arbiter
({
"kind"
:
"tap"
,
"n_taps"
:
2
})
arbiter
.
pointer_down
(
1
,
0.0
,
0.0
,
t
=
0.0
)
arbiter
.
pointer_up
(
1
,
0.0
,
0.0
,
t
=
0.05
)
assert
emitted
==
[],
"first tap alone must not fire a double-tap"
arbiter
.
pointer_down
(
1
,
0.0
,
0.0
,
t
=
0.15
)
arbiter
.
pointer_up
(
1
,
0.0
,
0.0
,
t
=
0.2
)
assert
_states
(
emitted
)
==
[
"ended"
]
def
test_double_tap_resets_after_gap
()
->
None
:
arbiter
,
emitted
=
_arbiter
({
"kind"
:
"tap"
,
"n_taps"
:
2
})
arbiter
.
pointer_down
(
1
,
0.0
,
0.0
,
t
=
0.0
)
arbiter
.
pointer_up
(
1
,
0.0
,
0.0
,
t
=
0.05
)
# Second tap arrives way past the multi-tap window.
arbiter
.
pointer_down
(
1
,
0.0
,
0.0
,
t
=
2.0
)
arbiter
.
pointer_up
(
1
,
0.0
,
0.0
,
t
=
2.05
)
assert
emitted
==
[]
# ======================================================================
# Long press
# ======================================================================
def
test_long_press_activates_after_deadline_poll
()
->
None
:
arbiter
,
emitted
=
_arbiter
({
"kind"
:
"long_press"
,
"min_duration_ms"
:
100.0
})
arbiter
.
pointer_down
(
1
,
5.0
,
5.0
,
t
=
0.0
)
assert
arbiter
.
next_deadline
()
==
0.1
arbiter
.
poll
(
0.05
)
assert
emitted
==
[]
arbiter
.
poll
(
0.12
)
assert
_states
(
emitted
)
==
[
"began"
]
assert
arbiter
.
next_deadline
()
is
None
arbiter
.
pointer_up
(
1
,
5.0
,
5.0
,
t
=
0.3
)
assert
_states
(
emitted
)
==
[
"began"
,
"ended"
]
def
test_long_press_cancelled_by_drift_after_activation
()
->
None
:
arbiter
,
emitted
=
_arbiter
({
"kind"
:
"long_press"
,
"min_duration_ms"
:
100.0
})
arbiter
.
pointer_down
(
1
,
0.0
,
0.0
,
t
=
0.0
)
arbiter
.
poll
(
0.15
)
arbiter
.
pointer_move
(
1
,
50.0
,
0.0
,
t
=
0.2
)
assert
_states
(
emitted
)
==
[
"began"
,
"cancelled"
]
def
test_long_press_never_fires_on_quick_release
()
->
None
:
arbiter
,
emitted
=
_arbiter
({
"kind"
:
"long_press"
,
"min_duration_ms"
:
100.0
})
arbiter
.
pointer_down
(
1
,
0.0
,
0.0
,
t
=
0.0
)
arbiter
.
pointer_up
(
1
,
0.0
,
0.0
,
t
=
0.05
)
arbiter
.
poll
(
0.2
)
assert
emitted
==
[]
# ======================================================================
# Pan
# ======================================================================
def
test_pan_activates_after_min_distance_and_tracks_translation
()
->
None
:
arbiter
,
emitted
=
_arbiter
({
"kind"
:
"pan"
,
"min_distance"
:
10.0
})
arbiter
.
pointer_down
(
1
,
0.0
,
0.0
,
t
=
0.0
)
arbiter
.
pointer_move
(
1
,
5.0
,
0.0
,
t
=
0.01
)
assert
emitted
==
[],
"below min_distance the pan must stay silent"
assert
arbiter
.
has_active_pan
()
is
False
arbiter
.
pointer_move
(
1
,
20.0
,
0.0
,
t
=
0.02
)
assert
_states
(
emitted
)
==
[
"began"
]
assert
arbiter
.
has_active_pan
()
is
True
arbiter
.
pointer_move
(
1
,
32.0
,
7.0
,
t
=
0.03
)
state
=
emitted
[
-
1
][
1
]
assert
state
[
"state"
]
==
"changed"
# Translation is measured from the activation anchor (20, 0).
assert
state
[
"translation_x"
]
==
12.0
assert
state
[
"translation_y"
]
==
7.0
assert
state
[
"velocity_x"
]
>
0.0
arbiter
.
pointer_up
(
1
,
40.0
,
7.0
,
t
=
0.04
)
end
=
emitted
[
-
1
][
1
]
assert
end
[
"state"
]
==
"ended"
assert
end
[
"translation_x"
]
==
20.0
assert
end
[
"velocity_x"
]
>
0.0
assert
arbiter
.
has_active_pan
()
is
False
def
test_pan_cancel_emits_cancelled_when_active
()
->
None
:
arbiter
,
emitted
=
_arbiter
({
"kind"
:
"pan"
,
"min_distance"
:
5.0
})
arbiter
.
pointer_down
(
1
,
0.0
,
0.0
,
t
=
0.0
)
arbiter
.
pointer_move
(
1
,
20.0
,
0.0
,
t
=
0.01
)
arbiter
.
cancel
(
t
=
0.02
)
assert
_states
(
emitted
)
==
[
"began"
,
"cancelled"
]
assert
arbiter
.
has_active_pan
()
is
False
# ======================================================================
# Swipe
# ======================================================================
def
test_swipe_resolves_direction_from_release_velocity
()
->
None
:
arbiter
,
emitted
=
_arbiter
({
"kind"
:
"swipe"
,
"min_velocity"
:
300.0
})
arbiter
.
pointer_down
(
1
,
0.0
,
0.0
,
t
=
0.0
)
arbiter
.
pointer_move
(
1
,
60.0
,
0.0
,
t
=
0.05
)
arbiter
.
pointer_up
(
1
,
120.0
,
0.0
,
t
=
0.1
)
assert
len
(
emitted
)
==
1
payload
=
emitted
[
0
][
1
]
assert
payload
[
"state"
]
==
"ended"
assert
payload
[
"direction"
]
==
"right"
assert
payload
[
"velocity_x"
]
>
300.0
def
test_swipe_below_min_velocity_is_ignored
()
->
None
:
arbiter
,
emitted
=
_arbiter
({
"kind"
:
"swipe"
,
"min_velocity"
:
300.0
})
arbiter
.
pointer_down
(
1
,
0.0
,
0.0
,
t
=
0.0
)
arbiter
.
pointer_move
(
1
,
5.0
,
0.0
,
t
=
0.5
)
arbiter
.
pointer_up
(
1
,
10.0
,
0.0
,
t
=
1.0
)
assert
emitted
==
[]
def
test_swipe_direction_filter_rejects_other_axes
()
->
None
:
arbiter
,
emitted
=
_arbiter
({
"kind"
:
"swipe"
,
"direction"
:
"up"
,
"min_velocity"
:
100.0
})
arbiter
.
pointer_down
(
1
,
0.0
,
0.0
,
t
=
0.0
)
arbiter
.
pointer_up
(
1
,
200.0
,
0.0
,
t
=
0.1
)
# fast *rightward* flick
assert
emitted
==
[]
arbiter
.
pointer_down
(
1
,
0.0
,
200.0
,
t
=
1.0
)
arbiter
.
pointer_up
(
1
,
0.0
,
0.0
,
t
=
1.1
)
# fast upward flick
assert
[
p
[
"direction"
]
for
_i
,
p
in
emitted
]
==
[
"up"
]
# ======================================================================
# Pinch and rotation
# ======================================================================
def
test_pinch_reports_scale_relative_to_initial_span
()
->
None
:
arbiter
,
emitted
=
_arbiter
({
"kind"
:
"pinch"
})
arbiter
.
pointer_down
(
1
,
0.0
,
0.0
,
t
=
0.0
)
assert
emitted
==
[],
"pinch needs two pointers"
arbiter
.
pointer_down
(
2
,
100.0
,
0.0
,
t
=
0.01
)
assert
_states
(
emitted
)
==
[
"began"
]
arbiter
.
pointer_move
(
2
,
200.0
,
0.0
,
t
=
0.05
)
changed
=
emitted
[
-
1
][
1
]
assert
changed
[
"state"
]
==
"changed"
assert
abs
(
changed
[
"scale"
]
-
2.0
)
<
1e-9
arbiter
.
pointer_up
(
2
,
200.0
,
0.0
,
t
=
0.1
)
ended
=
emitted
[
-
1
][
1
]
assert
ended
[
"state"
]
==
"ended"
assert
abs
(
ended
[
"scale"
]
-
2.0
)
<
1e-9
def
test_rotation_reports_radians_relative_to_initial_angle
()
->
None
:
arbiter
,
emitted
=
_arbiter
({
"kind"
:
"rotation"
})
arbiter
.
pointer_down
(
1
,
0.0
,
0.0
,
t
=
0.0
)
arbiter
.
pointer_down
(
2
,
100.0
,
0.0
,
t
=
0.01
)
assert
_states
(
emitted
)
==
[
"began"
]
arbiter
.
pointer_move
(
2
,
0.0
,
100.0
,
t
=
0.05
)
changed
=
emitted
[
-
1
][
1
]
assert
abs
(
changed
[
"rotation"
]
-
math
.
pi
/
2
)
<
1e-9
def
test_multiple_gestures_recognize_simultaneously
()
->
None
:
arbiter
,
emitted
=
_arbiter
(
{
"kind"
:
"tap"
},
{
"kind"
:
"swipe"
,
"min_velocity"
:
100.0
},
)
# A slow, stationary tap: tap fires, swipe stays silent.
arbiter
.
pointer_down
(
1
,
0.0
,
0.0
,
t
=
0.0
)
arbiter
.
pointer_up
(
1
,
0.0
,
0.0
,
t
=
0.05
)
assert
[
i
for
i
,
_p
in
emitted
]
==
[
0
]
# A fast flick: swipe fires (the movement makes the tap fail).
arbiter
.
pointer_down
(
1
,
0.0
,
0.0
,
t
=
1.0
)
arbiter
.
pointer_move
(
1
,
100.0
,
0.0
,
t
=
1.05
)
arbiter
.
pointer_up
(
1
,
200.0
,
0.0
,
t
=
1.1
)
assert
[
i
for
i
,
_p
in
emitted
]
==
[
0
,
1
]
# ======================================================================
# End-to-end: gestures prop through the reconciler
# ======================================================================
def
test_gesture_events_route_through_view_tag
()
->
None
:
from
pythonnative
.
element
import
Element
from
pythonnative
.
events
import
dispatch_event
from
pythonnative
.
reconciler
import
Reconciler
from
pythonnative
.
testing
import
FakeBackend
taps
:
List
[
GestureEvent
]
=
[]
el
=
Element
(
"View"
, {
"gestures"
: [
Tap
(
on_tap
=
taps
.
append
)]}, [])
backend
=
FakeBackend
()
rec
=
Reconciler
(
backend
)
rec
.
on_render_requested
=
lambda
:
None
rec
.
mount
(
el
)
tag
=
rec
.
root_tag
assert
tag
is
not
None
# The native payload received serialized specs (no closures).
view
=
backend
.
views
[
tag
]
assert
view
.
props
[
"gestures"
]
==
[
{
"kind"
:
"tap"
,
"n_taps"
:
1
,
"max_distance"
:
12.0
,
"simultaneous"
: [],
"wait_for"
: []}
]
# A handler-side arbiter (as the browser preview runs) feeds dispatch_event.
def
_emit
(
i
:
int
,
payload
:
Dict
[
str
,
Any
])
->
None
:
dispatch_event
(
tag
,
f"gesture:
{
i
}
"
,
payload
)
arbiter
=
GestureArbiter
(
view
.
props
[
"gestures"
],
_emit
)
arbiter
.
pointer_down
(
1
,
3.0
,
4.0
,
t
=
0.0
)
arbiter
.
pointer_up
(
1
,
3.0
,
4.0
,
t
=
0.1
)
assert
len
(
taps
)
==
1
assert
isinstance
(
taps
[
0
],
GestureEvent
)
assert
taps
[
0
].
x
==
3.0
and
taps
[
0
].
y
==
4.0
Back
|
FazBrowse Home
|
New Git URL