From 634789ae938d2eab4897832997b5e15d50724fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Tue, 16 May 2023 17:40:09 +0100 Subject: [PATCH] Add tests to screen modes. --- tests/test_screen_modes.py | 276 +++++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 tests/test_screen_modes.py diff --git a/tests/test_screen_modes.py b/tests/test_screen_modes.py new file mode 100644 index 000000000..c345b2903 --- /dev/null +++ b/tests/test_screen_modes.py @@ -0,0 +1,276 @@ +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(): + class FastCounter(Screen[None]): + def compose(self) -> ComposeResult: + self.lbl = Label("0") + yield self.lbl + + def on_mount(self) -> None: + self.set_interval(0.01, self.increment) + + def increment(self) -> None: + self.lbl.update(str(int(str(self.lbl.renderable)) + 1)) + + 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: + current = int(str(app.query_one(Label).renderable)) + await pilot.press("s") + assert str(app.query_one(Label).renderable) == ":)" + await pilot.press("s") + assert int(str(app.query_one(Label).renderable)) > current + + +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"]