All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 11m37s
Rename all sexp directories, files, identifiers, and references to sx. artdag/ excluded (separate media processing DSL). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
202 lines
6.7 KiB
Python
202 lines
6.7 KiB
Python
"""Tests for the Jinja ↔ s-expression bridge."""
|
|
|
|
import asyncio
|
|
|
|
from shared.sx.jinja_bridge import (
|
|
sx,
|
|
sx_async,
|
|
register_components,
|
|
get_component_env,
|
|
_COMPONENT_ENV,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def run(coro):
|
|
return asyncio.run(coro)
|
|
|
|
|
|
def setup_function():
|
|
"""Clear component env before each test."""
|
|
_COMPONENT_ENV.clear()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# sx() — synchronous rendering
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestSx:
|
|
def test_simple_html(self):
|
|
assert sx('(div "Hello")') == "<div>Hello</div>"
|
|
|
|
def test_with_kwargs(self):
|
|
html = sx('(p name)', name="Alice")
|
|
assert html == "<p>Alice</p>"
|
|
|
|
def test_multiple_kwargs(self):
|
|
html = sx('(a :href url title)', url="/about", title="About")
|
|
assert html == '<a href="/about">About</a>'
|
|
|
|
def test_escaping(self):
|
|
html = sx('(p text)', text="<script>alert(1)</script>")
|
|
assert "<script>" in html
|
|
assert "<script>" not in html
|
|
|
|
def test_nested(self):
|
|
html = sx('(div :class "card" (h1 title))', title="Hi")
|
|
assert html == '<div class="card"><h1>Hi</h1></div>'
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# register_components() + sx()
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestComponents:
|
|
def test_register_and_use(self):
|
|
register_components('''
|
|
(defcomp ~badge (&key label)
|
|
(span :class "badge" label))
|
|
''')
|
|
html = sx('(~badge :label "New")')
|
|
assert html == '<span class="badge">New</span>'
|
|
|
|
def test_multiple_components(self):
|
|
register_components('''
|
|
(defcomp ~tag (&key text)
|
|
(span :class "tag" text))
|
|
(defcomp ~pill (&key text)
|
|
(span :class "pill" text))
|
|
''')
|
|
assert '<span class="tag">A</span>' == sx('(~tag :text "A")')
|
|
assert '<span class="pill">B</span>' == sx('(~pill :text "B")')
|
|
|
|
def test_component_with_children(self):
|
|
register_components('''
|
|
(defcomp ~box (&key title &rest children)
|
|
(div :class "box" (h2 title) children))
|
|
''')
|
|
html = sx('(~box :title "Box" (p "Content"))')
|
|
assert '<div class="box">' in html
|
|
assert "<h2>Box</h2>" in html
|
|
assert "<p>Content</p>" in html
|
|
|
|
def test_component_with_kwargs_override(self):
|
|
"""Kwargs passed to sx() are available alongside components."""
|
|
register_components('''
|
|
(defcomp ~greeting (&key name)
|
|
(p (str "Hello " name)))
|
|
''')
|
|
html = sx('(~greeting :name user)', user="Bob")
|
|
assert html == "<p>Hello Bob</p>"
|
|
|
|
def test_component_env_persists(self):
|
|
"""Components registered once are available in subsequent calls."""
|
|
register_components('(defcomp ~x (&key v) (b v))')
|
|
assert sx('(~x :v "1")') == "<b>1</b>"
|
|
assert sx('(~x :v "2")') == "<b>2</b>"
|
|
|
|
def test_get_component_env(self):
|
|
register_components('(defcomp ~foo (&key x) (span x))')
|
|
env = get_component_env()
|
|
assert "~foo" in env
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Link card example — the first migration target
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestLinkCard:
|
|
def setup_method(self):
|
|
_COMPONENT_ENV.clear()
|
|
register_components('''
|
|
(defcomp ~link-card (&key link title image icon brand)
|
|
(a :href link
|
|
:class "block rounded border border-stone-200 bg-white hover:bg-stone-50 transition-colors no-underline"
|
|
(div :class "flex flex-row items-start gap-3 p-3"
|
|
(if image
|
|
(img :src image :alt "" :class "flex-shrink-0 w-16 h-16 rounded object-cover")
|
|
(div :class "flex-shrink-0 w-16 h-16 rounded bg-stone-100 flex items-center justify-center text-stone-400"
|
|
(i :class icon)))
|
|
(div :class "flex-1 min-w-0"
|
|
(div :class "font-medium text-stone-900 text-sm clamp-2" title)
|
|
(when brand
|
|
(div :class "text-xs text-stone-500 mt-0.5" brand))))))
|
|
''')
|
|
|
|
def test_with_image(self):
|
|
html = sx('''
|
|
(~link-card
|
|
:link "/products/apple/"
|
|
:title "Apple"
|
|
:image "/img/apple.jpg"
|
|
:icon "fas fa-shopping-bag")
|
|
''')
|
|
assert 'href="/products/apple/"' in html
|
|
assert '<img src="/img/apple.jpg"' in html
|
|
assert "Apple" in html
|
|
|
|
def test_without_image(self):
|
|
html = sx('''
|
|
(~link-card
|
|
:link "/posts/hello/"
|
|
:title "Hello World"
|
|
:icon "fas fa-file-alt")
|
|
''')
|
|
assert 'href="/posts/hello/"' in html
|
|
assert "<img" not in html
|
|
assert "fas fa-file-alt" in html
|
|
assert "Hello World" in html
|
|
|
|
def test_with_brand(self):
|
|
html = sx('''
|
|
(~link-card
|
|
:link "/p/x/"
|
|
:title "Widget"
|
|
:image "/img/w.jpg"
|
|
:brand "Acme Corp")
|
|
''')
|
|
assert "Acme Corp" in html
|
|
|
|
def test_without_brand(self):
|
|
html = sx('''
|
|
(~link-card
|
|
:link "/p/x/"
|
|
:title "Widget"
|
|
:image "/img/w.jpg")
|
|
''')
|
|
# brand div should not appear
|
|
assert "mt-0.5" not in html
|
|
|
|
def test_kwargs_from_python(self):
|
|
"""Pass data from Python (like a route handler would)."""
|
|
html = sx(
|
|
'(~link-card :link link :title title :image image :icon "fas fa-box")',
|
|
link="/products/banana/",
|
|
title="Banana",
|
|
image="/img/banana.jpg",
|
|
)
|
|
assert 'href="/products/banana/"' in html
|
|
assert "Banana" in html
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# sx_async() — async rendering (no real I/O, just passthrough)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestSxAsync:
|
|
def test_simple(self):
|
|
html = run(sx_async('(div "Async")'))
|
|
assert html == "<div>Async</div>"
|
|
|
|
def test_with_kwargs(self):
|
|
html = run(sx_async('(p name)', name="Alice"))
|
|
assert html == "<p>Alice</p>"
|
|
|
|
def test_with_component(self):
|
|
register_components('(defcomp ~x (&key v) (b v))')
|
|
html = run(sx_async('(~x :v "OK")'))
|
|
assert html == "<b>OK</b>"
|