Files
rose-ash/spec/tests/test-render-html.sx
giles 70a58bddd8 Exhaustive HTML render tests — 143 tests for adapter-html.sx
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) <noreply@anthropic.com>
2026-03-24 01:29:59 +00:00

764 lines
26 KiB
Plaintext

;; ==========================================================================
;; 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 &amp; b" (rh-val "a & b")))
(deftest "less-than escaped in text"
(assert-equal "a &lt; b" (rh-val "a < b")))
(deftest "greater-than escaped in text"
(assert-equal "a &gt; b" (rh-val "a > b")))
(deftest "multiple special chars escaped"
(assert-equal "&lt;b&gt;tag&lt;/b&gt;"
(rh-val "<b>tag</b>")))
(deftest "text inside element is escaped"
(assert-equal "<p>a &amp; b</p>"
(rh '(p "a & b")))))
;; --------------------------------------------------------------------------
;; 3. HTML escaping — attributes
;; --------------------------------------------------------------------------
(defsuite "html-escaping-attrs"
(deftest "ampersand escaped in attribute value"
(assert-equal "<div title=\"a &amp; b\"></div>"
(rh '(div :title "a & b"))))
(deftest "angle brackets escaped in attribute value"
(assert-equal "<div title=\"&lt;b&gt;\"></div>"
(rh '(div :title "<b>")))))
;; --------------------------------------------------------------------------
;; 4. Normal elements — open tag, children, close tag
;; --------------------------------------------------------------------------
(defsuite "html-normal-elements"
(deftest "div with text"
(assert-equal "<div>hello</div>" (rh '(div "hello"))))
(deftest "p with text"
(assert-equal "<p>paragraph</p>" (rh '(p "paragraph"))))
(deftest "span with text"
(assert-equal "<span>inline</span>" (rh '(span "inline"))))
(deftest "empty div"
(assert-equal "<div></div>" (rh '(div))))
(deftest "nested elements"
(assert-equal "<div><p>inner</p></div>"
(rh '(div (p "inner")))))
(deftest "multiple children"
(assert-equal "<div><p>a</p><p>b</p></div>"
(rh '(div (p "a") (p "b")))))
(deftest "deep nesting"
(assert-equal "<div><section><article><p>deep</p></article></section></div>"
(rh '(div (section (article (p "deep")))))))
(deftest "mixed text and element children"
(assert-equal "<p>hello <strong>world</strong></p>"
(rh '(p "hello " (strong "world"))))))
;; --------------------------------------------------------------------------
;; 5. Void elements — self-closing, no children
;; --------------------------------------------------------------------------
(defsuite "html-void-elements"
(deftest "br"
(assert-equal "<br />" (rh '(br))))
(deftest "hr"
(assert-equal "<hr />" (rh '(hr))))
(deftest "img with src"
(assert-equal "<img src=\"photo.jpg\" />"
(rh '(img :src "photo.jpg"))))
(deftest "input with type"
(assert-equal "<input type=\"text\" />"
(rh '(input :type "text"))))
(deftest "meta with charset"
(assert-equal "<meta charset=\"utf-8\" />"
(rh '(meta :charset "utf-8"))))
(deftest "link with rel and href"
(assert-equal "<link rel=\"stylesheet\" href=\"style.css\" />"
(rh '(link :rel "stylesheet" :href "style.css"))))
(deftest "source with src"
(assert-equal "<source src=\"video.mp4\" />"
(rh '(source :src "video.mp4"))))
(deftest "col"
(assert-equal "<col />" (rh '(col))))
(deftest "wbr"
(assert-equal "<wbr />" (rh '(wbr)))))
;; --------------------------------------------------------------------------
;; 6. Boolean attributes — name only when truthy
;; --------------------------------------------------------------------------
(defsuite "html-boolean-attrs"
(deftest "checked true"
(assert-equal "<input checked />"
(rh '(input :checked true))))
(deftest "checked false omitted"
(assert-equal "<input />"
(rh '(input :checked false))))
(deftest "disabled true"
(assert-equal "<button disabled>click</button>"
(rh '(button :disabled true "click"))))
(deftest "disabled false omitted"
(assert-equal "<button>click</button>"
(rh '(button :disabled false "click"))))
(deftest "readonly"
(assert-equal "<input readonly />"
(rh '(input :readonly true))))
(deftest "required"
(assert-equal "<input required />"
(rh '(input :required true))))
(deftest "multiple"
(assert-equal "<select multiple></select>"
(rh '(select :multiple true))))
(deftest "hidden"
(assert-equal "<div hidden></div>"
(rh '(div :hidden true))))
(deftest "autofocus"
(assert-equal "<input autofocus />"
(rh '(input :autofocus true))))
(deftest "autoplay"
(assert-equal "<video autoplay></video>"
(rh '(video :autoplay true))))
(deftest "loop"
(assert-equal "<video loop></video>"
(rh '(video :loop true))))
(deftest "muted"
(assert-equal "<video muted></video>"
(rh '(video :muted true))))
(deftest "controls"
(assert-equal "<audio controls></audio>"
(rh '(audio :controls true))))
(deftest "selected"
(assert-equal "<option selected>yes</option>"
(rh '(option :selected true "yes"))))
(deftest "open (details)"
(assert-equal "<details open></details>"
(rh '(details :open true))))
(deftest "defer"
(assert-equal "<script defer></script>"
(rh '(script :defer true))))
(deftest "async"
(assert-equal "<script async></script>"
(rh '(script :async true))))
(deftest "novalidate"
(assert-equal "<form novalidate></form>"
(rh '(form :novalidate true)))))
;; --------------------------------------------------------------------------
;; 7. Regular attributes
;; --------------------------------------------------------------------------
(defsuite "html-regular-attrs"
(deftest "class attribute"
(assert-equal "<div class=\"container\"></div>"
(rh '(div :class "container"))))
(deftest "id attribute"
(assert-equal "<div id=\"main\"></div>"
(rh '(div :id "main"))))
(deftest "style attribute"
(assert-equal "<div style=\"color: red\"></div>"
(rh '(div :style "color: red"))))
(deftest "data-* attribute"
(assert-equal "<div data-value=\"42\"></div>"
(rh '(div :data-value "42"))))
(deftest "aria-* attribute"
(assert-equal "<div aria-label=\"close\"></div>"
(rh '(div :aria-label "close"))))
(deftest "multiple attributes"
(assert-equal "<a href=\"/\" class=\"link\">home</a>"
(rh '(a :href "/" :class "link" "home"))))
(deftest "nil attribute omitted"
(assert-equal "<div></div>"
(rh '(div :class nil))))
(deftest "numeric attribute value"
(assert-equal "<input maxlength=\"10\" />"
(rh '(input :maxlength 10)))))
;; --------------------------------------------------------------------------
;; 8. Fragments — children without wrapper
;; --------------------------------------------------------------------------
(defsuite "html-fragments"
(deftest "fragment renders children without wrapper"
(assert-equal "<p>a</p><p>b</p>"
(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 "<p>a</p><p>b</p>"
(rh '(<> (<> (p "a")) (p "b"))))))
;; --------------------------------------------------------------------------
;; 9. Raw HTML — unescaped passthrough
;; --------------------------------------------------------------------------
(defsuite "html-raw"
(deftest "raw! passes through unescaped"
(assert-equal "<b>bold</b>"
(rh '(raw! "<b>bold</b>"))))
(deftest "raw! with multiple args"
(assert-equal "<em>a</em><em>b</em>"
(rh '(raw! "<em>a</em>" "<em>b</em>")))))
;; --------------------------------------------------------------------------
;; 10. Heading levels
;; --------------------------------------------------------------------------
(defsuite "html-headings"
(deftest "h1" (assert-equal "<h1>title</h1>" (rh '(h1 "title"))))
(deftest "h2" (assert-equal "<h2>sub</h2>" (rh '(h2 "sub"))))
(deftest "h3" (assert-equal "<h3>sec</h3>" (rh '(h3 "sec"))))
(deftest "h4" (assert-equal "<h4>sub</h4>" (rh '(h4 "sub"))))
(deftest "h5" (assert-equal "<h5>sub</h5>" (rh '(h5 "sub"))))
(deftest "h6" (assert-equal "<h6>sub</h6>" (rh '(h6 "sub")))))
;; --------------------------------------------------------------------------
;; 11. Lists (HTML)
;; --------------------------------------------------------------------------
(defsuite "html-lists"
(deftest "unordered list"
(assert-equal "<ul><li>a</li><li>b</li></ul>"
(rh '(ul (li "a") (li "b")))))
(deftest "ordered list"
(assert-equal "<ol><li>1</li><li>2</li></ol>"
(rh '(ol (li "1") (li "2")))))
(deftest "definition list"
(assert-equal "<dl><dt>term</dt><dd>def</dd></dl>"
(rh '(dl (dt "term") (dd "def"))))))
;; --------------------------------------------------------------------------
;; 12. Tables
;; --------------------------------------------------------------------------
(defsuite "html-tables"
(deftest "basic table"
(assert-equal "<table><tr><td>cell</td></tr></table>"
(rh '(table (tr (td "cell"))))))
(deftest "table with header"
(assert-equal "<table><thead><tr><th>col</th></tr></thead><tbody><tr><td>val</td></tr></tbody></table>"
(rh '(table (thead (tr (th "col"))) (tbody (tr (td "val"))))))))
;; --------------------------------------------------------------------------
;; 13. Forms
;; --------------------------------------------------------------------------
(defsuite "html-forms"
(deftest "form with action"
(assert-equal "<form action=\"/submit\"></form>"
(rh '(form :action "/submit"))))
(deftest "input types"
(assert-equal "<input type=\"email\" />"
(rh '(input :type "email"))))
(deftest "textarea"
(assert-equal "<textarea>content</textarea>"
(rh '(textarea "content"))))
(deftest "select with options"
(assert-equal "<select><option>a</option><option>b</option></select>"
(rh '(select (option "a") (option "b")))))
(deftest "button"
(assert-equal "<button type=\"submit\">go</button>"
(rh '(button :type "submit" "go"))))
(deftest "label with for"
(assert-equal "<label for=\"name\">Name</label>"
(rh '(label :for "name" "Name"))))
(deftest "fieldset and legend"
(assert-equal "<fieldset><legend>group</legend></fieldset>"
(rh '(fieldset (legend "group"))))))
;; --------------------------------------------------------------------------
;; 14. Media elements
;; --------------------------------------------------------------------------
(defsuite "html-media"
(deftest "video with src"
(assert-equal "<video src=\"v.mp4\"></video>"
(rh '(video :src "v.mp4"))))
(deftest "audio with controls"
(assert-equal "<audio controls></audio>"
(rh '(audio :controls true))))
(deftest "iframe"
(assert-equal "<iframe src=\"page.html\"></iframe>"
(rh '(iframe :src "page.html"))))
(deftest "canvas"
(assert-equal "<canvas width=\"100\" height=\"100\"></canvas>"
(rh '(canvas :width 100 :height 100))))
(deftest "picture with source and img"
(assert-equal "<picture><source srcset=\"photo.webp\" /><img src=\"photo.jpg\" /></picture>"
(rh '(picture (source :srcset "photo.webp") (img :src "photo.jpg"))))))
;; --------------------------------------------------------------------------
;; 15. Semantic elements
;; --------------------------------------------------------------------------
(defsuite "html-semantic"
(deftest "header" (assert-equal "<header>h</header>" (rh '(header "h"))))
(deftest "nav" (assert-equal "<nav>n</nav>" (rh '(nav "n"))))
(deftest "main" (assert-equal "<main>m</main>" (rh '(main "m"))))
(deftest "section" (assert-equal "<section>s</section>" (rh '(section "s"))))
(deftest "article" (assert-equal "<article>a</article>" (rh '(article "a"))))
(deftest "aside" (assert-equal "<aside>a</aside>" (rh '(aside "a"))))
(deftest "footer" (assert-equal "<footer>f</footer>" (rh '(footer "f"))))
(deftest "details and summary"
(assert-equal "<details><summary>more</summary><p>info</p></details>"
(rh '(details (summary "more") (p "info")))))
(deftest "figure and figcaption"
(assert-equal "<figure><img src=\"x.jpg\" /><figcaption>cap</figcaption></figure>"
(rh '(figure (img :src "x.jpg") (figcaption "cap"))))))
;; --------------------------------------------------------------------------
;; 16. SVG elements
;; --------------------------------------------------------------------------
(defsuite "html-svg"
(deftest "svg container"
(assert-equal "<svg viewBox=\"0 0 100 100\"></svg>"
(rh '(svg :viewBox "0 0 100 100"))))
(deftest "circle"
(assert-equal "<circle cx=\"50\" cy=\"50\" r=\"40\"></circle>"
(rh '(circle :cx 50 :cy 50 :r 40))))
(deftest "rect"
(assert-equal "<rect width=\"100\" height=\"50\"></rect>"
(rh '(rect :width 100 :height 50))))
(deftest "path"
(assert-equal "<path d=\"M0 0 L100 100\"></path>"
(rh '(path :d "M0 0 L100 100"))))
(deftest "g with transform"
(assert-equal "<g transform=\"translate(10,20)\"></g>"
(rh '(g :transform "translate(10,20)"))))
(deftest "text element"
(assert-equal "<text x=\"10\" y=\"20\">label</text>"
(rh '(text :x 10 :y 20 "label")))))
;; --------------------------------------------------------------------------
;; 17. Control flow in templates
;; --------------------------------------------------------------------------
(defsuite "html-control-flow"
(deftest "if true renders then-branch"
(assert-equal "<p>yes</p>"
(rh '(if true (p "yes") (p "no")))))
(deftest "if false renders else-branch"
(assert-equal "<p>no</p>"
(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 "<p>ok</p>"
(rh '(when true (p "ok")))))
(deftest "when false renders empty"
(assert-equal "" (rh '(when false (p "x")))))
(deftest "cond renders matching branch"
(assert-equal "<p>b</p>"
(rh '(cond false (p "a") true (p "b")))))
(deftest "cond else branch"
(assert-equal "<p>c</p>"
(rh '(cond false (p "a") :else (p "c"))))))
;; --------------------------------------------------------------------------
;; 18. Let bindings in templates
;; --------------------------------------------------------------------------
(defsuite "html-let"
(deftest "let binding used in template"
(assert-equal "<p>hello</p>"
(rh '(let ((x "hello")) (p x)))))
(deftest "let with multiple bindings"
(assert-equal "<p>helloworld</p>"
(rh '(let ((a "hello") (b "world")) (p a b)))))
(deftest "nested let"
(assert-equal "<div><p>inner</p></div>"
(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 "<li>a</li><li>b</li><li>c</li>"
(rh '(map (fn (x) (li x)) (list "a" "b" "c")))))
(deftest "for-each renders items"
(assert-equal "<p>1</p><p>2</p>"
(rh '(for-each (fn (x) (p (str x))) (list 1 2)))))
(deftest "map-indexed"
(assert-equal "<li>0: a</li><li>1: b</li>"
(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 "<div class=\"card\"><h2>hello</h2></div>"
(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 "<div class=\"box\"><p>inner</p></div>"
(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 "<section><h2>Title</h2><p>body</p></section>"
(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 "<div><em>hi</em></div>"
(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 "<div class=\"wrapped\"><p>hello</p></div>"
(rh-env '(~wrap (p "hello")) env)))))
;; --------------------------------------------------------------------------
;; 22. Begin/do — multi-expression body
;; --------------------------------------------------------------------------
(defsuite "html-begin"
(deftest "do renders all expressions"
(assert-equal "<p>a</p><p>b</p>"
(rh '(do (p "a") (p "b")))))
(deftest "begin renders all expressions"
(assert-equal "<h1>title</h1><p>body</p>"
(rh '(begin (h1 "title") (p "body"))))))
;; --------------------------------------------------------------------------
;; 23. Letrec in templates
;; --------------------------------------------------------------------------
(defsuite "html-letrec"
(deftest "letrec with side-effect rendering"
(assert-equal "<li>a</li><li>b</li>"
(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 "<ul><li>inside</li></ul>"
(rh '(scope "items"
(ul (li "inside"))))))
(deftest "provide renders body"
(assert-equal "<div>content</div>"
(rh '(provide "theme" "dark"
(div "content"))))))
;; --------------------------------------------------------------------------
;; 25. Other elements
;; --------------------------------------------------------------------------
(defsuite "html-other-elements"
(deftest "pre preserves structure"
(assert-equal "<pre>code here</pre>"
(rh '(pre "code here"))))
(deftest "code element"
(assert-equal "<code>x = 1</code>"
(rh '(code "x = 1"))))
(deftest "blockquote"
(assert-equal "<blockquote>quote</blockquote>"
(rh '(blockquote "quote"))))
(deftest "abbr with title"
(assert-equal "<abbr title=\"HyperText Markup Language\">HTML</abbr>"
(rh '(abbr :title "HyperText Markup Language" "HTML"))))
(deftest "time with datetime"
(assert-equal "<time datetime=\"2026-01-01\">New Year</time>"
(rh '(time :datetime "2026-01-01" "New Year"))))
(deftest "dialog"
(assert-equal "<dialog open>content</dialog>"
(rh '(dialog :open true "content"))))
(deftest "template"
(assert-equal "<template>inner</template>"
(rh '(template "inner"))))
(deftest "slot with name"
(assert-equal "<slot name=\"header\"></slot>"
(rh '(slot :name "header"))))
(deftest "noscript"
(assert-equal "<noscript>fallback</noscript>"
(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 "<p>inside</p>"))))))
;; --------------------------------------------------------------------------
;; 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 "<p>hello</p>")))))
(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 "<nav"))
(assert-true (string-contains? html "<a>link</a>"))))))
;; --------------------------------------------------------------------------
;; 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 "<li>item</li>")))))
(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 "<ul"))
(assert-true (string-contains? html "<li>a</li>"))
(assert-true (string-contains? html "<li>b</li>"))))))
;; --------------------------------------------------------------------------
;; 29. Thread macro in templates
;; --------------------------------------------------------------------------
(defsuite "html-threading"
(deftest "thread-first in template context"
(assert-equal "<p>HELLO</p>"
(rh '(p (-> "hello" upper))))))
;; --------------------------------------------------------------------------
;; 30. Define in templates
;; --------------------------------------------------------------------------
(defsuite "html-define-in-template"
(deftest "define then use in same template"
(assert-equal "<p>42</p>"
(rh '(do (define x 42) (p (str x))))))
(deftest "defcomp then use"
(assert-equal "<em>hi</em>"
(rh '(do (defcomp ~tag (&key text) (em text))
(~tag :text "hi"))))))