File: test_keymap.py

package info (click to toggle)
textual 2.1.2-1.1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 55,080 kB
  • sloc: python: 85,423; lisp: 1,669; makefile: 101
file content (194 lines) | stat: -rw-r--r-- 5,807 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
from __future__ import annotations

from typing import Any

from textual.app import App, ComposeResult
from textual.binding import Binding, Keymap
from textual.dom import DOMNode
from textual.widget import Widget
from textual.widgets import Label


class Counter(App[None]):
    BINDINGS = [
        Binding(key="i,up", action="increment", id="app.increment"),
        Binding(key="d,down", action="decrement", id="app.decrement"),
    ]

    def __init__(self, keymap: Keymap, *args: Any, **kwargs: Any):
        super().__init__(*args, **kwargs)
        self.count = 0
        self.clashed_bindings: set[Binding] | None = None
        self.clashed_node: DOMNode | None = None
        self.keymap = keymap

    def compose(self) -> ComposeResult:
        yield Label("foo")

    def on_mount(self) -> None:
        self.set_keymap(self.keymap)

    def action_increment(self) -> None:
        self.count += 1

    def action_decrement(self) -> None:
        self.count -= 1

    def handle_bindings_clash(
        self, clashed_bindings: set[Binding], node: DOMNode
    ) -> None:
        self.clashed_bindings = clashed_bindings
        self.clashed_node = node


async def test_keymap_default_binding_replaces_old_binding():
    app = Counter({"app.increment": "right,k"})
    async with app.run_test() as pilot:
        # The original bindings are removed - action not called.
        await pilot.press("i", "up")
        assert app.count == 0

        # The new bindings are active and call the action.
        await pilot.press("right", "k")
        assert app.count == 2


async def test_keymap_sends_message_when_clash():
    app = Counter({"app.increment": "d"})
    async with app.run_test() as pilot:
        await pilot.press("d")
        assert app.clashed_bindings is not None
        assert len(app.clashed_bindings) == 1
        clash = app.clashed_bindings.pop()
        assert app.clashed_node is app
        assert clash.key == "d"
        assert clash.action == "increment"
        assert clash.id == "app.increment"


async def test_keymap_with_unknown_id_is_noop():
    app = Counter({"this.is.an.unknown.id": "d"})
    async with app.run_test() as pilot:
        await pilot.press("d")
        assert app.count == -1


async def test_keymap_inherited_bindings_same_id():
    """When a child widget inherits from a parent widget, if they have
    a binding with the same ID, then both parent and child bindings will
    be overridden by the keymap (assuming the keymap has a mapping with the
    same ID)."""

    parent_counter = 0
    child_counter = 0

    class Parent(Widget, can_focus=True):
        BINDINGS = [
            Binding(key="x", action="increment", id="increment"),
        ]

        def action_increment(self) -> None:
            nonlocal parent_counter
            parent_counter += 1

    class Child(Parent):
        BINDINGS = [
            Binding(key="x", action="increment", id="increment"),
        ]

        def action_increment(self) -> None:
            nonlocal child_counter
            child_counter += 1

    class MyApp(App[None]):
        def compose(self) -> ComposeResult:
            yield Parent()
            yield Child()

        def on_mount(self) -> None:
            self.set_keymap({"increment": "i"})

    app = MyApp()
    async with app.run_test() as pilot:
        # Default binding is unbound due to keymap.
        await pilot.press("x")
        assert parent_counter == 0
        assert child_counter == 0

        # New binding is active, parent is focused - action called.
        await pilot.press("i")
        assert parent_counter == 1
        assert child_counter == 0

        # Tab to focus the child.
        await pilot.press("tab")

        # Default binding results in no change.
        await pilot.press("x")
        assert parent_counter == 1
        assert child_counter == 0

        # New binding is active, child is focused - action called.
        await pilot.press("i")
        assert parent_counter == 1
        assert child_counter == 1


async def test_keymap_child_with_different_id_overridden():
    """Ensures that overriding a parent binding doesn't influence a child
    binding with a different ID."""

    parent_counter = 0
    child_counter = 0

    class Parent(Widget, can_focus=True):
        BINDINGS = [
            Binding(key="x", action="increment", id="parent.increment"),
        ]

        def action_increment(self) -> None:
            nonlocal parent_counter
            parent_counter += 1

    class Child(Parent):
        BINDINGS = [
            Binding(key="x", action="increment", id="child.increment"),
        ]

        def action_increment(self) -> None:
            nonlocal child_counter
            child_counter += 1

    class MyApp(App[None]):
        def compose(self) -> ComposeResult:
            yield Parent()
            yield Child()

        def on_mount(self) -> None:
            self.set_keymap({"parent.increment": "i"})

    app = MyApp()
    async with app.run_test() as pilot:
        # Default binding is unbound due to keymap.
        await pilot.press("x")
        assert parent_counter == 0
        assert child_counter == 0

        # New binding is active, parent is focused - action called.
        await pilot.press("i")
        assert parent_counter == 1
        assert child_counter == 0

        # Tab to focus the child.
        await pilot.press("tab")

        # Default binding is still active on the child.
        await pilot.press("x")
        assert parent_counter == 1
        assert child_counter == 1

        # The binding from the keymap only affects the parent, so
        # pressing it with the child focused does nothing.
        await pilot.press("i")
        assert parent_counter == 1
        assert child_counter == 1