commit local changes.
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
"""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())
|
||||
Reference in New Issue
Block a user