"""Browser checks for the dialogs. pytest alone cannot catch CSS mistakes: a "#quick-dialog { display: flex }" overrides the browser's display:none for a closed and leaves it stuck on screen. Skipped when playwright or its browsers are missing. """ import socket import subprocess import sys import time from pathlib import Path import pytest sync_api = pytest.importorskip("playwright.sync_api", reason="playwright not installed") def _free_port() -> int: with socket.socket() as s: s.bind(("127.0.0.1", 0)) return s.getsockname()[1] @pytest.fixture(scope="module") def server(tmp_path_factory): tmp = tmp_path_factory.mktemp("ui") downloads = tmp / "downloads" (downloads / "Eine Folge").mkdir(parents=True) for i in (1, 2): (downloads / "Eine Folge" / f"{i:02d}.mp3").write_bytes(b"") # one book in three formats, the case the format column exists for for ext in (".epub", ".azw3", ".pdf"): (downloads / f"Der Lehrer{ext}").write_bytes(b"x" * 2048) port = _free_port() proc = subprocess.Popen( [sys.executable, "-m", "uvicorn", "wordarr.main:app", "--port", str(port)], cwd=Path(__file__).resolve().parent.parent, env={"PATH": "/usr/bin:/bin", "WORDARR_CONFIG_DIR": str(tmp / "config"), "WORDARR_DOWNLOAD_DIR": str(downloads)}, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) url = f"http://127.0.0.1:{port}" for _ in range(100): if proc.poll() is not None: pytest.skip("uvicorn did not start") try: with socket.create_connection(("127.0.0.1", port), timeout=0.2): break except OSError: time.sleep(0.1) else: proc.kill() pytest.skip("server did not come up") yield url proc.kill() @pytest.fixture def page(server): """A German browser, so the UI language is the same everywhere.""" with sync_api.sync_playwright() as pw: try: browser = pw.chromium.launch() except Exception as exc: # browsers not downloaded pytest.skip(f"no chromium: {exc}") page = browser.new_page(viewport={"width": 1200, "height": 900}, locale="de-DE") errors = [] page.on("pageerror", lambda e: errors.append(str(e))) page.goto(server, wait_until="networkidle") yield page assert not errors, errors browser.close() def test_dialogs_start_closed(page): for dialog in ("#quick-dialog", "#series-dialog", "#manual-dialog"): assert not page.locator(dialog).is_visible(), dialog # the import dialog lives inside the import section, so it can only be seen # once that tab is open - a reload would not have hidden a broken one page.click("nav button[data-view=import]") assert not page.locator("#quick-dialog").is_visible() def test_hidden_elements_really_are_hidden(page): """A CSS display rule can override the hidden attribute.""" page.click("nav button[data-view=import]") for sel in ("#import-progress", "#import-btn", "#import-table", "#missing-badge"): assert not page.locator(sel).is_visible(), sel def test_import_dialog_opens_and_closes(page): page.click("nav button[data-view=import]") page.click("#scan-btn") page.wait_for_selector("[data-quick]") page.click("[data-quick='0']") page.wait_for_selector("#quick-dialog[open]") assert page.locator("#quick-dialog").is_visible() # everything reachable, nothing cut off at the bottom for sel in ("#quick-m-submit", "#quick-close"): box = page.locator(sel).bounding_box() assert box["y"] + box["height"] <= 900, sel page.click("#quick-close") page.wait_for_timeout(150) assert not page.locator("#quick-dialog").is_visible() page.click("[data-quick='0']") page.wait_for_selector("#quick-dialog[open]") page.keyboard.press("Escape") page.wait_for_timeout(150) assert not page.locator("#quick-dialog").is_visible() def test_format_tags_and_the_format_filter(page): page.click("nav button[data-view=import]") page.click("#scan-btn") page.wait_for_selector("[data-quick]") tags = page.locator("#import-table .tag.fmt").all_text_contents() assert "azw3 · 2,0 KB" in tags assert "mp3" in " ".join(tags) # the audiobook folder is labelled too page.select_option("#import-filter-format", "epub") rows = page.locator("#import-table tbody tr") assert rows.count() == 1 assert "epub" in rows.first.locator(".tag.fmt").text_content() def test_the_browser_language_decides_and_the_toggle_switches(page, server): assert page.locator("nav button[data-view=search]").text_content() == "Suche" assert page.locator("#lang-toggle").text_content() == "EN" page.click("#lang-toggle") assert page.locator("nav button[data-view=search]").text_content() == "Search" assert page.locator("#lang-toggle").text_content() == "DE" assert page.get_attribute("html", "lang") == "en" assert page.get_attribute("#search-q", "placeholder") == "Title, author or ISBN…" page.reload(wait_until="networkidle") # the choice outlives the reload assert page.locator("nav button[data-view=search]").text_content() == "Search" def test_english_reaches_the_rendered_rows(page): page.click("#lang-toggle") page.click("nav button[data-view=import]") page.click("#scan-btn") page.wait_for_selector("[data-quick]") assert "— skip —" in page.locator("#import-table tbody select").first.text_content() assert "2.0 KB" in " ".join(page.locator("#import-table .tag.fmt").all_text_contents()) # switching back re-renders the table that is already on screen page.click("#lang-toggle") assert "— überspringen —" in page.locator("#import-table tbody select").first.text_content() assert "2,0 KB" in " ".join(page.locator("#import-table .tag.fmt").all_text_contents()) def test_the_header_shows_the_running_version(page): stamp = page.locator("#app-version").text_content() assert stamp.startswith("v") assert len(stamp) > 3 # version plus the build stamp behind it