From 87b139e236341c55b221cbaa3407dafeba997ca2 Mon Sep 17 00:00:00 2001 From: Steppenstreuner Date: Sat, 29 Aug 2026 05:10:52 +0200 Subject: [PATCH] fix(ui/ux): only lay out the import dialog while it is open --- static/style.css | 5 +- tests/test_ui_dialogs.py | 101 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 tests/test_ui_dialogs.py diff --git a/static/style.css b/static/style.css index 2f6735d..2285d4e 100644 --- a/static/style.css +++ b/static/style.css @@ -107,7 +107,10 @@ dialog label { display: flex; flex-direction: column; gap: 0.2rem; margin: 0.5re .series-list .gap { padding: 0.35rem 0.6rem; color: var(--muted); font-size: 0.85rem; } /* the dialog keeps head and foot in place; only the results scroll, so the "manuell anlegen" panel stays reachable no matter how many hits came back */ -#quick-dialog { display: flex; flex-direction: column; max-height: 85vh; width: min(680px, 92vw); } +/* [open] matters: a bare "#quick-dialog { display: flex }" would override the + browser's display:none for a closed dialog and leave it stuck on screen */ +#quick-dialog[open] { display: flex; flex-direction: column; } +#quick-dialog { max-height: 85vh; width: min(680px, 92vw); } #quick-scroll { flex: 1 1 auto; min-height: 4rem; overflow-y: auto; margin-top: 0.6rem; } #quick-results { margin: 0; } #quick-foot { flex: none; border-top: 1px solid var(--border); margin-top: 0.6rem; padding-top: 0.6rem; } diff --git a/tests/test_ui_dialogs.py b/tests/test_ui_dialogs.py new file mode 100644 index 0000000..0df4e45 --- /dev/null +++ b/tests/test_ui_dialogs.py @@ -0,0 +1,101 @@ +"""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_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()