Files
mono/shared/sx/tests/test_html.py
giles e8bc228c7f Rebrand sexp → sx across web platform (173 files)
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>
2026-03-01 11:06:57 +00:00

366 lines
12 KiB
Python

"""Tests for the HSX-style HTML renderer."""
import pytest
from shared.sx import parse, evaluate
from shared.sx.html import render, escape_text, escape_attr
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def r(text, env=None):
"""Parse and render a single expression."""
return render(parse(text), env)
# ---------------------------------------------------------------------------
# Escaping
# ---------------------------------------------------------------------------
class TestEscaping:
def test_escape_text_ampersand(self):
assert escape_text("A & B") == "A &amp; B"
def test_escape_text_lt_gt(self):
assert escape_text("<script>") == "&lt;script&gt;"
def test_escape_attr_quotes(self):
assert escape_attr('he said "hi"') == "he said &quot;hi&quot;"
def test_escape_attr_all(self):
assert escape_attr('&<>"') == "&amp;&lt;&gt;&quot;"
# ---------------------------------------------------------------------------
# Primitives / atoms
# ---------------------------------------------------------------------------
class TestAtoms:
def test_string(self):
assert r('"Hello"') == "Hello"
def test_string_with_entities(self):
assert r('"<b>bold</b>"') == "&lt;b&gt;bold&lt;/b&gt;"
def test_integer(self):
assert r("42") == "42"
def test_float(self):
assert r("3.14") == "3.14"
def test_nil(self):
assert r("nil") == ""
def test_true(self):
assert r("true") == ""
def test_false(self):
assert r("false") == ""
def test_keyword(self):
assert r(":hello") == "hello"
# ---------------------------------------------------------------------------
# Simple HTML elements
# ---------------------------------------------------------------------------
class TestElements:
def test_div(self):
assert r('(div "Hello")') == "<div>Hello</div>"
def test_empty_div(self):
assert r("(div)") == "<div></div>"
def test_nested(self):
assert r('(div (p "Hello"))') == "<div><p>Hello</p></div>"
def test_multiple_children(self):
assert r('(ul (li "One") (li "Two"))') == \
"<ul><li>One</li><li>Two</li></ul>"
def test_mixed_text_and_elements(self):
assert r('(p "Hello " (strong "world"))') == \
"<p>Hello <strong>world</strong></p>"
def test_deep_nesting(self):
assert r('(div (div (div (span "deep"))))') == \
"<div><div><div><span>deep</span></div></div></div>"
# ---------------------------------------------------------------------------
# Attributes
# ---------------------------------------------------------------------------
class TestAttributes:
def test_single_attr(self):
assert r('(div :class "card")') == '<div class="card"></div>'
def test_multiple_attrs(self):
html = r('(div :class "card" :id "main")')
assert 'class="card"' in html
assert 'id="main"' in html
def test_attr_with_children(self):
assert r('(div :class "card" (p "Body"))') == \
'<div class="card"><p>Body</p></div>'
def test_attr_escaping(self):
assert r('(div :title "A & B")') == '<div title="A &amp; B"></div>'
def test_attr_with_quotes(self):
assert r("""(div :title "say \\"hi\\"")""") == \
'<div title="say &quot;hi&quot;"></div>'
def test_false_attr_omitted(self):
env = {"flag": False}
html = render(parse("(div :hidden flag)"), env)
assert html == "<div></div>"
def test_nil_attr_omitted(self):
env = {"flag": None}
html = render(parse("(div :data-x flag)"), env)
assert html == "<div></div>"
def test_numeric_attr(self):
assert r('(input :tabindex 1)') == '<input tabindex="1">'
# ---------------------------------------------------------------------------
# Boolean attributes
# ---------------------------------------------------------------------------
class TestBooleanAttrs:
def test_disabled_true(self):
env = {"yes": True}
html = render(parse("(button :disabled yes)"), env)
assert html == "<button disabled></button>"
def test_disabled_false(self):
env = {"no": False}
html = render(parse("(button :disabled no)"), env)
assert html == "<button></button>"
def test_checked(self):
env = {"c": True}
html = render(parse('(input :type "checkbox" :checked c)'), env)
assert 'checked' in html
assert 'type="checkbox"' in html
def test_required(self):
env = {"r": True}
html = render(parse("(input :required r)"), env)
assert "required" in html
# ---------------------------------------------------------------------------
# Void elements
# ---------------------------------------------------------------------------
class TestVoidElements:
def test_br(self):
assert r("(br)") == "<br>"
def test_img(self):
html = r('(img :src "/photo.jpg" :alt "Photo")')
assert html == '<img src="/photo.jpg" alt="Photo">'
def test_input(self):
html = r('(input :type "text" :name "q")')
assert html == '<input type="text" name="q">'
def test_hr(self):
assert r("(hr)") == "<hr>"
def test_meta(self):
html = r('(meta :charset "utf-8")')
assert html == '<meta charset="utf-8">'
def test_link(self):
html = r('(link :rel "stylesheet" :href "/s.css")')
assert html == '<link rel="stylesheet" href="/s.css">'
def test_void_no_closing_tag(self):
# Void elements must not have a closing tag
html = r("(br)")
assert "</br>" not in html
# ---------------------------------------------------------------------------
# Fragment (<>)
# ---------------------------------------------------------------------------
class TestFragments:
def test_basic_fragment(self):
assert r('(<> (li "A") (li "B"))') == "<li>A</li><li>B</li>"
def test_empty_fragment(self):
assert r("(<>)") == ""
def test_nested_fragment(self):
html = r('(ul (<> (li "1") (li "2")))')
assert html == "<ul><li>1</li><li>2</li></ul>"
# ---------------------------------------------------------------------------
# raw!
# ---------------------------------------------------------------------------
class TestRawHtml:
def test_raw_string(self):
assert r('(raw! "<b>bold</b>")') == "<b>bold</b>"
def test_raw_no_escaping(self):
html = r('(raw! "<script>alert(1)</script>")')
assert "<script>" in html
def test_raw_with_variable(self):
env = {"content": "<em>hi</em>"}
html = render(parse("(raw! content)"), env)
assert html == "<em>hi</em>"
# ---------------------------------------------------------------------------
# Components (defcomp / ~prefix)
# ---------------------------------------------------------------------------
class TestComponents:
def test_basic_component(self):
env = {}
evaluate(parse('(defcomp ~badge (&key label) (span :class "badge" label))'), env)
html = render(parse('(~badge :label "New")'), env)
assert html == '<span class="badge">New</span>'
def test_component_with_children(self):
env = {}
evaluate(parse(
'(defcomp ~card (&key title &rest children)'
' (div :class "card" (h2 title) children))'
), env)
html = render(parse('(~card :title "Hi" (p "Body"))'), env)
assert html == '<div class="card"><h2>Hi</h2><p>Body</p></div>'
def test_nested_components(self):
env = {}
evaluate(parse('(defcomp ~inner (&key text) (em text))'), env)
evaluate(parse(
'(defcomp ~outer (&key label)'
' (div (~inner :text label)))'
), env)
html = render(parse('(~outer :label "Hello")'), env)
assert html == "<div><em>Hello</em></div>"
def test_component_with_conditional(self):
env = {}
evaluate(parse(
'(defcomp ~maybe (&key show text)'
' (when show (span text)))'
), env)
assert render(parse('(~maybe :show true :text "Yes")'), env) == "<span>Yes</span>"
assert render(parse('(~maybe :show false :text "No")'), env) == ""
# ---------------------------------------------------------------------------
# Expressions in render context
# ---------------------------------------------------------------------------
class TestExpressions:
def test_let_in_render(self):
html = r('(let ((x "Hello")) (p x))')
assert html == "<p>Hello</p>"
def test_if_true_branch(self):
html = r('(if true (p "Yes") (p "No"))')
assert html == "<p>Yes</p>"
def test_if_false_branch(self):
html = r('(if false (p "Yes") (p "No"))')
assert html == "<p>No</p>"
def test_when_true(self):
html = r('(when true (p "Shown"))')
assert html == "<p>Shown</p>"
def test_when_false(self):
html = r('(when false (p "Hidden"))')
assert html == ""
def test_map_rendering(self):
html = r('(map (lambda (x) (li x)) (list "A" "B" "C"))')
assert html == "<li>A</li><li>B</li><li>C</li>"
def test_variable_in_element(self):
env = {"name": "World"}
html = render(parse('(p (str "Hello " name))'), env)
assert html == "<p>Hello World</p>"
def test_str_in_attr(self):
env = {"slug": "apple"}
html = render(parse('(a :href (str "/p/" slug) "Link")'), env)
assert html == '<a href="/p/apple">Link</a>'
# ---------------------------------------------------------------------------
# Full page structure
# ---------------------------------------------------------------------------
class TestFullPage:
def test_simple_page(self):
html = r('''
(html :lang "en"
(head (title "Test"))
(body (h1 "Hello")))
''')
assert '<html lang="en">' in html
assert "<title>Test</title>" in html
assert "<h1>Hello</h1>" in html
assert "</body>" in html
assert "</html>" in html
def test_form(self):
html = r('''
(form :method "post" :action "/login"
(label :for "user" "Username")
(input :type "text" :name "user" :required true)
(button :type "submit" "Login"))
''')
assert '<form method="post" action="/login">' in html
assert '<label for="user">Username</label>' in html
assert 'required' in html
assert "<button" in html
def test_table(self):
html = r('''
(table
(thead (tr (th "Name") (th "Price")))
(tbody
(tr (td "Apple") (td "1.50"))
(tr (td "Banana") (td "0.75"))))
''')
assert "<thead>" in html
assert "<th>Name</th>" in html
assert "<td>Apple</td>" in html
# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------
class TestEdgeCases:
def test_empty_list(self):
assert render([], {}) == ""
def test_none(self):
assert render(None, {}) == ""
def test_dict_not_rendered(self):
assert render({"a": 1}, {}) == ""
def test_number_list(self):
# A list of plain numbers (not a call) renders each
assert render([1, 2, 3], {}) == "123"
def test_string_list(self):
assert render(["a", "b"], {}) == "ab"