156 lines
5.9 KiB
Python
156 lines
5.9 KiB
Python
"""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"")
|
|
# 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())
|