149 lines
5.9 KiB
Python
149 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)
|