File: test_animation.py

package info (click to toggle)
textual 2.1.2-1.1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 55,080 kB
  • sloc: python: 85,423; lisp: 1,669; makefile: 101
file content (213 lines) | stat: -rw-r--r-- 7,298 bytes parent folder | download | duplicates (2)
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")