"""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(["
" + "wort " * 2000 + "
"], 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)