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
|
from time import perf_counter
from textual.app import App, ComposeResult
from textual.reactive import var
from textual.widgets import Static
class AnimApp(App):
CSS = """
#foo {
height: 1;
}
"""
def compose(self) -> ComposeResult:
yield Static("foo", id="foo")
async def test_animate_height() -> None:
"""Test animating styles.height works."""
# Styles.height is a scalar, which makes it more complicated to animate
app = AnimApp()
async with app.run_test() as pilot:
static = app.query_one(Static)
assert static.size.height == 1
assert static.styles.height.value == 1
static.styles.animate("height", 100, duration=0.5, easing="linear")
start = perf_counter()
# Wait for the animation to finished
await pilot.wait_for_animation()
elapsed = perf_counter() - start
# Check that the full time has elapsed
assert elapsed >= 0.5
# Check the height reached the maximum
assert static.styles.height.value == 100
async def test_scheduling_animation() -> None:
"""Test that scheduling an animation works."""
app = AnimApp()
delay = 0.1
async with app.run_test() as pilot:
styles = app.query_one(Static).styles
styles.background = "black"
styles.animate("background", "white", delay=delay, duration=0)
# Still black immediately after call, animation hasn't started yet due to `delay`
assert styles.background.rgb == (0, 0, 0)
await pilot.wait_for_scheduled_animations()
assert styles.background.rgb == (255, 255, 255)
async def test_wait_for_current_animations() -> None:
"""Test that we can wait only for the current animations taking place."""
app = AnimApp()
delay = 10
async with app.run_test() as pilot:
styles = app.query_one(Static).styles
styles.animate("height", 100, duration=0.1)
start = perf_counter()
styles.animate("height", 200, duration=0.1, delay=delay)
# Wait for the first animation to finish
await pilot.wait_for_animation()
elapsed = perf_counter() - start
assert elapsed < (delay / 2)
async def test_wait_for_current_and_scheduled_animations() -> None:
"""Test that we can wait for current and scheduled animations."""
app = AnimApp()
async with app.run_test() as pilot:
styles = app.query_one(Static).styles
start = perf_counter()
styles.animate("height", 50, duration=0.01)
styles.animate("background", "black", duration=0.01, delay=0.05)
await pilot.wait_for_scheduled_animations()
elapsed = perf_counter() - start
assert elapsed >= 0.06
assert styles.background.rgb == (0, 0, 0)
async def test_reverse_animations() -> None:
"""Test that you can create reverse animations.
Regression test for #1372 https://github.com/Textualize/textual/issues/1372
"""
app = AnimApp()
async with app.run_test() as pilot:
static = app.query_one(Static)
styles = static.styles
# Starting point.
styles.background = "black"
assert styles.background.rgb == (0, 0, 0)
# First, make sure we can go from black to white and back, step by step.
styles.animate("background", "white", duration=0.01)
await pilot.wait_for_animation()
assert styles.background.rgb == (255, 255, 255)
styles.animate("background", "black", duration=0.01)
await pilot.wait_for_animation()
assert styles.background.rgb == (0, 0, 0)
# Now, the actual test is to make sure we go back to black if creating both at once.
styles.animate("background", "white", duration=0.01)
styles.animate("background", "black", duration=0.01)
await pilot.wait_for_animation()
assert styles.background.rgb == (0, 0, 0)
async def test_schedule_reverse_animations() -> None:
"""Test that you can schedule reverse animations.
Regression test for #1372 https://github.com/Textualize/textual/issues/1372
"""
app = AnimApp()
async with app.run_test() as pilot:
static = app.query_one(Static)
styles = static.styles
# Starting point.
styles.background = "black"
assert styles.background.rgb == (0, 0, 0)
# First, make sure we can go from black to white and back, step by step.
styles.animate("background", "white", delay=0.01, duration=0.01)
await pilot.wait_for_scheduled_animations()
assert styles.background.rgb == (255, 255, 255)
styles.animate("background", "black", delay=0.01, duration=0.01)
await pilot.wait_for_scheduled_animations()
assert styles.background.rgb == (0, 0, 0)
# Now, the actual test is to make sure we go back to black if scheduling both at once.
styles.animate("background", "white", delay=0.025, duration=0.05)
# While the black -> white animation runs, start the white -> black animation.
styles.animate("background", "black", delay=0.05, duration=0.01)
await pilot.wait_for_scheduled_animations()
assert styles.background.rgb == (0, 0, 0)
class CancelAnimWidget(Static):
counter: var[float] = var(23)
class CancelAnimApp(App[None]):
counter: var[float] = var(23)
def compose(self) -> ComposeResult:
yield CancelAnimWidget()
async def test_cancel_app_animation() -> None:
"""It should be possible to cancel a running app animation."""
async with CancelAnimApp().run_test() as pilot:
pilot.app.animate("counter", value=0, final_value=1000, duration=60)
await pilot.pause()
assert pilot.app.animator.is_being_animated(pilot.app, "counter")
await pilot.app.stop_animation("counter")
assert not pilot.app.animator.is_being_animated(pilot.app, "counter")
async def test_cancel_app_non_animation() -> None:
"""It should be possible to attempt to cancel a non-running app animation."""
async with CancelAnimApp().run_test() as pilot:
assert not pilot.app.animator.is_being_animated(pilot.app, "counter")
await pilot.app.stop_animation("counter")
assert not pilot.app.animator.is_being_animated(pilot.app, "counter")
async def test_cancel_widget_animation() -> None:
"""It should be possible to cancel a running widget animation."""
async with CancelAnimApp().run_test() as pilot:
widget = pilot.app.query_one(CancelAnimWidget)
widget.animate("counter", value=0, final_value=1000, duration=60)
await pilot.pause()
assert pilot.app.animator.is_being_animated(widget, "counter")
await widget.stop_animation("counter")
assert not pilot.app.animator.is_being_animated(widget, "counter")
async def test_cancel_widget_non_animation() -> None:
"""It should be possible to attempt to cancel a non-running widget animation."""
async with CancelAnimApp().run_test() as pilot:
widget = pilot.app.query_one(CancelAnimWidget)
assert not pilot.app.animator.is_being_animated(widget, "counter")
await widget.stop_animation("counter")
assert not pilot.app.animator.is_being_animated(widget, "counter")
|