Track 1.1 of master plan: expand from sexp-only tests to cover DTOs, HTTP signatures, HMAC auth, URL utilities, Jinja filters, calendar helpers, config freeze, activity bus registry, parse utilities, sexp helpers, error classes, and jinja bridge render API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
167 lines
6.0 KiB
Python
167 lines
6.0 KiB
Python
"""Tests for the render() function and component loading in jinja_bridge.
|
|
|
|
These test functionality added in recent commits (render() API,
|
|
load_sexp_dir, snake→kebab conversion) that isn't covered by the existing
|
|
shared/sexp/tests/test_jinja_bridge.py.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
|
|
import pytest
|
|
|
|
from shared.sexp.jinja_bridge import (
|
|
render,
|
|
register_components,
|
|
load_sexp_dir,
|
|
_COMPONENT_ENV,
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clean_env():
|
|
"""Clear component env before each test."""
|
|
_COMPONENT_ENV.clear()
|
|
yield
|
|
_COMPONENT_ENV.clear()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# render() — call component by name with Python kwargs
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestRender:
|
|
def test_basic_render(self):
|
|
register_components('(defcomp ~badge (&key label) (span :class "badge" label))')
|
|
html = render("badge", label="New")
|
|
assert html == '<span class="badge">New</span>'
|
|
|
|
def test_tilde_prefix_optional(self):
|
|
register_components('(defcomp ~pill (&key text) (em text))')
|
|
# Both forms should work
|
|
assert render("pill", text="Hi") == render("~pill", text="Hi")
|
|
|
|
def test_snake_to_kebab_conversion(self):
|
|
"""Python snake_case kwargs should map to sexp kebab-case params."""
|
|
register_components('''
|
|
(defcomp ~card (&key nav-html link-href)
|
|
(div :class "card" (a :href link-href nav-html)))
|
|
''')
|
|
html = render("card", nav_html="Nav", link_href="/about")
|
|
assert 'href="/about"' in html
|
|
assert "Nav" in html
|
|
|
|
def test_multiple_kwargs(self):
|
|
register_components('''
|
|
(defcomp ~item (&key title price image-url)
|
|
(div (h3 title) (span price) (img :src image-url)))
|
|
''')
|
|
html = render("item", title="Widget", price="£10", image_url="/img/w.jpg")
|
|
assert "Widget" in html
|
|
assert "£10" in html
|
|
assert 'src="/img/w.jpg"' in html
|
|
|
|
def test_unknown_component_raises(self):
|
|
with pytest.raises(ValueError, match="Unknown component"):
|
|
render("nonexistent", label="x")
|
|
|
|
def test_empty_kwargs(self):
|
|
register_components('(defcomp ~empty () (hr))')
|
|
html = render("empty")
|
|
assert html == "<hr>"
|
|
|
|
def test_html_escaping_in_values(self):
|
|
register_components('(defcomp ~safe (&key text) (p text))')
|
|
html = render("safe", text='<script>alert("xss")</script>')
|
|
assert "<script>" not in html
|
|
assert "<script>" in html
|
|
|
|
def test_boolean_false_value(self):
|
|
register_components('''
|
|
(defcomp ~toggle (&key active)
|
|
(when active (span "ON")))
|
|
''')
|
|
html = render("toggle", active=False)
|
|
assert "ON" not in html
|
|
|
|
def test_boolean_true_value(self):
|
|
register_components('''
|
|
(defcomp ~toggle (&key active)
|
|
(when active (span "ON")))
|
|
''')
|
|
html = render("toggle", active=True)
|
|
assert "ON" in html
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# load_sexp_dir
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestLoadSexpDir:
|
|
def test_loads_sexp_files(self):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
# Write a .sexp file
|
|
with open(os.path.join(tmpdir, "components.sexp"), "w") as f:
|
|
f.write('(defcomp ~test-comp (&key msg) (div msg))')
|
|
|
|
load_sexp_dir(tmpdir)
|
|
html = render("test-comp", msg="loaded!")
|
|
assert html == "<div>loaded!</div>"
|
|
|
|
def test_loads_sexpr_files(self):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
with open(os.path.join(tmpdir, "nav.sexpr"), "w") as f:
|
|
f.write('(defcomp ~nav-item (&key href label) (a :href href label))')
|
|
|
|
load_sexp_dir(tmpdir)
|
|
html = render("nav-item", href="/about", label="About")
|
|
assert 'href="/about"' in html
|
|
|
|
def test_loads_multiple_files(self):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
with open(os.path.join(tmpdir, "a.sexp"), "w") as f:
|
|
f.write('(defcomp ~comp-a (&key x) (b x))')
|
|
with open(os.path.join(tmpdir, "b.sexp"), "w") as f:
|
|
f.write('(defcomp ~comp-b (&key y) (i y))')
|
|
|
|
load_sexp_dir(tmpdir)
|
|
assert render("comp-a", x="A") == "<b>A</b>"
|
|
assert render("comp-b", y="B") == "<i>B</i>"
|
|
|
|
def test_empty_directory(self):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
load_sexp_dir(tmpdir) # should not raise
|
|
|
|
def test_ignores_non_sexp_files(self):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
with open(os.path.join(tmpdir, "readme.txt"), "w") as f:
|
|
f.write("not a sexp file")
|
|
with open(os.path.join(tmpdir, "comp.sexp"), "w") as f:
|
|
f.write('(defcomp ~real (&key v) (span v))')
|
|
|
|
load_sexp_dir(tmpdir)
|
|
assert "~real" in _COMPONENT_ENV
|
|
# txt file should not have been loaded
|
|
assert len([k for k in _COMPONENT_ENV if k.startswith("~")]) == 1
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# register_components — multiple definitions in one source
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestRegisterComponents:
|
|
def test_multiple_in_one_source(self):
|
|
register_components('''
|
|
(defcomp ~a (&key x) (b x))
|
|
(defcomp ~b (&key y) (i y))
|
|
''')
|
|
assert "~a" in _COMPONENT_ENV
|
|
assert "~b" in _COMPONENT_ENV
|
|
|
|
def test_overwrite_existing(self):
|
|
register_components('(defcomp ~ow (&key x) (b x))')
|
|
assert render("ow", x="v1") == "<b>v1</b>"
|
|
register_components('(defcomp ~ow (&key x) (i x))')
|
|
assert render("ow", x="v2") == "<i>v2</i>"
|