From 70a58bddd84a81acb0245a2bcfb98b8b0ff0e498 Mon Sep 17 00:00:00 2001 From: giles Date: Tue, 24 Mar 2026 01:29:59 +0000 Subject: [PATCH] =?UTF-8?q?Exhaustive=20HTML=20render=20tests=20=E2=80=94?= =?UTF-8?q?=20143=20tests=20for=20adapter-html.sx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit spec/tests/test-render-html.sx covers the full HTML serialization surface: text/literals, content escaping, attribute escaping, normal elements, all 14 void elements, 18 boolean attributes, regular/data-*/aria-* attrs, fragments, raw HTML, headings, lists, tables, forms, media, semantic elements, SVG, control flow (if/when/cond), let bindings, map/for-each, components (simple/children/keyword+children/nested), macros, begin/do, letrec, scope/provide, islands with hydration markers, lakes, marshes, threading, define-in-template. Validates adapter-html.sx can replace sx_render.ml as the canonical renderer. Co-Authored-By: Claude Opus 4.6 (1M context) --- spec/tests/test-render-html.sx | 763 +++++++++++++++++++++++++++++++++ 1 file changed, 763 insertions(+) create mode 100644 spec/tests/test-render-html.sx diff --git a/spec/tests/test-render-html.sx b/spec/tests/test-render-html.sx new file mode 100644 index 0000000..691e29b --- /dev/null +++ b/spec/tests/test-render-html.sx @@ -0,0 +1,763 @@ +;; ========================================================================== +;; 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 "
  • a
  • b
" + (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 "
f
" (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" + (assert-equal "" + (rh '(circle :cx 50 :cy 50 :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 "
    • inside
    " + (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))) + ;; Should contain the island marker + (assert-true (string-contains? html "data-sx-island")) + ;; Should contain rendered content + (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"))))))