mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
This is the heart of the issue introduced by
b48a1402b8
and which is being investigated in
https://github.com/Textualize/textual/issues/1343 -- the child widget can be
focused, but (as far as the author of the code is concerned) it has no
bindings. Bindings for movement-oriented keys exist on the screen which
composes up the widget into it. Up until 0.5.0 this worked just fine. As of
0.6.0, because binding inheritance was introduced, the bindings for movement
that live at the `Widget` level cause the widget that has no bindings to
appear to have bindings.
While this can potentially be worked around with the use of
inherit_bindings, this isn't a very satisfying solution and also breaks the
rule of least astonishment.
This test is going to be key to all of this. This is the test that should be
made to work without breaking any of the other currently-passing tests.
206 lines
7.7 KiB
Python
206 lines
7.7 KiB
Python
import pytest
|
|
|
|
from textual.app import App, ComposeResult
|
|
from textual.widgets import Static
|
|
from textual.screen import Screen
|
|
from textual.binding import Binding
|
|
|
|
##############################################################################
|
|
# These are the movement keys within Textual; they kind of have a special
|
|
# status in that they will get bound to movement-related methods.
|
|
MOVEMENT_KEYS = ["up", "down", "left", "right", "home", "end", "pageup", "pagedown"]
|
|
|
|
##############################################################################
|
|
class NoBindings(App[None]):
|
|
"""An app with zero bindings."""
|
|
|
|
|
|
async def test_just_app_no_bindings() -> None:
|
|
"""An app with no bindings should have no bindings, other than ctrl+c."""
|
|
async with NoBindings().run_test() as pilot:
|
|
assert list(pilot.app._bindings.keys.keys()) == ["ctrl+c"]
|
|
|
|
|
|
##############################################################################
|
|
class AlphaBinding(App[None]):
|
|
"""An app with a simple alpha key binding."""
|
|
|
|
BINDINGS = [Binding("a", "a", "a")]
|
|
|
|
|
|
async def test_just_app_alpha_binding() -> None:
|
|
"""An app with a single binding should have just the one binding."""
|
|
async with AlphaBinding().run_test() as pilot:
|
|
assert sorted(pilot.app._bindings.keys.keys()) == sorted(["ctrl+c", "a"])
|
|
|
|
|
|
##############################################################################
|
|
class ScreenNoBindings(Screen):
|
|
"""A screen with no added bindings."""
|
|
|
|
|
|
class AppWithScreenNoBindings(App[None]):
|
|
"""An app with no extra bindings but with a custom screen."""
|
|
|
|
SCREENS = {"main": ScreenNoBindings}
|
|
|
|
def on_mount(self) -> None:
|
|
self.push_screen("main")
|
|
|
|
|
|
@pytest.mark.xfail(
|
|
reason="Screen is incorrectly starting with bindings for movement keys [issue#1343]"
|
|
)
|
|
async def test_app_screen_no_bindings() -> None:
|
|
"""An screen with no bindings should have no bindings."""
|
|
async with AppWithScreenNoBindings().run_test() as pilot:
|
|
assert list(pilot.app.screen._bindings.keys.keys()) == []
|
|
|
|
|
|
##############################################################################
|
|
class ScreenWithBindings(Screen):
|
|
"""A screen with a simple alpha key binding."""
|
|
|
|
BINDINGS = [Binding("a", "a", "a")]
|
|
|
|
|
|
class AppWithScreenThatHasABinding(App[None]):
|
|
"""An app with no extra bindings but with a custom screen with a binding."""
|
|
|
|
SCREENS = {"main": ScreenWithBindings}
|
|
|
|
def on_mount(self) -> None:
|
|
self.push_screen("main")
|
|
|
|
|
|
@pytest.mark.xfail(
|
|
reason="Screen is incorrectly starting with bindings for movement keys [issue#1343]"
|
|
)
|
|
async def test_app_screen_with_bindings() -> None:
|
|
"""An app with a screen and a binding should only have ctrl+c as a binding."""
|
|
async with AppWithScreenThatHasABinding().run_test() as pilot:
|
|
assert list(pilot.app.screen._bindings.keys.keys()) == ["a"]
|
|
|
|
|
|
##############################################################################
|
|
class NoBindingsAndStaticWidgetNoBindings(App[None]):
|
|
"""An app with no bindings, enclosing a widget with no bindings."""
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield Static("Poetry! They should have sent a poet.")
|
|
|
|
|
|
@pytest.mark.xfail(
|
|
reason="Static is incorrectly starting with bindings for movement keys [issue#1343]"
|
|
)
|
|
async def test_just_app_no_bindings_widget_no_bindings() -> None:
|
|
"""A widget with no bindings should have no bindings. Its app should have just ctrl+c"""
|
|
async with NoBindingsAndStaticWidgetNoBindings().run_test() as pilot:
|
|
assert list(pilot.app._bindings.keys.keys()) == ["ctrl+c"]
|
|
assert list(pilot.app.screen.query_one(Static)._bindings.keys.keys()) == []
|
|
|
|
|
|
##############################################################################
|
|
class AppKeyRecorder(App[None]):
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.pressed_keys: list[str] = []
|
|
|
|
async def action_record(self, key: str) -> None:
|
|
self.pressed_keys.append(key)
|
|
|
|
|
|
##############################################################################
|
|
class AppWithMovementKeysBound(AppKeyRecorder):
|
|
BINDINGS = [
|
|
Binding("x", "record('x')", "x"),
|
|
*[Binding(key, f"record({key}')", key) for key in MOVEMENT_KEYS],
|
|
]
|
|
|
|
|
|
async def test_pressing_alpha_on_app() -> None:
|
|
"""Test that pressing the an alpha key, when it's bound on the app, results in an action fire."""
|
|
async with AppWithMovementKeysBound().run_test() as pilot:
|
|
await pilot.press(*"xxxxx")
|
|
assert "".join(pilot.app.pressed_keys) == "xxxxx"
|
|
|
|
|
|
@pytest.mark.xfail(
|
|
reason="Up key isn't firing bound action on an app due to key inheritence of its screen [issue#1343]"
|
|
)
|
|
async def test_pressing_movement_keys_app() -> None:
|
|
"""Test that pressing the movement keys, when they're bound on the app, results in an action fire."""
|
|
async with AppWithMovementKeysBound().run_test() as pilot:
|
|
await pilot.press("x", *MOVEMENT_KEYS, "x")
|
|
assert pilot.app.pressed_keys == ["x", *MOVEMENT_KEYS, "x"]
|
|
|
|
|
|
##############################################################################
|
|
class WidgetWithBindings(Static, can_focus=True):
|
|
"""A widget that has its own bindings for the movement keys."""
|
|
|
|
BINDINGS = [
|
|
Binding("x", "record('x')", "x"),
|
|
*[Binding(key, f"local_record('{key}')", key) for key in MOVEMENT_KEYS]
|
|
]
|
|
|
|
async def action_local_record(self, key: str) -> None:
|
|
# Sneaky forward reference. Just for the purposes of testing.
|
|
await self.app.action_record(f"locally_{key}")
|
|
|
|
|
|
class AppWithWidgetWithBindings(AppKeyRecorder):
|
|
"""A test app that composes with a widget that has movement bindings."""
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield WidgetWithBindings()
|
|
|
|
def on_mount(self) -> None:
|
|
self.query_one(WidgetWithBindings).focus()
|
|
|
|
|
|
async def test_focused_child_widget_with_movement_bindings() -> None:
|
|
"""A focused child widget with movement bindings should handle its own actions."""
|
|
async with AppWithWidgetWithBindings().run_test() as pilot:
|
|
await pilot.press("x", *MOVEMENT_KEYS, "x")
|
|
assert pilot.app.pressed_keys == ["x", *[f"locally_{key}" for key in MOVEMENT_KEYS], "x"]
|
|
|
|
##############################################################################
|
|
class FocusableWidgetWithNoBindings(Static, can_focus=True):
|
|
"""A widget that can receive focus but has no bindings."""
|
|
|
|
class ScreenWithMovementBindings(Screen):
|
|
"""A screen that binds keys, including movement keys."""
|
|
|
|
BINDINGS = [
|
|
Binding("x", "record('x')", "x"),
|
|
*[Binding(key, f"screen_record('{key}')", key) for key in MOVEMENT_KEYS]
|
|
]
|
|
|
|
async def action_screen_record(self, key: str) -> None:
|
|
# Sneaky forward reference. Just for the purposes of testing.
|
|
await self.app.action_record(f"screen_{key}")
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield FocusableWidgetWithNoBindings()
|
|
|
|
def on_mount(self) -> None:
|
|
self.query_one(FocusableWidgetWithNoBindings).focus()
|
|
|
|
class AppWithScreenWithBindingsWidgetNoBindings(AppKeyRecorder):
|
|
"""An app with a non-default screen that handles movement key bindings."""
|
|
|
|
SCREENS = {"main":ScreenWithMovementBindings}
|
|
|
|
def on_mount(self) -> None:
|
|
self.push_screen("main")
|
|
|
|
@pytest.mark.xfail(
|
|
reason="Movement keys never make it to the screen with such bindings due to key inheritance and pre-bound movement keys [issue#1343]"
|
|
)
|
|
async def test_focused_child_widget_with_movement_bindings_on_screen() -> None:
|
|
"""A focused child widget, with movement bindings in the screen, should trigger screen actions."""
|
|
async with AppWithScreenWithBindingsWidgetNoBindings().run_test() as pilot:
|
|
await pilot.press("x", *MOVEMENT_KEYS, "x")
|
|
assert pilot.app.pressed_keys == ["x", *[f"locally_{key}" for key in MOVEMENT_KEYS], "x"]
|