Files
2026-10-02 15:29:35 +02:00

237 lines
8.2 KiB
Python

"""Headless smoke tests for the TUI (uses Textual's test driver, no terminal)."""
from __future__ import annotations
import asyncio
import pytest
from rich.table import Table
from textual.widgets import ListView, Static
from sjaur.tui.app import InstallerConfirmScreen, SJaurApp
def pytest_fail(message: str):
pytest.fail(message)
def run(coro):
return asyncio.run(coro)
def test_tui_shows_popular_results_and_live_search(engine):
async def scenario():
app = SJaurApp(engine=engine, cache=None, cache_note="test")
async with app.run_test(size=(120, 36)) as pilot:
await pilot.pause()
list_view = app.query_one("#results", ListView)
initial_count = len(list_view.children)
assert initial_count > 0 # empty input shows the most-voted packages
# Type a query; results update live.
await pilot.click("#search")
for char in "firefox":
await pilot.press(char)
await pilot.pause(1.0) # debounce + background search worker
current = [r.package.name for r in app._current_results]
assert "firefox" in current
assert len(current) < initial_count
run(scenario())
def test_tui_details_pane_follows_selection(engine):
async def scenario():
app = SJaurApp(engine=engine, cache=None, cache_note="test")
async with app.run_test(size=(120, 36)) as pilot:
await pilot.pause()
details = app.query_one("#details", Static)
renderable = details.content
# The first popular package's details are rendered on startup.
assert isinstance(renderable, Table)
# Highlight another row; the detail pane follows.
list_view = app.query_one("#results", ListView)
list_view.index = 2
await pilot.pause()
assert app.query_one("#details", Static).content is not renderable
run(scenario())
def test_tui_updates_results_on_every_keystroke(engine):
async def scenario():
app = SJaurApp(engine=engine, cache=None, cache_note="test")
async with app.run_test(size=(120, 36)) as pilot:
await pilot.pause()
popular = [r.package.name for r in app._current_results]
await pilot.click("#search")
await pilot.press("f", "z", "f")
await pilot.pause(0.05) # less than the debounce: still instant feedback
live = [r.package.name for r in app._current_results]
assert set(live) < set(popular) # narrowed immediately, not after enter
await pilot.pause(1.0) # background search refines further
assert "fzf" in [r.package.name for r in app._current_results]
assert "fzf" in app._results_cache # finished search is cached
run(scenario())
def test_tui_cached_results_are_restored_instantly(engine):
async def scenario():
app = SJaurApp(engine=engine, cache=None, cache_note="test")
async with app.run_test(size=(120, 36)) as pilot:
await pilot.pause()
await pilot.click("#search")
for _ in range(3):
await pilot.press("f", "z", "f")
await pilot.pause(1.0)
fzf_results = [r.package.name for r in app._current_results]
# Clear and retype: results come back from the cache.
await pilot.press("backspace", "backspace", "backspace")
await pilot.pause(1.0)
assert app._current_results # popular list restored
await pilot.press("f", "z", "f")
await pilot.pause(0.05)
assert [r.package.name for r in app._current_results] == fzf_results
run(scenario())
def test_tui_install_flow_is_requested_for_selected_package(engine):
async def scenario():
app = SJaurApp(engine=engine, cache=None, cache_note="test")
async with app.run_test(size=(120, 36)) as pilot:
await pilot.pause()
calls = []
app._install_flow = lambda package, refresh: calls.append(
(package.name, refresh)
)
# Highlight a package and open the install dialog.
app.action_focus_results()
await pilot.pause()
await pilot.press("ctrl+i")
await pilot.pause()
assert isinstance(app.screen, InstallerConfirmScreen)
# 'n' cancels: nothing is installed.
await pilot.press("n")
await pilot.pause()
assert calls == []
# Re-open and confirm with 'y'.
await pilot.press("ctrl+i")
await pilot.pause()
await pilot.press("y")
await pilot.pause()
assert len(calls) == 1
assert calls[0][1] is False
# 'u' confirms with a refresh (update the clone first).
await pilot.press("ctrl+i")
await pilot.pause()
await pilot.press("u")
await pilot.pause()
assert calls[-1][1] is True
# The plain 'i' key (results focused) opens the dialog as well.
await pilot.press("escape")
await pilot.pause()
app.action_focus_results()
await pilot.pause()
await pilot.press("i")
await pilot.pause()
assert isinstance(app.screen, InstallerConfirmScreen)
run(scenario())
def test_tui_install_reports_environment_problems(engine, monkeypatch):
async def scenario():
import sjaur.tui.app as app_module
monkeypatch.setattr(
app_module,
"check_environment",
lambda **kwargs: ["'git' not found in PATH — required"],
)
app = SJaurApp(engine=engine, cache=None, cache_note="test")
async with app.run_test(size=(120, 36)) as pilot:
await pilot.pause()
app._install_flow = lambda package, refresh: pytest_fail("flow must not run")
app.action_focus_results()
await pilot.pause()
await pilot.press("ctrl+i")
await pilot.pause()
await pilot.press("y")
await pilot.pause()
assert "git" in str(app.query_one("#status", Static).content)
run(scenario())
def test_tui_install_flow_error_is_reported(engine, monkeypatch):
async def scenario():
app = SJaurApp(engine=engine, cache=None, cache_note="test")
async with app.run_test(size=(120, 36)) as pilot:
await pilot.pause()
def failing_flow(package, refresh):
from sjaur.errors import SjaurError
raise SjaurError("makepkg failed with exit code 4")
app._install_flow = failing_flow
app._install_package(engine.packages[0], refresh=False)
await pilot.pause()
# The report goes to the suspended terminal, so no status error:
# the app must simply survive and stay usable.
assert app._search_input.has_focus
run(scenario())
def test_tui_escape_clears_search(engine):
async def scenario():
app = SJaurApp(engine=engine, cache=None, cache_note="test")
async with app.run_test(size=(120, 36)) as pilot:
await pilot.pause()
await pilot.click("#search")
for char in "fzf":
await pilot.press(char)
await pilot.pause(1.0)
assert app._current_results
await pilot.press("escape")
await pilot.pause(1.0)
assert app._search_input.value == ""
run(scenario())
def test_tui_enter_expands_details_and_escape_returns(engine):
async def scenario():
app = SJaurApp(engine=engine, cache=None, cache_note="test")
async with app.run_test(size=(120, 36)) as pilot:
await pilot.pause()
content = app.query_one("#content")
# Focus the results and open the selected package.
app.action_focus_results()
await pilot.pause()
await pilot.press("enter")
await pilot.pause()
assert content.has_class("expanded")
await pilot.press("escape")
await pilot.pause()
assert not content.has_class("expanded")
run(scenario())