fix(ui/ux): only lay out the import dialog while it is open
This commit is contained in:
+4
-1
@@ -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; }
|
.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
|
/* 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 */
|
"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-scroll { flex: 1 1 auto; min-height: 4rem; overflow-y: auto; margin-top: 0.6rem; }
|
||||||
#quick-results { margin: 0; }
|
#quick-results { margin: 0; }
|
||||||
#quick-foot { flex: none; border-top: 1px solid var(--border); margin-top: 0.6rem; padding-top: 0.6rem; }
|
#quick-foot { flex: none; border-top: 1px solid var(--border); margin-top: 0.6rem; padding-top: 0.6rem; }
|
||||||
|
|||||||
@@ -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 <dialog> 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()
|
||||||
Reference in New Issue
Block a user