"""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"") 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): 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}) 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 - it did for the dialog and again for the progress bar.""" 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()