-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_animated.py
More file actions
168 lines (130 loc) · 5.28 KB
/
test_animated.py
File metadata and controls
168 lines (130 loc) · 5.28 KB
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
"""Unit tests for the Animated API (AnimatedValue, timing, sequence, etc.)."""
from __future__ import annotations
import threading
import time
from typing import Any
from pythonnative.animated import Animated, AnimatedValue
# ======================================================================
# AnimatedValue
# ======================================================================
def test_animated_value_initial() -> None:
v = AnimatedValue(3.14)
assert v.value == 3.14
assert float(v) == 3.14
def test_animated_value_set_value_fires_subscribers() -> None:
v = AnimatedValue(0.0)
received: list = []
unsub = v.add_listener("opacity", lambda new_val: received.append(new_val))
v.set_value(0.5)
v.set_value(1.0)
assert received == [0.5, 1.0]
unsub()
v.set_value(0.0)
assert received == [0.5, 1.0]
def test_animated_value_subscriber_exception_isolated() -> None:
v = AnimatedValue(0.0)
def boom(_: float) -> None:
raise RuntimeError("oops")
received: list = []
v.add_listener("a", boom)
v.add_listener("b", lambda x: received.append(x))
v.set_value(1.0)
assert received == [1.0]
# ======================================================================
# Timing animation
# ======================================================================
def _run_until(predicate: Any, timeout: float = 2.0) -> bool:
"""Spin until ``predicate`` returns True or ``timeout`` elapses."""
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if predicate():
return True
time.sleep(0.02)
return False
def test_timing_completes_at_target() -> None:
v = AnimatedValue(0.0)
completed = threading.Event()
Animated.timing(v, to=10.0, duration=80, easing="linear").start(on_complete=completed.set)
assert completed.wait(2.0), "animation never completed"
assert abs(v.value - 10.0) < 0.05
def test_timing_progress_fires_listener() -> None:
v = AnimatedValue(0.0)
received: list = []
v.add_listener("opacity", lambda x: received.append(x))
completed = threading.Event()
Animated.timing(v, to=1.0, duration=120, easing="linear").start(on_complete=completed.set)
assert completed.wait(2.0)
# Should have received intermediate values, not just the final.
assert len(received) >= 3
assert received[0] > 0.0
assert received[-1] == 1.0
def test_timing_easing_curves() -> None:
"""Easing curves change intermediate values; we just smoke-test the API."""
for easing in ("linear", "ease_in", "ease_out", "ease_in_out", "bounce"):
v = AnimatedValue(0.0)
completed = threading.Event()
Animated.timing(v, to=1.0, duration=40, easing=easing).start(on_complete=completed.set)
assert completed.wait(2.0), f"easing {easing} did not finish"
assert abs(v.value - 1.0) < 0.05
# ======================================================================
# Spring animation
# ======================================================================
def test_spring_settles() -> None:
v = AnimatedValue(0.0)
completed = threading.Event()
Animated.spring(v, to=5.0, stiffness=200, damping=20, mass=1.0).start(on_complete=completed.set)
assert completed.wait(3.0)
assert abs(v.value - 5.0) < 0.1
# ======================================================================
# Composition: sequence and parallel
# ======================================================================
def test_sequence_runs_in_order() -> None:
v1 = AnimatedValue(0.0)
v2 = AnimatedValue(0.0)
completed = threading.Event()
Animated.sequence(
[
Animated.timing(v1, to=1.0, duration=40, easing="linear"),
Animated.timing(v2, to=2.0, duration=40, easing="linear"),
]
).start(on_complete=completed.set)
assert completed.wait(3.0)
assert abs(v1.value - 1.0) < 0.1
assert abs(v2.value - 2.0) < 0.1
def test_parallel_runs_concurrently() -> None:
v1 = AnimatedValue(0.0)
v2 = AnimatedValue(0.0)
completed = threading.Event()
started = time.monotonic()
Animated.parallel(
[
Animated.timing(v1, to=1.0, duration=120, easing="linear"),
Animated.timing(v2, to=2.0, duration=120, easing="linear"),
]
).start(on_complete=completed.set)
assert completed.wait(3.0)
elapsed = time.monotonic() - started
# Two parallel 120ms animations should take ~120ms, not 240ms.
assert elapsed < 0.5
assert abs(v1.value - 1.0) < 0.1
assert abs(v2.value - 2.0) < 0.1
def test_delay_waits_then_completes() -> None:
completed = threading.Event()
started = time.monotonic()
Animated.delay(80).start(on_complete=completed.set)
assert completed.wait(2.0)
assert (time.monotonic() - started) >= 0.05
# ======================================================================
# Stop
# ======================================================================
def test_stop_freezes_value() -> None:
v = AnimatedValue(0.0)
handle = Animated.timing(v, to=10.0, duration=400, easing="linear")
handle.start()
time.sleep(0.05)
handle.stop()
snapshot = v.value
time.sleep(0.2)
# After stop, value should not advance further toward 10.
assert abs(v.value - snapshot) < 0.1
assert v.value < 9.0