All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 13m11s
- 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
95 lines
3.8 KiB
Python
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]]
|