|
|
@@ -40,30 +40,41 @@ def server():
|
|
|
shutil.rmtree(directory, ignore_errors=True)
|
|
|
|
|
|
|
|
|
-def test_enter_sends_execute_and_e_opens_editor(server):
|
|
|
+def test_ctrl_r_sends_execute_and_e_opens_editor(server):
|
|
|
pytest.importorskip("textual")
|
|
|
+ from textual.widgets import Input
|
|
|
+
|
|
|
from stt_daemon.tui.app import AssistantTUI
|
|
|
|
|
|
path, _service = server
|
|
|
app = AssistantTUI(path)
|
|
|
sent: list[tuple] = []
|
|
|
+ observed: dict = {}
|
|
|
|
|
|
async def scenario():
|
|
|
async with app.run_test() as pilot:
|
|
|
await pilot.pause()
|
|
|
app.client.send = lambda type_, **fields: sent.append((type_, fields))
|
|
|
|
|
|
- await pilot.press("enter")
|
|
|
+ await pilot.press("ctrl+r")
|
|
|
await pilot.pause()
|
|
|
await pilot.press("a")
|
|
|
await pilot.pause()
|
|
|
await pilot.press("escape")
|
|
|
await pilot.pause()
|
|
|
|
|
|
+ await pilot.press("e")
|
|
|
+ await pilot.pause()
|
|
|
+ edit = app.query_one("#edit", Input)
|
|
|
+ observed["edit_display"] = edit.display
|
|
|
+ observed["edit_focused"] = edit.has_focus
|
|
|
+
|
|
|
asyncio.run(scenario())
|
|
|
assert ("command_execute", {}) in sent
|
|
|
assert ("command_start", {}) in sent
|
|
|
assert ("command_cancel", {}) in sent
|
|
|
+ assert observed["edit_display"] is True
|
|
|
+ assert observed["edit_focused"] is True
|
|
|
|
|
|
|
|
|
def test_c_key_confirms_before_clearing(server):
|
|
|
@@ -374,7 +385,7 @@ def test_triage_disabled_composes_no_triage_widget(server):
|
|
|
|
|
|
|
|
|
def test_triage_enabled_composes_split(server):
|
|
|
- """triage_enabled=True: #main wraps #transcript + #triage."""
|
|
|
+ """triage_enabled=True: #main wraps #triage (left) + #chat (right); transcript stays outside."""
|
|
|
pytest.importorskip("textual")
|
|
|
from textual.containers import Horizontal
|
|
|
from textual.widgets import RichLog
|
|
|
@@ -390,16 +401,75 @@ def test_triage_enabled_composes_split(server):
|
|
|
main = app.query_one("#main", Horizontal)
|
|
|
transcript = app.query_one("#transcript", RichLog)
|
|
|
triage = app.query_one("#triage", RichLog)
|
|
|
- assert transcript.parent is main
|
|
|
+ chat = app.query_one("#chat", RichLog)
|
|
|
assert triage.parent is main
|
|
|
+ assert chat.parent is main
|
|
|
+ assert transcript.parent is not main
|
|
|
assert triage.border_title == "triage"
|
|
|
+ # triage sits LEFT of chat inside #main.
|
|
|
+ assert main.children.index(triage) < main.children.index(chat)
|
|
|
# Outside widgets stay outside #main.
|
|
|
assert app.query_one("#status").parent is not main
|
|
|
- assert app.query_one("#chat").parent is not main
|
|
|
+ assert transcript.parent is not main
|
|
|
|
|
|
asyncio.run(scenario())
|
|
|
|
|
|
|
|
|
+def test_triage_enabled_layout_proportions(server):
|
|
|
+ """Rendered proportions: #triage ~1/3 and #chat ~2/3 of #main; #transcript ~25% of screen."""
|
|
|
+ pytest.importorskip("textual")
|
|
|
+ from textual.containers import Horizontal
|
|
|
+ from textual.widgets import RichLog
|
|
|
+
|
|
|
+ from stt_daemon.tui.app import AssistantTUI
|
|
|
+
|
|
|
+ path, _service = server
|
|
|
+ app = AssistantTUI(path, triage_enabled=True)
|
|
|
+
|
|
|
+ async def scenario(size=None):
|
|
|
+ kwargs = {"size": size} if size else {}
|
|
|
+ async with app.run_test(**kwargs) as pilot:
|
|
|
+ await pilot.pause()
|
|
|
+ main = app.query_one("#main", Horizontal)
|
|
|
+ transcript = app.query_one("#transcript", RichLog)
|
|
|
+ triage = app.query_one("#triage", RichLog)
|
|
|
+ chat = app.query_one("#chat", RichLog)
|
|
|
+ return (
|
|
|
+ main.size.width,
|
|
|
+ triage.size.width,
|
|
|
+ chat.size.width,
|
|
|
+ app.screen.size.height,
|
|
|
+ transcript.region.height,
|
|
|
+ )
|
|
|
+
|
|
|
+ main_w, triage_w, chat_w, screen_h, transcript_h = asyncio.run(scenario())
|
|
|
+ print(
|
|
|
+ f"measured: main_w={main_w} triage_w={triage_w} chat_w={chat_w} "
|
|
|
+ f"screen_h={screen_h} transcript_h={transcript_h}"
|
|
|
+ )
|
|
|
+ # Content boxes exclude the 1-cell border on each side; allow ±2 px for
|
|
|
+ # borders/rounding on the fractional split.
|
|
|
+ assert abs(triage_w / main_w - 1 / 3) < 0.05, (main_w, triage_w, chat_w)
|
|
|
+ assert abs(chat_w / main_w - 2 / 3) < 0.05, (main_w, triage_w, chat_w)
|
|
|
+ # region.height includes the transcript's 2 border rows; at the small
|
|
|
+ # default size rounding is coarse, so only demand a floor here. The
|
|
|
+ # precise 25% check runs at a taller size below.
|
|
|
+ assert transcript_h / screen_h >= 0.18, (screen_h, transcript_h)
|
|
|
+ assert transcript_h >= 1, transcript_h
|
|
|
+
|
|
|
+ app2 = AssistantTUI(path, triage_enabled=True)
|
|
|
+
|
|
|
+ async def scenario_tall():
|
|
|
+ async with app2.run_test(size=(80, 40)) as pilot:
|
|
|
+ await pilot.pause()
|
|
|
+ transcript2 = app2.query_one("#transcript", RichLog)
|
|
|
+ return app2.screen.size.height, transcript2.region.height
|
|
|
+
|
|
|
+ screen_h2, transcript_h2 = asyncio.run(scenario_tall())
|
|
|
+ print(f"measured tall: screen_h={screen_h2} transcript_h={transcript_h2}")
|
|
|
+ assert abs(transcript_h2 / screen_h2 - 0.25) < 0.06, (screen_h2, transcript_h2)
|
|
|
+
|
|
|
+
|
|
|
def test_triage_enabled_renders_signals_on_epoch_change(server):
|
|
|
"""A triage event + status event must actually write rendered lines into #triage."""
|
|
|
pytest.importorskip("textual")
|
|
|
@@ -554,3 +624,436 @@ def test_default_ctor_has_no_triage_arg(server):
|
|
|
app = AssistantTUI(path)
|
|
|
assert app.triage_enabled is False
|
|
|
assert app._seen_triage_epoch == 0
|
|
|
+
|
|
|
+
|
|
|
+def test_maximize_triage_hides_chat_and_fills_screen(server):
|
|
|
+ """Focus #triage, press m: #main maximized, chat hidden, triage fills screen."""
|
|
|
+ pytest.importorskip("textual")
|
|
|
+ from textual.widgets import RichLog
|
|
|
+
|
|
|
+ from stt_daemon.tui.app import AssistantTUI
|
|
|
+
|
|
|
+ path, _service = server
|
|
|
+ app = AssistantTUI(path, triage_enabled=True)
|
|
|
+ observed: dict = {}
|
|
|
+
|
|
|
+ async def scenario():
|
|
|
+ async with app.run_test(size=(80, 40)) as pilot:
|
|
|
+ await pilot.pause()
|
|
|
+ app.query_one("#triage", RichLog).focus()
|
|
|
+ await pilot.pause()
|
|
|
+ await pilot.press("m")
|
|
|
+ await pilot.pause()
|
|
|
+ observed["maximized_is_main"] = app.screen.maximized is app.query_one("#main")
|
|
|
+ observed["triage_display"] = app.query_one("#triage", RichLog).display
|
|
|
+ observed["chat_display"] = app.query_one("#chat", RichLog).display
|
|
|
+ observed["region"] = app.query_one("#triage", RichLog).region
|
|
|
+ observed["screen_size"] = app.screen.size
|
|
|
+
|
|
|
+ asyncio.run(scenario())
|
|
|
+ print("triage maximized: region=%s screen=%s" % (observed["region"], observed["screen_size"]))
|
|
|
+ assert observed["maximized_is_main"] is True
|
|
|
+ assert observed["triage_display"] is True
|
|
|
+ assert observed["chat_display"] is False
|
|
|
+ region = observed["region"]
|
|
|
+ size = observed["screen_size"]
|
|
|
+ assert abs(region.width - size.width) <= 2, (region, size)
|
|
|
+ assert abs(region.height - size.height) <= 2, (region, size)
|
|
|
+
|
|
|
+
|
|
|
+def test_maximize_toggle_restores_layout(server):
|
|
|
+ """Press m again: minimize restores both panes."""
|
|
|
+ pytest.importorskip("textual")
|
|
|
+ from textual.widgets import RichLog
|
|
|
+
|
|
|
+ from stt_daemon.tui.app import AssistantTUI
|
|
|
+
|
|
|
+ path, _service = server
|
|
|
+ app = AssistantTUI(path, triage_enabled=True)
|
|
|
+ observed: dict = {}
|
|
|
+
|
|
|
+ async def scenario():
|
|
|
+ async with app.run_test(size=(80, 40)) as pilot:
|
|
|
+ await pilot.pause()
|
|
|
+ app.query_one("#triage", RichLog).focus()
|
|
|
+ await pilot.pause()
|
|
|
+ await pilot.press("m")
|
|
|
+ await pilot.pause()
|
|
|
+ await pilot.press("m")
|
|
|
+ await pilot.pause()
|
|
|
+ observed["maximized"] = app.screen.maximized
|
|
|
+ observed["triage_display"] = app.query_one("#triage", RichLog).display
|
|
|
+ observed["chat_display"] = app.query_one("#chat", RichLog).display
|
|
|
+
|
|
|
+ asyncio.run(scenario())
|
|
|
+ assert observed["maximized"] is None
|
|
|
+ assert observed["triage_display"] is True
|
|
|
+ assert observed["chat_display"] is True
|
|
|
+
|
|
|
+
|
|
|
+def test_maximize_transcript_fills_screen(server):
|
|
|
+ """Focus #transcript, press m: transcript maximized and fills the screen."""
|
|
|
+ pytest.importorskip("textual")
|
|
|
+ from textual.widgets import RichLog
|
|
|
+
|
|
|
+ from stt_daemon.tui.app import AssistantTUI
|
|
|
+
|
|
|
+ path, _service = server
|
|
|
+ app = AssistantTUI(path, triage_enabled=True)
|
|
|
+ observed: dict = {}
|
|
|
+
|
|
|
+ async def scenario():
|
|
|
+ async with app.run_test(size=(80, 40)) as pilot:
|
|
|
+ await pilot.pause()
|
|
|
+ app.query_one("#transcript", RichLog).focus()
|
|
|
+ await pilot.pause()
|
|
|
+ await pilot.press("m")
|
|
|
+ await pilot.pause()
|
|
|
+ observed["maximized_is_transcript"] = (
|
|
|
+ app.screen.maximized is app.query_one("#transcript")
|
|
|
+ )
|
|
|
+ observed["region"] = app.query_one("#transcript", RichLog).region
|
|
|
+ observed["screen_size"] = app.screen.size
|
|
|
+ await pilot.press("m")
|
|
|
+ await pilot.pause()
|
|
|
+ observed["restored"] = app.screen.maximized
|
|
|
+
|
|
|
+ asyncio.run(scenario())
|
|
|
+ print("transcript maximized: region=%s screen=%s"
|
|
|
+ % (observed["region"], observed["screen_size"]))
|
|
|
+ assert observed["maximized_is_transcript"] is True
|
|
|
+ region = observed["region"]
|
|
|
+ size = observed["screen_size"]
|
|
|
+ assert abs(region.width - size.width) <= 2, (region, size)
|
|
|
+ assert abs(region.height - size.height) <= 2, (region, size)
|
|
|
+ assert observed["restored"] is None
|
|
|
+
|
|
|
+
|
|
|
+def test_escape_while_maximized_restores_panes(server):
|
|
|
+ """Escape minimizes (ESCAPE_TO_MINIMIZE) and _drain restores the hidden sibling."""
|
|
|
+ pytest.importorskip("textual")
|
|
|
+ from textual.widgets import RichLog
|
|
|
+
|
|
|
+ from stt_daemon.tui.app import AssistantTUI
|
|
|
+
|
|
|
+ path, _service = server
|
|
|
+ app = AssistantTUI(path, triage_enabled=True)
|
|
|
+ observed: dict = {}
|
|
|
+
|
|
|
+ async def scenario():
|
|
|
+ async with app.run_test(size=(80, 40)) as pilot:
|
|
|
+ await pilot.pause()
|
|
|
+ app.query_one("#triage", RichLog).focus()
|
|
|
+ await pilot.pause()
|
|
|
+ await pilot.press("m")
|
|
|
+ await pilot.pause()
|
|
|
+ assert app.screen.maximized is not None
|
|
|
+ await pilot.press("escape")
|
|
|
+ await pilot.pause()
|
|
|
+ # Let the 0.1s _drain timer run so the restore path executes.
|
|
|
+ for _ in range(10):
|
|
|
+ await pilot.pause()
|
|
|
+ await asyncio.sleep(0.05)
|
|
|
+ if app.screen.maximized is None and app.query_one("#chat", RichLog).display:
|
|
|
+ break
|
|
|
+ observed["maximized"] = app.screen.maximized
|
|
|
+ observed["triage_display"] = app.query_one("#triage", RichLog).display
|
|
|
+ observed["chat_display"] = app.query_one("#chat", RichLog).display
|
|
|
+
|
|
|
+ asyncio.run(scenario())
|
|
|
+ assert observed["maximized"] is None
|
|
|
+ assert observed["triage_display"] is True
|
|
|
+ assert observed["chat_display"] is True
|
|
|
+
|
|
|
+
|
|
|
+def test_maximize_chat_when_triage_disabled(server):
|
|
|
+ """triage_enabled=False: focus #chat, press m maximizes #main without crashing."""
|
|
|
+ pytest.importorskip("textual")
|
|
|
+ from textual.widgets import RichLog
|
|
|
+
|
|
|
+ from stt_daemon.tui.app import AssistantTUI
|
|
|
+
|
|
|
+ path, _service = server
|
|
|
+ app = AssistantTUI(path)
|
|
|
+ observed: dict = {}
|
|
|
+
|
|
|
+ async def scenario():
|
|
|
+ async with app.run_test(size=(80, 40)) as pilot:
|
|
|
+ await pilot.pause()
|
|
|
+ # Disabled: no #main/#triage; focusing #chat then m maximizes #chat itself.
|
|
|
+ app.query_one("#chat", RichLog).focus()
|
|
|
+ await pilot.pause()
|
|
|
+ await pilot.press("m")
|
|
|
+ await pilot.pause()
|
|
|
+ observed["maximized"] = app.screen.maximized
|
|
|
+ observed["chat"] = app.query_one("#chat", RichLog)
|
|
|
+ observed["region"] = app.query_one("#chat", RichLog).region
|
|
|
+ observed["screen_size"] = app.screen.size
|
|
|
+ await pilot.press("m")
|
|
|
+ await pilot.pause()
|
|
|
+ observed["restored"] = app.screen.maximized
|
|
|
+
|
|
|
+ asyncio.run(scenario())
|
|
|
+ print("chat maximized (disabled): region=%s screen=%s"
|
|
|
+ % (observed["region"], observed["screen_size"]))
|
|
|
+ assert observed["maximized"] is observed["chat"]
|
|
|
+ assert observed["restored"] is None
|
|
|
+
|
|
|
+
|
|
|
+def test_footer_hidden_while_maximized(server):
|
|
|
+ """ALLOW_IN_MAXIMIZED_VIEW hides the Footer while a pane is maximized."""
|
|
|
+ pytest.importorskip("textual")
|
|
|
+ from textual.widgets import Footer, RichLog
|
|
|
+
|
|
|
+ from stt_daemon.tui.app import AssistantTUI
|
|
|
+
|
|
|
+ path, _service = server
|
|
|
+ app = AssistantTUI(path, triage_enabled=True)
|
|
|
+ observed: dict = {}
|
|
|
+
|
|
|
+ async def scenario():
|
|
|
+ async with app.run_test(size=(80, 40)) as pilot:
|
|
|
+ await pilot.pause()
|
|
|
+ app.query_one("#triage", RichLog).focus()
|
|
|
+ await pilot.pause()
|
|
|
+ await pilot.press("m")
|
|
|
+ await pilot.pause()
|
|
|
+ observed["maximized"] = app.screen.maximized
|
|
|
+ observed["footer_region"] = app.query_one(Footer).region
|
|
|
+ await pilot.press("m")
|
|
|
+ await pilot.pause()
|
|
|
+
|
|
|
+ asyncio.run(scenario())
|
|
|
+ assert observed["maximized"] is not None
|
|
|
+ footer_region = observed["footer_region"]
|
|
|
+ assert footer_region.height == 0 or footer_region.width == 0, observed
|
|
|
+
|
|
|
+
|
|
|
+def test_question_mark_key_toggles_help_panel(server):
|
|
|
+ pytest.importorskip("textual")
|
|
|
+ from textual.widgets import HelpPanel
|
|
|
+
|
|
|
+ from stt_daemon.tui.app import AssistantTUI
|
|
|
+
|
|
|
+ path, _service = server
|
|
|
+ app = AssistantTUI(path)
|
|
|
+ observed: dict = {}
|
|
|
+
|
|
|
+ async def scenario():
|
|
|
+ async with app.run_test() as pilot:
|
|
|
+ await pilot.pause()
|
|
|
+ assert list(app.screen.query(HelpPanel)) == []
|
|
|
+ await pilot.press("question_mark")
|
|
|
+ await pilot.pause()
|
|
|
+ observed["shown"] = list(app.screen.query(HelpPanel)) != []
|
|
|
+ app.screen.query_one(HelpPanel) # must succeed while shown
|
|
|
+ await pilot.press("question_mark")
|
|
|
+ await pilot.pause()
|
|
|
+ observed["hidden"] = list(app.screen.query(HelpPanel)) == []
|
|
|
+
|
|
|
+ asyncio.run(scenario())
|
|
|
+ assert observed["shown"] is True, "? did not mount HelpPanel"
|
|
|
+ assert observed["hidden"] is True, "second ? did not remove HelpPanel"
|
|
|
+
|
|
|
+
|
|
|
+def test_app_bindings_list():
|
|
|
+ """BINDINGS: 7 visible footer entries; hidden ones after; no h binding."""
|
|
|
+ pytest.importorskip("textual")
|
|
|
+ from stt_daemon.tui.app import AssistantTUI
|
|
|
+
|
|
|
+ pairs = [(b.key, b.description) for b in AssistantTUI.BINDINGS]
|
|
|
+ assert ("ctrl+r", "execute") in pairs
|
|
|
+ assert ("m", "maximize pane") in pairs
|
|
|
+ assert ("question_mark", "help") in pairs
|
|
|
+ assert ("ctrl+s", "save screenshot") in pairs
|
|
|
+ assert ("a", "arm command") in pairs
|
|
|
+ assert ("escape", "cancel") in pairs
|
|
|
+ assert ("e", "edit command") in pairs
|
|
|
+ assert ("t", "chat toggle") in pairs
|
|
|
+ assert ("c", "clear") in pairs
|
|
|
+ assert ("q", "quit") in pairs
|
|
|
+ assert ("ctrl+p", "instructions") in pairs
|
|
|
+ assert not any(key == "h" for key, _ in pairs)
|
|
|
+ assert not any(key == "ctrl+m" for key, _ in pairs)
|
|
|
+ assert not any(key == "ctrl+h" for key, _ in pairs)
|
|
|
+ assert not any(key == "enter" for key, _ in pairs)
|
|
|
+ assert not any(key == "ctrl+k" for key, _ in pairs)
|
|
|
+ by_key = {b.key: b for b in AssistantTUI.BINDINGS}
|
|
|
+ assert by_key["question_mark"].show is True
|
|
|
+ assert by_key["m"].show is False
|
|
|
+ assert by_key["c"].show is False
|
|
|
+ assert by_key["ctrl+s"].show is False
|
|
|
+ assert by_key["ctrl+p"].show is False
|
|
|
+ # Visible seven first, in footer order.
|
|
|
+ visible = [(b.key, b.description) for b in AssistantTUI.BINDINGS if b.show]
|
|
|
+ assert visible == [
|
|
|
+ ("a", "arm command"),
|
|
|
+ ("ctrl+r", "execute"),
|
|
|
+ ("escape", "cancel"),
|
|
|
+ ("e", "edit command"),
|
|
|
+ ("t", "chat toggle"),
|
|
|
+ ("question_mark", "help"),
|
|
|
+ ("q", "quit"),
|
|
|
+ ]
|
|
|
+ assert by_key["ctrl+p"].priority is True
|
|
|
+
|
|
|
+
|
|
|
+def test_footer_shows_ordered_entries(server):
|
|
|
+ """Left list is the 7 visible bindings (? for help); instructions only right-aligned."""
|
|
|
+ pytest.importorskip("textual")
|
|
|
+ from textual.widgets import Footer
|
|
|
+ from textual.widgets._footer import FooterKey
|
|
|
+
|
|
|
+ from stt_daemon.tui.app import AssistantTUI
|
|
|
+
|
|
|
+ path, _service = server
|
|
|
+ app = AssistantTUI(path)
|
|
|
+ observed: dict = {}
|
|
|
+
|
|
|
+ async def scenario():
|
|
|
+ async with app.run_test() as pilot:
|
|
|
+ await pilot.pause()
|
|
|
+ shown = [
|
|
|
+ (b.binding.key, b.binding.description)
|
|
|
+ for b in app.screen.active_bindings.values()
|
|
|
+ if b.binding.show
|
|
|
+ ]
|
|
|
+ observed["shown"] = shown
|
|
|
+ footer = app.query_one(Footer)
|
|
|
+ keys = list(footer.query(FooterKey))
|
|
|
+ observed["keys"] = [
|
|
|
+ (k.key, k.key_display, k.description, k.action, set(k.classes))
|
|
|
+ for k in keys
|
|
|
+ ]
|
|
|
+
|
|
|
+ asyncio.run(scenario())
|
|
|
+ expected = [
|
|
|
+ ("a", "arm command"),
|
|
|
+ ("ctrl+r", "execute"),
|
|
|
+ ("escape", "cancel"),
|
|
|
+ ("e", "edit command"),
|
|
|
+ ("t", "chat toggle"),
|
|
|
+ ("question_mark", "help"),
|
|
|
+ ("q", "quit"),
|
|
|
+ ]
|
|
|
+ assert observed["shown"] == expected, observed["shown"]
|
|
|
+ # Help advertises as "?" in the footer.
|
|
|
+ assert app.get_key_display(
|
|
|
+ next(b for b in AssistantTUI.BINDINGS if b.key == "question_mark")
|
|
|
+ ) == "?"
|
|
|
+ # The composed Footer: exactly one "instructions" entry and it is the
|
|
|
+ # right-aligned command-palette key; no left duplicate exists.
|
|
|
+ instructions = [k for k in observed["keys"] if k[2] == "instructions"]
|
|
|
+ assert len(instructions) == 1, observed["keys"]
|
|
|
+ key, key_display, _desc, action, classes = instructions[0]
|
|
|
+ assert action == "command_palette", instructions
|
|
|
+ assert "-command-palette" in classes, instructions
|
|
|
+ assert key == "ctrl+p", instructions
|
|
|
+ non_palette = [k for k in observed["keys"] if "-command-palette" not in k[4]]
|
|
|
+ assert not any(k[2] == "instructions" for k in non_palette), observed["keys"]
|
|
|
+ assert not any(k[3] == "command_palette" for k in non_palette), observed["keys"]
|
|
|
+
|
|
|
+
|
|
|
+def test_ctrl_p_opens_command_palette(server):
|
|
|
+ """ctrl+p still opens the palette; its binding description is 'instructions'."""
|
|
|
+ pytest.importorskip("textual")
|
|
|
+ from textual.command import CommandPalette
|
|
|
+
|
|
|
+ from stt_daemon.tui.app import AssistantTUI
|
|
|
+
|
|
|
+ path, _service = server
|
|
|
+ app = AssistantTUI(path)
|
|
|
+ observed: dict = {}
|
|
|
+
|
|
|
+ async def scenario():
|
|
|
+ async with app.run_test() as pilot:
|
|
|
+ await pilot.pause()
|
|
|
+ await pilot.press("ctrl+p")
|
|
|
+ await pilot.pause()
|
|
|
+ await pilot.pause()
|
|
|
+ observed["is_palette"] = isinstance(app.screen, CommandPalette)
|
|
|
+ observed["stack"] = [type(s).__name__ for s in app.screen_stack]
|
|
|
+
|
|
|
+ asyncio.run(scenario())
|
|
|
+ palette_binding = next(b for b in AssistantTUI.BINDINGS if b.key == "ctrl+p")
|
|
|
+ assert palette_binding.description == "instructions"
|
|
|
+ assert observed["is_palette"], "ctrl+p did not open the command palette: %s" % (
|
|
|
+ observed["stack"],
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def test_question_mark_toggles_help_panel(server):
|
|
|
+ """? toggles the help panel and is advertised in the left footer list."""
|
|
|
+ pytest.importorskip("textual")
|
|
|
+ from textual.widgets import HelpPanel
|
|
|
+
|
|
|
+ from stt_daemon.tui.app import AssistantTUI
|
|
|
+
|
|
|
+ path, _service = server
|
|
|
+ app = AssistantTUI(path)
|
|
|
+ observed: dict = {}
|
|
|
+
|
|
|
+ async def scenario():
|
|
|
+ async with app.run_test() as pilot:
|
|
|
+ await pilot.pause()
|
|
|
+ assert list(app.screen.query(HelpPanel)) == []
|
|
|
+ await pilot.press("?")
|
|
|
+ await pilot.pause()
|
|
|
+ observed["shown"] = list(app.screen.query(HelpPanel)) != []
|
|
|
+ await pilot.press("?")
|
|
|
+ await pilot.pause()
|
|
|
+ observed["hidden"] = list(app.screen.query(HelpPanel)) == []
|
|
|
+
|
|
|
+ asyncio.run(scenario())
|
|
|
+ assert observed["shown"] is True, "? did not mount HelpPanel"
|
|
|
+ assert observed["hidden"] is True, "second ? did not remove HelpPanel"
|
|
|
+
|
|
|
+
|
|
|
+def test_plain_m_maximizes_triage_pane(server):
|
|
|
+ pytest.importorskip("textual")
|
|
|
+
|
|
|
+ from stt_daemon.tui.app import AssistantTUI
|
|
|
+
|
|
|
+ from textual.widgets import RichLog
|
|
|
+
|
|
|
+ path, _service = server
|
|
|
+ app = AssistantTUI(path, triage_enabled=True)
|
|
|
+
|
|
|
+ async def scenario():
|
|
|
+ async with app.run_test(size=(80, 40)) as pilot:
|
|
|
+ await pilot.pause()
|
|
|
+ app.query_one("#triage", RichLog).focus()
|
|
|
+ await pilot.pause()
|
|
|
+ await pilot.press("m")
|
|
|
+ await pilot.pause()
|
|
|
+ assert app.screen.maximized is app.query_one("#main")
|
|
|
+ await pilot.press("m")
|
|
|
+ await pilot.pause()
|
|
|
+ assert app.screen.maximized is None
|
|
|
+ assert app.query_one("#triage", RichLog).display is True
|
|
|
+ assert app.query_one("#chat", RichLog).display is True
|
|
|
+
|
|
|
+ asyncio.run(scenario())
|
|
|
+
|
|
|
+
|
|
|
+def test_screenshot_saves_svg(server, tmp_path, monkeypatch):
|
|
|
+ pytest.importorskip("textual")
|
|
|
+ import glob
|
|
|
+
|
|
|
+ from stt_daemon.tui.app import AssistantTUI
|
|
|
+
|
|
|
+ monkeypatch.chdir(tmp_path)
|
|
|
+ path, _service = server
|
|
|
+ app = AssistantTUI(path)
|
|
|
+
|
|
|
+ async def scenario():
|
|
|
+ async with app.run_test() as pilot:
|
|
|
+ await pilot.pause()
|
|
|
+ await pilot.press("ctrl+s")
|
|
|
+ await pilot.pause()
|
|
|
+ await pilot.pause()
|
|
|
+
|
|
|
+ asyncio.run(scenario())
|
|
|
+ files = glob.glob(str(tmp_path / "stt-tui-*.svg"))
|
|
|
+ assert files, "no stt-tui-*.svg found in %s" % tmp_path
|
|
|
+ assert os.path.getsize(files[0]) > 0
|