;; ========================================================================== ;; test-render-html.sx — Exhaustive tests for HTML rendering ;; ;; Tests render-to-html against the HTML serialization specification. ;; Every test verifies the SX renderer produces correct HTML strings. ;; ;; Requires: test-framework.sx, adapter-html.sx loaded. ;; ========================================================================== ;; Helper: render a QUOTED SX expression to HTML string. ;; The expression is first evaluated in the env (to resolve symbols), ;; then the result is passed to render-to-html. ;; For simple values (strings, numbers), use rh-val instead. (define rh (fn (expr) (let ((env (env-extend (test-env)))) (render-to-html expr env)))) ;; Helper: render a literal value (no evaluation needed). ;; Uses render-value-to-html which skips the eval-expr dispatch. (define rh-val (fn (val) (render-value-to-html val (env-extend (test-env))))) ;; Helper: render with a pre-built env (define rh-env (fn (expr env) (render-to-html expr env))) ;; -------------------------------------------------------------------------- ;; 1. Text content and literals ;; -------------------------------------------------------------------------- (defsuite "html-text" (deftest "string renders as escaped text" (assert-equal "hello" (rh-val "hello"))) (deftest "number renders as string" (assert-equal "42" (rh-val 42))) (deftest "float renders as string" (assert-equal "3.14" (rh-val 3.14))) (deftest "boolean true renders as text" (assert-equal "true" (rh-val true))) (deftest "boolean false renders as text" (assert-equal "false" (rh-val false))) (deftest "nil renders as empty string" (assert-equal "" (rh-val nil))) (deftest "keyword renders as text" (assert-equal "hello" (rh-val :hello)))) ;; -------------------------------------------------------------------------- ;; 2. HTML escaping — content ;; -------------------------------------------------------------------------- (defsuite "html-escaping-content" (deftest "ampersand escaped in text" (assert-equal "a & b" (rh-val "a & b"))) (deftest "less-than escaped in text" (assert-equal "a < b" (rh-val "a < b"))) (deftest "greater-than escaped in text" (assert-equal "a > b" (rh-val "a > b"))) (deftest "multiple special chars escaped" (assert-equal "<b>tag</b>" (rh-val "tag"))) (deftest "text inside element is escaped" (assert-equal "

a & b

" (rh '(p "a & b"))))) ;; -------------------------------------------------------------------------- ;; 3. HTML escaping — attributes ;; -------------------------------------------------------------------------- (defsuite "html-escaping-attrs" (deftest "ampersand escaped in attribute value" (assert-equal "
" (rh '(div :title "a & b")))) (deftest "angle brackets escaped in attribute value" (assert-equal "
" (rh '(div :title ""))))) ;; -------------------------------------------------------------------------- ;; 4. Normal elements — open tag, children, close tag ;; -------------------------------------------------------------------------- (defsuite "html-normal-elements" (deftest "div with text" (assert-equal "
hello
" (rh '(div "hello")))) (deftest "p with text" (assert-equal "

paragraph

" (rh '(p "paragraph")))) (deftest "span with text" (assert-equal "inline" (rh '(span "inline")))) (deftest "empty div" (assert-equal "
" (rh '(div)))) (deftest "nested elements" (assert-equal "

inner

" (rh '(div (p "inner"))))) (deftest "multiple children" (assert-equal "

a

b

" (rh '(div (p "a") (p "b"))))) (deftest "deep nesting" (assert-equal "

deep

" (rh '(div (section (article (p "deep"))))))) (deftest "mixed text and element children" (assert-equal "

hello world

" (rh '(p "hello " (strong "world")))))) ;; -------------------------------------------------------------------------- ;; 5. Void elements — self-closing, no children ;; -------------------------------------------------------------------------- (defsuite "html-void-elements" (deftest "br" (assert-equal "
" (rh '(br)))) (deftest "hr" (assert-equal "
" (rh '(hr)))) (deftest "img with src" (assert-equal "" (rh '(img :src "photo.jpg")))) (deftest "input with type" (assert-equal "" (rh '(input :type "text")))) (deftest "meta with charset" (assert-equal "" (rh '(meta :charset "utf-8")))) (deftest "link with rel and href" (assert-equal "" (rh '(link :rel "stylesheet" :href "style.css")))) (deftest "source with src" (assert-equal "" (rh '(source :src "video.mp4")))) (deftest "col" (assert-equal "" (rh '(col)))) (deftest "wbr" (assert-equal "" (rh '(wbr))))) ;; -------------------------------------------------------------------------- ;; 6. Boolean attributes — name only when truthy ;; -------------------------------------------------------------------------- (defsuite "html-boolean-attrs" (deftest "checked true" (assert-equal "" (rh '(input :checked true)))) (deftest "checked false omitted" (assert-equal "" (rh '(input :checked false)))) (deftest "disabled true" (assert-equal "" (rh '(button :disabled true "click")))) (deftest "disabled false omitted" (assert-equal "" (rh '(button :disabled false "click")))) (deftest "readonly" (assert-equal "" (rh '(input :readonly true)))) (deftest "required" (assert-equal "" (rh '(input :required true)))) (deftest "multiple" (assert-equal "" (rh '(select :multiple true)))) (deftest "hidden" (assert-equal "" (rh '(div :hidden true)))) (deftest "autofocus" (assert-equal "" (rh '(input :autofocus true)))) (deftest "autoplay" (assert-equal "" (rh '(video :autoplay true)))) (deftest "loop" (assert-equal "" (rh '(video :loop true)))) (deftest "muted" (assert-equal "" (rh '(video :muted true)))) (deftest "controls" (assert-equal "" (rh '(audio :controls true)))) (deftest "selected" (assert-equal "" (rh '(option :selected true "yes")))) (deftest "open (details)" (assert-equal "
" (rh '(details :open true)))) (deftest "defer" (assert-equal "" (rh '(script :defer true)))) (deftest "async" (assert-equal "" (rh '(script :async true)))) (deftest "novalidate" (assert-equal "
" (rh '(form :novalidate true))))) ;; -------------------------------------------------------------------------- ;; 7. Regular attributes ;; -------------------------------------------------------------------------- (defsuite "html-regular-attrs" (deftest "class attribute" (assert-equal "
" (rh '(div :class "container")))) (deftest "id attribute" (assert-equal "
" (rh '(div :id "main")))) (deftest "style attribute" (assert-equal "
" (rh '(div :style "color: red")))) (deftest "data-* attribute" (assert-equal "
" (rh '(div :data-value "42")))) (deftest "aria-* attribute" (assert-equal "
" (rh '(div :aria-label "close")))) (deftest "multiple attributes" (assert-equal "home" (rh '(a :href "/" :class "link" "home")))) (deftest "nil attribute omitted" (assert-equal "
" (rh '(div :class nil)))) (deftest "numeric attribute value" (assert-equal "" (rh '(input :maxlength 10))))) ;; -------------------------------------------------------------------------- ;; 8. Fragments — children without wrapper ;; -------------------------------------------------------------------------- (defsuite "html-fragments" (deftest "fragment renders children without wrapper" (assert-equal "

a

b

" (rh '(<> (p "a") (p "b"))))) (deftest "empty fragment" (assert-equal "" (rh '(<>)))) (deftest "fragment with text" (assert-equal "hello world" (rh '(<> "hello " "world")))) (deftest "nested fragment" (assert-equal "

a

b

" (rh '(<> (<> (p "a")) (p "b")))))) ;; -------------------------------------------------------------------------- ;; 9. Raw HTML — unescaped passthrough ;; -------------------------------------------------------------------------- (defsuite "html-raw" (deftest "raw! passes through unescaped" (assert-equal "bold" (rh '(raw! "bold")))) (deftest "raw! with multiple args" (assert-equal "ab" (rh '(raw! "a" "b"))))) ;; -------------------------------------------------------------------------- ;; 10. Heading levels ;; -------------------------------------------------------------------------- (defsuite "html-headings" (deftest "h1" (assert-equal "

title

" (rh '(h1 "title")))) (deftest "h2" (assert-equal "

sub

" (rh '(h2 "sub")))) (deftest "h3" (assert-equal "

sec

" (rh '(h3 "sec")))) (deftest "h4" (assert-equal "

sub

" (rh '(h4 "sub")))) (deftest "h5" (assert-equal "
sub
" (rh '(h5 "sub")))) (deftest "h6" (assert-equal "
sub
" (rh '(h6 "sub"))))) ;; -------------------------------------------------------------------------- ;; 11. Lists (HTML) ;; -------------------------------------------------------------------------- (defsuite "html-lists" (deftest "unordered list" (assert-equal "" (rh '(ul (li "a") (li "b"))))) (deftest "ordered list" (assert-equal "
  1. 1
  2. 2
" (rh '(ol (li "1") (li "2"))))) (deftest "definition list" (assert-equal "
term
def
" (rh '(dl (dt "term") (dd "def")))))) ;; -------------------------------------------------------------------------- ;; 12. Tables ;; -------------------------------------------------------------------------- (defsuite "html-tables" (deftest "basic table" (assert-equal "
cell
" (rh '(table (tr (td "cell")))))) (deftest "table with header" (assert-equal "
col
val
" (rh '(table (thead (tr (th "col"))) (tbody (tr (td "val")))))))) ;; -------------------------------------------------------------------------- ;; 13. Forms ;; -------------------------------------------------------------------------- (defsuite "html-forms" (deftest "form with action" (assert-equal "
" (rh '(form :action "/submit")))) (deftest "input types" (assert-equal "" (rh '(input :type "email")))) (deftest "textarea" (assert-equal "" (rh '(textarea "content")))) (deftest "select with options" (assert-equal "" (rh '(select (option "a") (option "b"))))) (deftest "button" (assert-equal "" (rh '(button :type "submit" "go")))) (deftest "label with for" (assert-equal "" (rh '(label :for "name" "Name")))) (deftest "fieldset and legend" (assert-equal "
group
" (rh '(fieldset (legend "group")))))) ;; -------------------------------------------------------------------------- ;; 14. Media elements ;; -------------------------------------------------------------------------- (defsuite "html-media" (deftest "video with src" (assert-equal "" (rh '(video :src "v.mp4")))) (deftest "audio with controls" (assert-equal "" (rh '(audio :controls true)))) (deftest "iframe" (assert-equal "" (rh '(iframe :src "page.html")))) (deftest "canvas" (assert-equal "" (rh '(canvas :width 100 :height 100)))) (deftest "picture with source and img" (assert-equal "" (rh '(picture (source :srcset "photo.webp") (img :src "photo.jpg")))))) ;; -------------------------------------------------------------------------- ;; 15. Semantic elements ;; -------------------------------------------------------------------------- (defsuite "html-semantic" (deftest "header" (assert-equal "
h
" (rh '(header "h")))) (deftest "nav" (assert-equal "" (rh '(nav "n")))) (deftest "main" (assert-equal "
m
" (rh '(main "m")))) (deftest "section" (assert-equal "
s
" (rh '(section "s")))) (deftest "article" (assert-equal "
a
" (rh '(article "a")))) (deftest "aside" (assert-equal "" (rh '(aside "a")))) (deftest "footer" (assert-equal "" (rh '(footer "f")))) (deftest "details and summary" (assert-equal "
more

info

" (rh '(details (summary "more") (p "info"))))) (deftest "figure and figcaption" (assert-equal "
cap
" (rh '(figure (img :src "x.jpg") (figcaption "cap")))))) ;; -------------------------------------------------------------------------- ;; 16. SVG elements ;; -------------------------------------------------------------------------- (defsuite "html-svg" (deftest "svg container" (assert-equal "" (rh '(svg :viewBox "0 0 100 100")))) (deftest "circle" (let ((html (rh '(circle :cx 50 :cy 50 :r 40)))) (assert-true (string-contains? html "cx=\"50\"")) (assert-true (string-contains? html "cy=\"50\"")) (assert-true (string-contains? html "r=\"40\"")))) (deftest "rect" (assert-equal "" (rh '(rect :width 100 :height 50)))) (deftest "path" (assert-equal "" (rh '(path :d "M0 0 L100 100")))) (deftest "g with transform" (assert-equal "" (rh '(g :transform "translate(10,20)")))) (deftest "text element" (assert-equal "label" (rh '(text :x 10 :y 20 "label"))))) ;; -------------------------------------------------------------------------- ;; 17. Control flow in templates ;; -------------------------------------------------------------------------- (defsuite "html-control-flow" (deftest "if true renders then-branch" (assert-equal "

yes

" (rh '(if true (p "yes") (p "no"))))) (deftest "if false renders else-branch" (assert-equal "

no

" (rh '(if false (p "yes") (p "no"))))) (deftest "if false without else renders empty" (assert-equal "" (rh '(if false (p "x"))))) (deftest "when true renders body" (assert-equal "

ok

" (rh '(when true (p "ok"))))) (deftest "when false renders empty" (assert-equal "" (rh '(when false (p "x"))))) (deftest "cond renders matching branch" (assert-equal "

b

" (rh '(cond false (p "a") true (p "b"))))) (deftest "cond else branch" (assert-equal "

c

" (rh '(cond false (p "a") :else (p "c")))))) ;; -------------------------------------------------------------------------- ;; 18. Let bindings in templates ;; -------------------------------------------------------------------------- (defsuite "html-let" (deftest "let binding used in template" (assert-equal "

hello

" (rh '(let ((x "hello")) (p x))))) (deftest "let with multiple bindings" (assert-equal "

helloworld

" (rh '(let ((a "hello") (b "world")) (p a b))))) (deftest "nested let" (assert-equal "

inner

" (rh '(let ((x "inner")) (div (let ((y x)) (p y)))))))) ;; -------------------------------------------------------------------------- ;; 19. Map / for-each in templates ;; -------------------------------------------------------------------------- (defsuite "html-iteration" (deftest "map over items" (assert-equal "
  • a
  • b
  • c
  • " (rh '(map (fn (x) (li x)) (list "a" "b" "c"))))) (deftest "for-each renders items" (assert-equal "

    1

    2

    " (rh '(for-each (fn (x) (p (str x))) (list 1 2))))) (deftest "map-indexed" (assert-equal "
  • 0: a
  • 1: b
  • " (rh '(map-indexed (fn (i x) (li (str i ": " x))) (list "a" "b")))))) ;; -------------------------------------------------------------------------- ;; 20. Components ;; -------------------------------------------------------------------------- (defsuite "html-components" (deftest "simple component" (let ((env (env-extend (test-env)))) (eval-expr '(defcomp ~card (&key title) (div :class "card" (h2 title))) env) (assert-equal "

    hello

    " (rh-env '(~card :title "hello") env)))) (deftest "component with children" (let ((env (env-extend (test-env)))) (eval-expr '(defcomp ~box (&rest children) (div :class "box" children)) env) (assert-equal "

    inner

    " (rh-env '(~box (p "inner")) env)))) (deftest "component with keyword and children" (let ((env (env-extend (test-env)))) (eval-expr '(defcomp ~panel (&key title &rest children) (section (h2 title) children)) env) (assert-equal "

    Title

    body

    " (rh-env '(~panel :title "Title" (p "body")) env)))) (deftest "nested components" (let ((env (env-extend (test-env)))) (eval-expr '(defcomp ~inner (&key text) (em text)) env) (eval-expr '(defcomp ~outer (&key text) (div (~inner :text text))) env) (assert-equal "
    hi
    " (rh-env '(~outer :text "hi") env))))) ;; -------------------------------------------------------------------------- ;; 21. Macros ;; -------------------------------------------------------------------------- (defsuite "html-macros" (deftest "macro expands and renders" (let ((env (env-extend (test-env)))) (eval-expr '(defmacro ~wrap (body) `(div :class "wrapped" ,body)) env) (assert-equal "

    hello

    " (rh-env '(~wrap (p "hello")) env))))) ;; -------------------------------------------------------------------------- ;; 22. Begin/do — multi-expression body ;; -------------------------------------------------------------------------- (defsuite "html-begin" (deftest "do renders all expressions" (assert-equal "

    a

    b

    " (rh '(do (p "a") (p "b"))))) (deftest "begin renders all expressions" (assert-equal "

    title

    body

    " (rh '(begin (h1 "title") (p "body")))))) ;; -------------------------------------------------------------------------- ;; 23. Letrec in templates ;; -------------------------------------------------------------------------- (defsuite "html-letrec" (deftest "letrec with side-effect rendering" (assert-equal "
  • a
  • b
  • " (rh '(letrec ((items (list "a" "b"))) (do (map (fn (x) (li x)) items))))))) ;; -------------------------------------------------------------------------- ;; 24. Scope/provide in templates ;; -------------------------------------------------------------------------- (defsuite "html-scope" (deftest "scope renders body" (assert-equal "" (rh '(scope "items" (ul (li "inside")))))) (deftest "provide renders body" (assert-equal "
    content
    " (rh '(provide "theme" "dark" (div "content")))))) ;; -------------------------------------------------------------------------- ;; 25. Other elements ;; -------------------------------------------------------------------------- (defsuite "html-other-elements" (deftest "pre preserves structure" (assert-equal "
    code here
    " (rh '(pre "code here")))) (deftest "code element" (assert-equal "x = 1" (rh '(code "x = 1")))) (deftest "blockquote" (assert-equal "
    quote
    " (rh '(blockquote "quote")))) (deftest "abbr with title" (assert-equal "HTML" (rh '(abbr :title "HyperText Markup Language" "HTML")))) (deftest "time with datetime" (assert-equal "" (rh '(time :datetime "2026-01-01" "New Year")))) (deftest "dialog" (assert-equal "content" (rh '(dialog :open true "content")))) (deftest "template" (assert-equal "" (rh '(template "inner")))) (deftest "slot with name" (assert-equal "" (rh '(slot :name "header")))) (deftest "noscript" (assert-equal "" (rh '(noscript "fallback"))))) ;; -------------------------------------------------------------------------- ;; 26. Islands — defisland with hydration markers ;; -------------------------------------------------------------------------- (defsuite "html-islands" (deftest "island renders with data-sx-island attribute" (let ((env (env-extend (test-env)))) (eval-expr '(defisland ~counter (&key start) (span (str "count: " start))) env) (let ((html (rh-env '(~counter :start 0) env))) (assert-true (string-contains? html "data-sx-island")) (assert-true (string-contains? html "count: 0"))))) (deftest "island name appears in marker" (let ((env (env-extend (test-env)))) (eval-expr '(defisland ~toggle (&key label) (button label)) env) (let ((html (rh-env '(~toggle :label "click") env))) (assert-true (string-contains? html "toggle")) (assert-true (string-contains? html "click"))))) (deftest "island with children" (let ((env (env-extend (test-env)))) (eval-expr '(defisland ~wrapper (&rest children) (div :class "island" children)) env) (let ((html (rh-env '(~wrapper (p "inside")) env))) (assert-true (string-contains? html "data-sx-island")) (assert-true (string-contains? html "

    inside

    ")))))) ;; -------------------------------------------------------------------------- ;; 27. Lakes — server-morphable slots within islands ;; -------------------------------------------------------------------------- (defsuite "html-lakes" (deftest "lake renders with data-sx-lake attribute" (let ((env (env-extend (test-env)))) (let ((html (rh-env '(lake :id "content" (p "hello")) env))) (assert-true (string-contains? html "data-sx-lake")) (assert-true (string-contains? html "content")) (assert-true (string-contains? html "

    hello

    "))))) (deftest "lake with custom tag" (let ((env (env-extend (test-env)))) (let ((html (rh-env '(lake :id "nav" :tag "nav" (a "link")) env))) (assert-true (starts-with? html "link")))))) ;; -------------------------------------------------------------------------- ;; 28. Marshes — reactive server-morphable slots ;; -------------------------------------------------------------------------- (defsuite "html-marshes" (deftest "marsh renders with data-sx-marsh attribute" (let ((env (env-extend (test-env)))) (let ((html (rh-env '(marsh :id "feed" (li "item")) env))) (assert-true (string-contains? html "data-sx-marsh")) (assert-true (string-contains? html "feed")) (assert-true (string-contains? html "
  • item
  • "))))) (deftest "marsh with custom tag" (let ((env (env-extend (test-env)))) (let ((html (rh-env '(marsh :id "list" :tag "ul" (li "a") (li "b")) env))) (assert-true (starts-with? html "a")) (assert-true (string-contains? html "
  • b
  • ")))))) ;; -------------------------------------------------------------------------- ;; 29. Thread macro in templates ;; -------------------------------------------------------------------------- (defsuite "html-threading" (deftest "thread-first in template context" (assert-equal "

    HELLO

    " (rh '(p (-> "hello" upper)))))) ;; -------------------------------------------------------------------------- ;; 30. Define in templates ;; -------------------------------------------------------------------------- (defsuite "html-define-in-template" (deftest "define then use in same template" (assert-equal "

    42

    " (rh '(do (define x 42) (p (str x)))))) (deftest "defcomp then use" (assert-equal "hi" (rh '(do (defcomp ~tag (&key text) (em text)) (~tag :text "hi"))))))