diora-web/tui/tests/test_library_screen.py
mrwnslz 695037fb9c TUI: Bibliothek wie im Web (Weiterlesen-Karte, Status- und Ordner-Chips, Suche, Sortierung, Länge)
library.py hält die Regeln der Web-Bibliothek (53-library.js) als reine Funktionen:
Status Angefangen/Ungelesen/Gelesen, Ordner (mehrere je Buch, Reihenfolge aus den
verschlüsselten Bibliothekseinstellungen, "Ohne Ordner"), Suche ohne Akzente, sechs
Sortierungen mit natürlicher Zahlenordnung, Weiterlesen-Karte aus den zuletzt gelesenen,
Bücher/Zeitschriften. library_screen.py zeigt sie an (Tasten 1-4, o, /, s, m, c; Chips und
Karte klickbar). Metadaten (Ordner, Seitenzahl, Zeitschrift) und die Ordnerreihenfolge
kommen beim Abgleich; fehlt die Länge, rechnet die TUI sie für geladene Bücher aus dem Text
(1800 Zeichen je Seite). crypto.encrypt für Stufe 3. 54 Tests, Smoke prüft Ordner/Länge/Prefs
gegen den lokalen Server.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:18:49 +01:00

148 lines
5.9 KiB
Python

"""The library screen: chips, search, sort, folders, section, the Weiterlesen card."""
import asyncio
import pytest
from textual.widgets import Input, ListView
from diora_tui import library_meta, progress
from diora_tui.app import DioraTuiApp
from diora_tui.library_screen import ChoiceScreen, LibraryScreen
from diora_tui.reader_screen import ReaderScreen
T = 1_760_000_000_000
def entry(i, title, **kw):
return {"server_id": i, "title": title, "author": kw.pop("author", ""), "is_read": kw.pop("is_read", False),
"uploaded_at": f"2026-0{i}-01", "folders": kw.pop("folders", []), "pages": kw.pop("pages", 0), **kw}
@pytest.fixture
def world(tmp_path, monkeypatch):
store = progress.ProgressStore(tmp_path / "p.json")
monkeypatch.setattr(progress, "_default", store)
now = progress.now_ms()
library_meta.set_remote({
"s1": entry(1, "Das Kapital", author="Marx", folders=["Politik"], pages=900),
"s2": entry(2, "Zauberberg", author="Mann", folders=["Roman"], pages=700),
"s3": entry(3, "Ödipus", is_read=True, pages=90),
"s4": entry(4, "Jacobin 24", folders=["Zeitschriften"], pages=80),
})
library_meta.set_extra("folder_order", ["Roman", "Politik"])
library_meta.set_extra("sort", "recent")
store.apply_server("s2", {"position_anchor": "5:0.5", "scroll_fraction": 0.5, "read_at": now - 120_000, "device": "Handy"})
store.apply_server("s1", {"position_anchor": "5:0.5", "scroll_fraction": 0.2, "read_at": now - 3_600_000 * 30})
lib = tmp_path / "lib"
lib.mkdir()
yield lib
library_meta.set_remote({})
library_meta.set_extra("folder_order", [])
library_meta.set_extra("sort", "recent")
def run(lib, scenario):
async def go():
app = DioraTuiApp(lib)
async with app.run_test(size=(120, 40)) as pilot:
await pilot.pause(0.3)
await scenario(app, app.screen, pilot)
asyncio.run(go())
def titles(screen):
return [r.title for r in screen.shown]
def text_of(screen, selector):
return str(screen.query_one(selector).render())
def test_card_chips_and_list(world):
async def scenario(app, screen, pilot):
assert screen.going.title == "Zauberberg"
assert "Zauberberg" not in titles(screen) # once, on top
assert titles(screen) == ["Das Kapital", "Ödipus"] # read last first, the rest by upload (never opened last)
card = text_of(screen, "#lib-card")
assert "Zauberberg" in card and "50 %" in card and "Seite 350 von 700" in card and "vor 2 Minuten" in card and "Handy" in card
chips = text_of(screen, "#lib-chips")
assert "Angefangen 2" in chips and "Ungelesen 0" in chips and "Gelesen 1" in chips
assert "Roman 1" in chips and "Politik 1" in chips and chips.index("Roman") < chips.index("Politik") # the user's order
assert "Jacobin" not in " ".join(titles(screen)) # a magazine: the other section
run(world, scenario)
def test_status_search_and_reset(world):
async def scenario(app, screen, pilot):
await pilot.press("2")
assert titles(screen) == ["Zauberberg", "Das Kapital"] and screen.going is None # no card while filtered
await pilot.press("2") # the same chip again: off
assert screen.status == "all"
await pilot.press("4")
assert titles(screen) == ["Ödipus"]
await pilot.press("escape")
assert screen.status == "all"
await pilot.press("/")
assert screen.query_one(Input).has_focus
await pilot.press(*"mann") # typing digits or letters is not a shortcut while the box has focus
assert titles(screen) == ["Zauberberg"]
await pilot.press("escape")
assert screen.q == "" and not screen.query_one(Input).display and len(screen.shown) == 2
run(world, scenario)
def test_sort_and_folder_pickers(world):
async def scenario(app, screen, pilot):
await pilot.press("s")
assert isinstance(app.screen, ChoiceScreen)
await pilot.press("down", "down", "enter") # Autor
assert screen.sort == "author"
assert library_meta.get_extra("sort") == screen.sort
await pilot.press("s", "end", "enter") # Längste zuerst
assert screen.sort == "long"
assert titles(screen)[0] == "Das Kapital"
assert "· 900 S." in str(screen.query_one(ListView).query("Label").first().render())
await pilot.press("o", "down", "enter") # first folder in the user's order: Roman
assert screen.folder == "Roman" and screen.going is None
assert titles(screen) == ["Zauberberg"] # filtered: the card gives way, the book is in the list
await pilot.press("o", "home", "enter") # Alle Ordner
assert screen.folder is None
run(world, scenario)
def test_sections_and_continue_key(world, monkeypatch):
async def scenario(app, screen, pilot):
await pilot.press("m")
assert screen.magazines and titles(screen) == ["Jacobin 24"]
await pilot.press("m")
assert not screen.magazines
opened = []
monkeypatch.setattr(app, "push_screen", lambda s, *a, **k: opened.append(s))
await pilot.press("c")
assert len(opened) == 0 # the book has no file here and no login: it says so instead
assert not isinstance(app.screen, ReaderScreen)
run(world, scenario)
def test_lengths_are_worked_out_for_downloaded_books_when_sorting_by_length(world, epub):
path = epub(["<p>" + "wort " * 2000 + "</p>"], title="Lokal") # ~10000 chars: 6 pages
path.rename(world / "lokal.epub")
async def scenario(app, screen, pilot):
await pilot.press("s", "end", "enter")
for _ in range(60):
await pilot.pause(0.1)
if library_meta.local_pages():
break
await pilot.pause(0.5)
row = next(r for r in screen.rows if r.title == "lokal")
assert library_meta.local_pages() == {row.key: 6} and row.pages == 6
run(world, scenario)