diff --git a/.gitignore b/.gitignore index d3b2bcb969..97b3813f5c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ docs/build docs/source/_build tools/*.txt playground/ +.mypy_cache/ +.screenshot_cache/ # Byte-compiled / optimized / DLL files __pycache__/ @@ -129,3 +131,6 @@ sandbox/ # Used by mkdocs-material social plugin .cache + +# Snapshot tests output +__snapshots__/ diff --git a/CHANGELOG.md b/CHANGELOG.md index ac56f495c3..f34ee2b40d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [8.1.1] - 2026-03-10 + +### Fixed + +- Hotfix for animation on complete https://github.com/Textualize/textual/pull/6412 + ## [8.1.0] - 2026-03-10 ### Changed @@ -3381,6 +3387,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040 - New handler system for messages that doesn't require inheritance - Improved traceback handling +[8.1.1]: https://github.com/Textualize/textual/compare/v8.1.0...v8.1.1 [8.1.0]: https://github.com/Textualize/textual/compare/v8.0.2...v8.1.0 [8.0.2]: https://github.com/Textualize/textual/compare/v8.0.1...v8.0.2 [8.0.1]: https://github.com/Textualize/textual/compare/v8.0.0...v8.0.1 diff --git a/pyproject.toml b/pyproject.toml index a2ae6d66e2..a1ca675c94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "textual" -version = "8.1.0" +version = "8.1.1" homepage = "https://github.com/Textualize/textual" repository = "https://github.com/Textualize/textual" documentation = "https://textual.textualize.io/" @@ -36,6 +36,7 @@ include = [ # that works around that. { path = "docs-offline/**/*", format = "sdist" }, ] +exclude = ["tests/snapshot_tests/__snapshots__"] [tool.poetry.urls] "Bug Tracker" = "https://github.com/Textualize/textual/issues" diff --git a/src/textual/_animator.py b/src/textual/_animator.py index 299333059d..a6970b4990 100644 --- a/src/textual/_animator.py +++ b/src/textual/_animator.py @@ -458,10 +458,7 @@ def _animate( if (current_animation := self._animations.get(animation_key)) is not None: if (on_complete := current_animation.on_complete) is not None: - on_complete() - self._animations.pop(animation_key) - if current_animation == animation: - return + self.app.call_later(on_complete) self._animations[animation_key] = animation self._timer.resume() diff --git a/tests/test_animation.py b/tests/test_animation.py index f40c1fa888..9784e3db95 100644 --- a/tests/test_animation.py +++ b/tests/test_animation.py @@ -213,34 +213,89 @@ async def test_cancel_widget_non_animation() -> None: assert not pilot.app.animator.is_being_animated(widget, "counter") -async def test_double_animation_on_complete() -> None: - """Test that animating an attribute a second time, fires its `on_complete` callback.""" +async def test_double_animation_same_value_on_complete() -> None: + """Test that animating an attribute a second time to the same value, fires its `on_complete` callback.""" - complete_count = 0 + completed: list[str] = [] class AnimApp(App): x = var(0) - def on_key(self) -> None: + async def animate_a(self) -> None: def on_complete() -> None: - nonlocal complete_count - complete_count += 1 + completed.append("a") self.animator.animate( self, "x", - 100 + complete_count, + 100, + duration=0.1, + on_complete=on_complete, + ) + + async def animate_b(self) -> None: + + def on_complete() -> None: + completed.append("b") + + self.animator.animate( + self, + "x", + 100, + duration=0.1, + on_complete=on_complete, + ) + + app = AnimApp() + async with app.run_test() as pilot: + await app.animate_a() + assert app.x != 100 + await app.animate_b() + await pilot.wait_for_animation() + assert completed == ["a", "b"] + assert app.x == 100 + + +async def test_double_animation_different_value_on_complete() -> None: + """Test that animating an attribute a second time to a different value, fires its `on_complete` callback.""" + + completed: list[str] = [] + + class AnimApp(App): + x = var(0) + + async def animate_a(self) -> None: + + def on_complete() -> None: + completed.append("a") + + self.animator.animate( + self, + "x", + 100, + duration=0.1, + on_complete=on_complete, + ) + + async def animate_b(self) -> None: + + def on_complete() -> None: + completed.append("b") + + self.animator.animate( + self, + "x", + 101, duration=0.1, on_complete=on_complete, ) app = AnimApp() async with app.run_test() as pilot: - # Press space twice to initiate 2 animations - await pilot.press("space") - await pilot.press("space") - # Wait for animations to complete + await app.animate_a() + assert app.x != 101 + await app.animate_b() # animate to different value await pilot.wait_for_animation() - # Check that on_complete callback was invoked twice - assert complete_count == 2 + assert completed == ["a", "b"] + assert app.x == 101