server-up/tests/test_ui_logica.py
Ramon 6c84def759
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 4m46s
fix: herstel app verwijderen en mobiele modals
2026-08-04 18:50:16 +02:00

104 lines
4.2 KiB
Python

"""De logica van het invulmenu, uitgevoerd zoals de browser het doet.
De stapindeling, de geavanceerd-schakelaar en het blokkeren van 'Volgende'
zitten in JavaScript. Een Python-test kan daar niets over zeggen, dus draaien we
de echte component uit index.html in Node. Zonder Node slaat dit zichzelf over.
"""
import shutil
import subprocess
from pathlib import Path
import pytest
WORTEL = Path(__file__).resolve().parent.parent
SCRIPT = WORTEL / "tests" / "js" / "menu.test.js"
@pytest.mark.skipif(not shutil.which("node"), reason="node ontbreekt")
def test_invulmenu_logica():
r = subprocess.run(["node", str(SCRIPT)], capture_output=True, text=True,
timeout=60, cwd=str(WORTEL))
assert r.returncode == 0, r.stdout + r.stderr
@pytest.mark.skipif(not shutil.which("node"), reason="node ontbreekt")
def test_installatiemodal_in_een_echte_dom():
"""Rendert de modal met Alpine en klikt op Volgende.
Losse logica zegt niets over de binding: `:disabled="lijst.length"` gaf bij
een lege lijst 0, en Alpine haalt een boolean-attribuut alleen weg bij
null, undefined of false — dus de knop stond juist uit.
"""
r = subprocess.run(["node", str(WORTEL / "tests" / "js" / "modal.dom.test.js")],
capture_output=True, text=True, timeout=90, cwd=str(WORTEL))
if r.returncode == 2:
pytest.skip(r.stdout.strip() or "jsdom ontbreekt")
assert r.returncode == 0, r.stdout + r.stderr
@pytest.mark.skipif(not shutil.which("node"), reason="node ontbreekt")
def test_javascript_van_de_pagina_parseert():
"""Een typefout in de component maakt de hele interface blanco; dat wil je
niet pas in de browser ontdekken."""
import re
html = (WORTEL / "server-up" / "templates" / "index.html").read_text(encoding="utf-8")
blokken = re.findall(r"<script[^>]*>(.*?)</script>", html, re.S)
grootste = max(blokken, key=len)
r = subprocess.run(["node", "--check", "-"], input=grootste,
capture_output=True, text=True, timeout=30)
assert r.returncode == 0, r.stderr
def test_de_html_van_de_pagina_is_gebalanceerd():
"""Een tag die niet dichtgaat maakt de pagina stil kapot.
De hele interface is één sjabloon van ruim drieduizend regels met Alpine-
templates die in elkaar zitten. Zet je bij het bewerken één `</div>`
verkeerd, dan schuift de rest van de pagina in de verkeerde tak en zie je
dat pas in de browser — als je die kant op klikt.
"""
from html.parser import HTMLParser
# Tags die geen sluiting hebben; svg-vormen zitten erbij omdat de iconen
# als inline svg meekomen.
LEEG = {"input", "br", "img", "hr", "meta", "link", "source", "area", "col",
"embed", "track", "wbr", "path", "circle", "rect", "line",
"polyline", "polygon", "use"}
class Balans(HTMLParser):
def __init__(self):
super().__init__()
self.stapel = []
self.fouten = []
def handle_starttag(self, tag, attrs):
if tag not in LEEG:
self.stapel.append((tag, self.getpos()[0]))
def handle_endtag(self, tag):
if tag in LEEG:
return
if not self.stapel:
self.fouten.append(f"regel {self.getpos()[0]}: </{tag}> zonder opening")
elif self.stapel[-1][0] != tag:
open_tag, regel = self.stapel[-1]
self.fouten.append(
f"regel {self.getpos()[0]}: </{tag}> sluit <{open_tag}> "
f"van regel {regel} af")
else:
self.stapel.pop()
p = Balans()
p.feed((WORTEL / "server-up" / "templates" / "index.html")
.read_text(encoding="utf-8"))
assert not p.fouten, p.fouten[:5]
assert not p.stapel, [f"<{t}> op regel {r} gaat nooit dicht" for t, r in p.stapel[:5]]
def test_modals_blijven_binnen_het_mobiele_scherm_en_scrollbaar():
html = (WORTEL / "server-up" / "templates" / "index.html").read_text(
encoding="utf-8")
assert "height: 100dvh" in html
assert "max-height: 100dvh" in html
assert ".modal-body" in html and "overflow-y-auto" in html
assert "-webkit-overflow-scrolling: touch" in html