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
195
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
from unittest.mock import MagicMock
import pytest
from pyratatui import Rect
from pygittools.tui.changes_list import ChangesList
from pygittools.tui.input_context import InputContext, input_context, nav_context
from pygittools.tui.keys import key
from pygittools.tui.log_list import CommitLogList
from pygittools.tui.pages.actions import ActionsPage
from pygittools.tui.pages.help import HelpPage
from pygittools.tui.tasks_list import TasksBoardList
from pygittools.tui.types import PageAction, PageContext
from pygittools.tui.worktrees_list import WorktreesList
_HeaderFocus = Literal["changes", "repo", "branch", "actions"]
_HomeTab = Literal["status", "log", "tasks", "worktrees"]
@dataclass
class FakeHost:
page_context: PageContext | None
changes: ChangesList
log: CommitLogList
tasks: TasksBoardList
worktrees: WorktreesList
header_focus: _HeaderFocus = "changes"
active_tab: _HomeTab = "status"
has_actions: bool = False
def set_header_focus(self, focus: _HeaderFocus) -> None:
self.header_focus = focus
def set_active_tab(self, tab: _HomeTab) -> None:
self.active_tab = tab
@pytest.fixture
def host(page_ctx: PageContext) -> FakeHost:
changes = ChangesList()
changes.refresh(page_ctx.repo)
log = CommitLogList()
log.refresh(page_ctx.repo)
tasks = TasksBoardList()
tasks.refresh(page_ctx.repo)
worktrees = WorktreesList()
worktrees.refresh(page_ctx.repo)
return FakeHost(page_context=page_ctx, changes=changes, log=log, tasks=tasks, worktrees=worktrees)
def test_nav_context_status_hints(host: FakeHost) -> None:
nav = nav_context()
host.header_focus = "repo"
assert "browse projects" in nav.status_hint(host)
host.header_focus = "branch"
assert "switch branch" in nav.status_hint(host)
host.active_tab = "log"
host.header_focus = "changes"
assert "j/k move" in nav.status_hint(host)
def test_nav_context_opens_help(host: FakeHost) -> None:
dispatch = nav_context().dispatch_key(host, key("?"))
assert dispatch.page_result is not None
assert dispatch.page_result.action == PageAction.PUSH
assert isinstance(dispatch.page_result.next_page, HelpPage)
def test_nav_context_opens_actions(host: FakeHost) -> None:
dispatch = nav_context().dispatch_key(host, key("!"))
assert dispatch.page_result is not None
assert dispatch.page_result.action == PageAction.PUSH
assert isinstance(dispatch.page_result.next_page, ActionsPage)
def test_nav_context_repo_header(host: FakeHost) -> None:
host.header_focus = "repo"
nav = nav_context()
nav.dispatch_key(host, key("Down"))
assert host.header_focus == "branch"
dispatch = nav.dispatch_key(host, key("Enter"))
assert dispatch.page_result is not None
assert dispatch.page_result.action == PageAction.PUSH
def test_nav_context_branch_header(host: FakeHost) -> None:
host.header_focus = "branch"
nav = nav_context()
nav.dispatch_key(host, key("Up"))
assert host.header_focus == "repo"
nav.dispatch_key(host, key("Down"))
assert host.header_focus == "branch"
nav.dispatch_key(host, key("Down"))
assert host.header_focus == "actions"
assert "No action groups" in nav.status_hint(host)
nav.dispatch_key(host, key("Down"))
assert host.header_focus == "changes"
host.header_focus = "branch"
dispatch = nav.dispatch_key(host, key("Enter"))
assert dispatch.page_result is not None
assert dispatch.page_result.action == PageAction.PUSH
def test_nav_context_tab_switch(host: FakeHost) -> None:
nav = nav_context()
host.active_tab = "log"
nav.dispatch_key(host, key("Left"))
assert host.active_tab == "status"
host.active_tab = "status"
nav.dispatch_key(host, key("Right"))
assert host.active_tab == "log"
def test_nav_context_actions_row(host: FakeHost) -> None:
host.has_actions = True
nav = nav_context()
nav.dispatch_key(host, key("Up"))
assert host.header_focus == "actions"
assert "open actions" in nav.status_hint(host)
nav.dispatch_key(host, key("Up"))
assert host.header_focus == "branch"
nav.dispatch_key(host, key("Down"))
assert host.header_focus == "actions"
dispatch = nav.dispatch_key(host, key("Enter"))
assert dispatch.page_result is not None
assert dispatch.page_result.action == PageAction.PUSH
assert isinstance(dispatch.page_result.next_page, ActionsPage)
host.header_focus = "actions"
nav.dispatch_key(host, key("Down"))
assert host.header_focus == "changes"
def test_nav_context_moves_to_branch_from_top(host: FakeHost) -> None:
host.changes._list_state.select(0)
nav = nav_context()
nav.dispatch_key(host, key("Up"))
assert host.header_focus == "actions"
assert nav.dispatch_key(host, key("Enter")).page_result is None
nav.dispatch_key(host, key("Up"))
assert host.header_focus == "branch"
def test_nav_context_log_tab(host: FakeHost) -> None:
host.active_tab = "log"
nav_context().dispatch_key(host, key("Down"))
assert (host.log._list_state.selected or 0) >= 0
def test_nav_context_enters_input(host: FakeHost) -> None:
host.changes._list_state.select(0)
dispatch = nav_context().dispatch_key(host, key("a"))
assert dispatch.next_mode == "input"
def test_input_context_esc_returns_nav(host: FakeHost) -> None:
dispatch = input_context().dispatch_key(host, key("Esc"))
assert dispatch.next_mode == "nav"
def test_input_context_commit_relinquishes(host: FakeHost, monkeypatch: pytest.MonkeyPatch) -> None:
host.changes.set_commit_message("msg")
monkeypatch.setattr(
"pygittools.tui.changes_list.commit_staged",
lambda _repo, _msg: (True, ""),
)
dispatch = input_context().dispatch_key(host, key("Enter"))
assert dispatch.next_mode == "nav"
def test_input_context_status_hint() -> None:
assert "Esc" in InputContext().status_hint(
FakeHost(None, ChangesList(), CommitLogList(), TasksBoardList(), WorktreesList()),
)
def test_nav_context_without_page_context() -> None:
host = FakeHost(
page_context=None,
changes=ChangesList(),
log=CommitLogList(),
tasks=TasksBoardList(),
worktrees=WorktreesList(),
)
assert nav_context().dispatch_key(host, key("Down")).page_result is None
def test_draw_helpers_unused() -> None:
# keep import of Rect available for call sites in other modules
assert Rect(0, 0, 1, 1).width == 1
assert MagicMock() is not None