from functools import partial from itertools import cycle from typing import Type import pytest from textual.app import ( ActiveModeError, App, ComposeResult, InvalidModeError, UnknownModeError, ) from textual.screen import ModalScreen, Screen from textual.widgets import Footer, Header, Label, TextLog FRUITS = cycle("apple mango strawberry banana peach pear melon watermelon".split()) class ScreenBindingsMixin(Screen[None]): BINDINGS = [ ("1", "one", "Mode 1"), ("2", "two", "Mode 2"), ("p", "push", "Push rnd scrn"), ("o", "pop_screen", "Pop"), ("r", "remove", "Remove mode 1"), ] def action_one(self) -> None: self.app.switch_mode("one") def action_two(self) -> None: self.app.switch_mode("two") def action_fruits(self) -> None: self.app.switch_mode("fruits") def action_push(self) -> None: self.app.push_screen(FruitModal()) class BaseScreen(ScreenBindingsMixin): def __init__(self, label): super().__init__() self.label = label def compose(self) -> ComposeResult: yield Header() yield Label(self.label) yield Footer() def action_remove(self) -> None: self.app.remove_mode("one") class FruitModal(ModalScreen[str], ScreenBindingsMixin): BINDINGS = [("d", "dismiss_fruit", "Dismiss")] def compose(self) -> ComposeResult: yield Label(next(FRUITS)) class FruitsScreen(ScreenBindingsMixin): def compose(self) -> ComposeResult: yield TextLog() @pytest.fixture def ModesApp(): class ModesApp(App[None]): MODES = { "one": lambda: BaseScreen("one"), "two": "screen_two", } SCREENS = { "screen_two": lambda: BaseScreen("two"), } def on_mount(self): self.switch_mode("one") return ModesApp async def test_mode_setup(ModesApp: Type[App]): app = ModesApp() async with app.run_test(): assert isinstance(app.screen, BaseScreen) assert str(app.screen.query_one(Label).renderable) == "one" async def test_switch_mode(ModesApp: Type[App]): app = ModesApp() async with app.run_test() as pilot: await pilot.press("2") assert str(app.screen.query_one(Label).renderable) == "two" await pilot.press("1") assert str(app.screen.query_one(Label).renderable) == "one" async def test_switch_same_mode(ModesApp: Type[App]): app = ModesApp() async with app.run_test() as pilot: await pilot.press("1") assert str(app.screen.query_one(Label).renderable) == "one" await pilot.press("1") assert str(app.screen.query_one(Label).renderable) == "one" async def test_switch_unknown_mode(ModesApp: Type[App]): app = ModesApp() async with app.run_test(): with pytest.raises(UnknownModeError): app.switch_mode("unknown mode here") async def test_remove_mode(ModesApp: Type[App]): app = ModesApp() async with app.run_test() as pilot: app.switch_mode("two") await pilot.pause() assert str(app.screen.query_one(Label).renderable) == "two" app.remove_mode("one") assert "one" not in app.MODES async def test_remove_active_mode(ModesApp: Type[App]): app = ModesApp() async with app.run_test(): with pytest.raises(ActiveModeError): app.remove_mode("one") async def test_add_mode(ModesApp: Type[App]): app = ModesApp() async with app.run_test() as pilot: app.add_mode("three", BaseScreen("three")) app.switch_mode("three") await pilot.pause() assert str(app.screen.query_one(Label).renderable) == "three" async def test_add_mode_duplicated(ModesApp: Type[App]): app = ModesApp() async with app.run_test(): with pytest.raises(InvalidModeError): app.add_mode("one", BaseScreen("one")) async def test_screen_stack_preserved(ModesApp: Type[App]): fruits = [] N = 5 app = ModesApp() async with app.run_test() as pilot: # Build the stack up. for _ in range(N): await pilot.press("p") fruits.append(str(app.query_one(Label).renderable)) assert len(app.screen_stack) == N + 1 # Switch out and back. await pilot.press("2") assert len(app.screen_stack) == 1 await pilot.press("1") # Check the stack. assert len(app.screen_stack) == N + 1 for _ in range(N): assert str(app.query_one(Label).renderable) == fruits.pop() await pilot.press("o") async def test_inactive_stack_is_alive(): """This tests that timers in screens outside the active stack keep going.""" pings = [] class FastCounter(Screen[None]): def compose(self) -> ComposeResult: yield Label("fast") def on_mount(self) -> None: self.set_interval(0.01, self.ping) def ping(self) -> None: pings.append(str(self.app.query_one(Label).renderable)) def key_s(self): self.app.switch_mode("smile") class SmileScreen(Screen[None]): def compose(self) -> ComposeResult: yield Label(":)") def key_s(self): self.app.switch_mode("fast") class ModesApp(App[None]): MODES = { "fast": FastCounter, "smile": SmileScreen, } def on_mount(self) -> None: self.switch_mode("fast") app = ModesApp() async with app.run_test() as pilot: await pilot.press("s") assert str(app.query_one(Label).renderable) == ":)" await pilot.press("s") assert ":)" in pings async def test_multiple_mode_callbacks(): written = [] class LogScreen(Screen[None]): def __init__(self, value): super().__init__() self.value = value def key_p(self) -> None: self.app.push_screen(ResultScreen(self.value), written.append) class ResultScreen(Screen[str]): def __init__(self, value): super().__init__() self.value = value def key_p(self) -> None: self.dismiss(self.value) def key_f(self) -> None: self.app.switch_mode("first") def key_o(self) -> None: self.app.switch_mode("other") class ModesApp(App[None]): MODES = { "first": lambda: LogScreen("first"), "other": lambda: LogScreen("other"), } def on_mount(self) -> None: self.switch_mode("first") def key_f(self) -> None: self.switch_mode("first") def key_o(self) -> None: self.switch_mode("other") app = ModesApp() async with app.run_test() as pilot: # Push and dismiss ResultScreen("first") await pilot.press("p") await pilot.press("p") assert written == ["first"] # Push ResultScreen("first") await pilot.press("p") # Switch to LogScreen("other") await pilot.press("o") # Push and dismiss ResultScreen("other") await pilot.press("p") await pilot.press("p") assert written == ["first", "other"] # Go back to ResultScreen("first") await pilot.press("f") # Dismiss ResultScreen("first") await pilot.press("p") assert written == ["first", "other", "first"]