server-up/tests/test_ui_logica.py
Ramon 072fdff3a2
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 13m11s
v0.8.40-beta - Filter in de app store is een menu geworden
- Twintig categorieknoppen en veertien labelknoppen stonden altijd uitgeklapt
  boven de store; bij 256 apps vulde dat een half scherm. Nu een knop Filter
  met een popover: categorieen in een raster van twee kolommen met kleur en
  aantal, labels met een eigen zoekveld, en onderaan het aantal resultaten met
  een wisknop. Sluit met Escape of door ernaast te klikken
- De labelfilter werkte maar half: de rij was afgekapt op veertien terwijl er
  297 labels zijn, dus 283 daarvan waren niet te kiezen. Het menu zoekt nu in
  de volledige lijst; zonder zoekterm de veertig meest gebruikte, en een gekozen
  label blijft zichtbaar ook als het zeldzaam is
- Actieve filters staan als chips naast de knop, met een kruisje per stuk. Het
  zoekveld heeft een wisknop
- Nieuwe test die het hele sjabloon op gebalanceerde HTML-tags controleert. Een
  verkeerd geplaatste </div> schuift de rest van de pagina in een andere tak en
  dat merk je anders pas in de browser
- Nieuwe JS-tests voor de filterlogica: filteren op categorie en label, de
  teller, wissen, en zoeken naar een zeldzaam label

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9eqpADJSRs49SoGGr4NAy
2026-08-01 08:39:36 +02:00

95 lines
3.8 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]]