diff --git a/CHANGELOG.md b/CHANGELOG.md index b7d55e99b..81cbdbf06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed - DOMQuery now raises InvalidQueryFormat in response to invalid query strings, rather than cryptic CSS error +- Dropped quit_after, screenshot, and screenshot_title from App.run, which can all be done via auto_pilot +- Widgets are now closed in reversed DOM order ### Added +- Added Unmount event - Added App.run_async method +- Added App.run_test context manager +- Added auto_pilot to App.run and App.run_async ## [0.2.1] - 2022-10-23 diff --git a/src/textual/app.py b/src/textual/app.py index a6cd98fab..7d2f93fb3 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -633,6 +633,38 @@ class App(Generic[ReturnType], DOMNode): await asyncio.sleep(0.02) await app._animator.wait_for_idle() + @asynccontextmanager + async def run_test(self, *, headless: bool = True): + """An asynchronous context manager for testing app. + + Args: + headless (bool, optional): Run in headless mode (no output or input). Defaults to True. + + """ + from .pilot import Pilot + + app = self + app_ready_event = asyncio.Event() + + def on_app_ready() -> None: + """Called when app is ready to process events.""" + app_ready_event.set() + + async def run_app(app) -> None: + await app._process_messages(ready_callback=on_app_ready, headless=headless) + + # Launch the app in the "background" + asyncio.create_task(run_app(app)) + + # Wait until the app has performed all startup routines. + await app_ready_event.wait() + + # Context manager returns pilot object to manipulate the app + yield Pilot(app) + + # Shutdown the app cleanly + await app._shutdown() + async def run_async( self, *, @@ -655,8 +687,16 @@ class App(Generic[ReturnType], DOMNode): async def app_ready() -> None: """Called by the message loop when the app is ready.""" if auto_pilot is not None: + + async def run_auto_pilot(pilot) -> None: + try: + await auto_pilot(pilot) + except Exception: + app.exit() + raise + pilot = Pilot(app) - asyncio.create_task(auto_pilot(pilot)) + asyncio.create_task(run_auto_pilot(pilot)) await app._process_messages(ready_callback=app_ready, headless=headless) await app._shutdown() diff --git a/src/textual/pilot.py b/src/textual/pilot.py index 79472facf..58b2743d1 100644 --- a/src/textual/pilot.py +++ b/src/textual/pilot.py @@ -17,10 +17,15 @@ class Pilot: self._app = app def __rich_repr__(self) -> rich.repr.Result: - yield "app", "self._app" + yield "app", self._app @property def app(self) -> App: + """Get a reference to the application. + + Returns: + App: The App instance. + """ return self._app async def press(self, *keys: str) -> None: @@ -39,3 +44,11 @@ class Pilot: delay (float, optional): Seconds to pause. Defaults to 50ms. """ await asyncio.sleep(delay) + + async def exit(self, result: object) -> None: + """Exit the app with the given result. + + Args: + result (object): The app result returned by `run` or `run_async`. + """ + self.app.exit(result)