Skip to content

Commit cfd5d53

Browse files
committed
test for unmount
1 parent 02658de commit cfd5d53

File tree

4 files changed

+85
-18
lines changed

4 files changed

+85
-18
lines changed

src/textual/app.py

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -638,11 +638,18 @@ async def _press_keys(self, keys: Iterable[str]) -> None:
638638
await app._animator.wait_for_idle()
639639

640640
@asynccontextmanager
641-
async def run_test(self, *, headless: bool = True):
641+
async def run_test(
642+
self,
643+
*,
644+
headless: bool = True,
645+
size: tuple[int, int] | None = (80, 24),
646+
):
642647
"""An asynchronous context manager for testing app.
643648
644649
Args:
645650
headless (bool, optional): Run in headless mode (no output or input). Defaults to True.
651+
size (tuple[int, int] | None, optional): Force terminal size to `(WIDTH, HEIGHT)`,
652+
or None to auto-detect. Defaults to None.
646653
647654
"""
648655
from .pilot import Pilot
@@ -655,7 +662,11 @@ def on_app_ready() -> None:
655662
app_ready_event.set()
656663

657664
async def run_app(app) -> None:
658-
await app._process_messages(ready_callback=on_app_ready, headless=headless)
665+
await app._process_messages(
666+
ready_callback=on_app_ready,
667+
headless=headless,
668+
terminal_size=size,
669+
)
659670

660671
# Launch the app in the "background"
661672
app_task = asyncio.create_task(run_app(app))
@@ -1135,24 +1146,30 @@ async def _process_messages(
11351146

11361147
async def run_process_messages():
11371148
"""The main message loop, invoke below."""
1149+
1150+
async def invoke_ready_callback() -> None:
1151+
if ready_callback is not None:
1152+
ready_result = ready_callback()
1153+
if inspect.isawaitable(ready_result):
1154+
await ready_result
1155+
11381156
try:
1139-
await self._dispatch_message(events.Compose(sender=self))
1140-
await self._dispatch_message(events.Mount(sender=self))
1141-
finally:
1142-
self._mounted_event.set()
1157+
try:
1158+
await self._dispatch_message(events.Compose(sender=self))
1159+
await self._dispatch_message(events.Mount(sender=self))
1160+
finally:
1161+
self._mounted_event.set()
11431162

1144-
Reactive._initialize_object(self)
1163+
Reactive._initialize_object(self)
11451164

1146-
self.stylesheet.update(self)
1147-
self.refresh()
1165+
self.stylesheet.update(self)
1166+
self.refresh()
11481167

1149-
await self.animator.start()
1150-
await self._ready()
1168+
await self.animator.start()
11511169

1152-
if ready_callback is not None:
1153-
ready_result = ready_callback()
1154-
if inspect.isawaitable(ready_result):
1155-
await ready_result
1170+
finally:
1171+
await self._ready()
1172+
await invoke_ready_callback()
11561173

11571174
self._running = True
11581175

@@ -1356,7 +1373,7 @@ async def _shutdown(self) -> None:
13561373
await self._close_all()
13571374
await self._close_messages()
13581375

1359-
await self._dispatch_message(events.UnMount(sender=self))
1376+
await self._dispatch_message(events.Unmount(sender=self))
13601377

13611378
self._print_error_renderables()
13621379
if self.devtools is not None and self.devtools.is_connected:

src/textual/events.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ class Mount(Event, bubble=False, verbose=False):
123123
"""Sent when a widget is *mounted* and may receive messages."""
124124

125125

126-
class UnMount(Mount, bubble=False, verbose=False):
126+
class Unmount(Mount, bubble=False, verbose=False):
127127
"""Sent when a widget is unmounted and may not longer receive messages."""
128128

129129

src/textual/message_pump.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ async def _close_messages(self) -> None:
283283
for timer in stop_timers:
284284
await timer.stop()
285285
self._timers.clear()
286-
await self._message_queue.put(events.UnMount(sender=self))
286+
await self._message_queue.put(events.Unmount(sender=self))
287287
await self._message_queue.put(None)
288288
if self._task is not None and asyncio.current_task() != self._task:
289289
# Ensure everything is closed before returning

tests/test_unmount.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from textual.app import App, ComposeResult
2+
from textual import events
3+
from textual.containers import Container
4+
from textual.screen import Screen
5+
6+
7+
async def test_unmount():
8+
"""Text unmount events are received in reverse DOM order."""
9+
unmount_ids: list[str] = []
10+
11+
class UnmountWidget(Container):
12+
def on_unmount(self, event: events.Unmount):
13+
unmount_ids.append(f"{self.__class__.__name__}#{self.id}")
14+
15+
class MyScreen(Screen):
16+
def compose(self) -> ComposeResult:
17+
yield UnmountWidget(
18+
UnmountWidget(
19+
UnmountWidget(id="bar1"), UnmountWidget(id="bar2"), id="bar"
20+
),
21+
UnmountWidget(
22+
UnmountWidget(id="baz1"), UnmountWidget(id="baz2"), id="baz"
23+
),
24+
id="top",
25+
)
26+
27+
def on_unmount(self, event: events.Unmount):
28+
unmount_ids.append(f"{self.__class__.__name__}#{self.id}")
29+
30+
class UnmountApp(App):
31+
async def on_mount(self) -> None:
32+
self.push_screen(MyScreen(id="main"))
33+
34+
app = UnmountApp()
35+
async with app.run_test() as pilot:
36+
await pilot.pause() # TODO remove when push_screen is awaitable
37+
await pilot.exit(None)
38+
39+
expected = [
40+
"UnmountWidget#bar1",
41+
"UnmountWidget#bar2",
42+
"UnmountWidget#baz1",
43+
"UnmountWidget#baz2",
44+
"UnmountWidget#bar",
45+
"UnmountWidget#baz",
46+
"UnmountWidget#top",
47+
"MyScreen#main",
48+
]
49+
50+
assert unmount_ids == expected

0 commit comments

Comments
 (0)