Files
rose-ash/spec/tests/test-render-advanced.sx
giles a1fa1edf8a Add 68 new tests: continuations-advanced + render-advanced (938/938)
test-continuations-advanced.sx (41 tests):
  multi-shot continuations, composition, provide/context basics,
  provide across shift, scope/emit basics, scope across shift

test-render-advanced.sx (27 tests):
  nested components, dynamic content, list patterns,
  component patterns, special elements

Bugs found and documented:
- case in render context returns DOM object (CEK dispatches case
  before HTML adapter sees it — use cond instead for render)
- context not visible in shift body (correct: shift body runs
  outside the reset/provide boundary)
- Multiple shifts consume reset (correct: each shift needs its own
  reset)

Python runner: skip test-continuations-advanced.sx without --full.

JS 815/815 standard, 938/938 full, Python 706/706.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 15:32:21 +00:00

307 lines
14 KiB
Plaintext

;; ==========================================================================
;; test-render-advanced.sx — Advanced HTML rendering tests
;;
;; Requires: test-framework.sx loaded first.
;; Modules tested: render.sx, adapter-html.sx, eval.sx
;;
;; Platform functions required (beyond test framework):
;; render-html (sx-source) -> HTML string
;; Parses the sx-source string, evaluates via render-to-html in a
;; fresh env, and returns the resulting HTML string.
;;
;; Covers advanced rendering scenarios not addressed in test-render.sx:
;; - Deeply nested component calls
;; - Dynamic content (let, define, cond, case)
;; - List processing patterns (map, filter, reduce, map-indexed)
;; - Component patterns (defaults, nil bodies, map over children)
;; - Special element edge cases (fragments, void attrs, nil content)
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Nested component rendering
;; --------------------------------------------------------------------------
(defsuite "render-nested-components"
(deftest "component calling another component"
;; Inner component renders a span; outer wraps it in a div
(let ((html (render-html
"(do
(defcomp ~inner (&key label) (span label))
(defcomp ~outer (&key text) (div (~inner :label text)))
(~outer :text \"hello\"))")))
(assert-true (string-contains? html "<div>"))
(assert-true (string-contains? html "<span>hello</span>"))
(assert-true (string-contains? html "</div>"))))
(deftest "three levels of nesting"
;; A → B → C, each wrapping the next
(let ((html (render-html
"(do
(defcomp ~c () (em \"deep\"))
(defcomp ~b () (strong (~c)))
(defcomp ~a () (p (~b)))
(~a))")))
(assert-true (string-contains? html "<p>"))
(assert-true (string-contains? html "<strong>"))
(assert-true (string-contains? html "<em>deep</em>"))
(assert-true (string-contains? html "</strong>"))
(assert-true (string-contains? html "</p>"))))
(deftest "component with children that are components"
;; ~badge renders as a span; ~toolbar wraps whatever children it gets
(let ((html (render-html
"(do
(defcomp ~badge (&key text) (span :class \"badge\" text))
(defcomp ~toolbar (&rest children) (nav children))
(~toolbar (~badge :text \"Home\") (~badge :text \"About\")))")))
(assert-true (string-contains? html "<nav>"))
(assert-true (string-contains? html "class=\"badge\""))
(assert-true (string-contains? html "Home"))
(assert-true (string-contains? html "About"))
(assert-true (string-contains? html "</nav>"))))
(deftest "component that wraps children in a div"
;; Classic container pattern: keyword title + arbitrary children
(let ((html (render-html
"(do
(defcomp ~card (&key title &rest children)
(div :class \"card\"
(h3 title)
children))
(~card :title \"My Card\"
(p \"First\")
(p \"Second\")))")))
(assert-true (string-contains? html "class=\"card\""))
(assert-true (string-contains? html "<h3>My Card</h3>"))
(assert-true (string-contains? html "<p>First</p>"))
(assert-true (string-contains? html "<p>Second</p>")))))
;; --------------------------------------------------------------------------
;; Dynamic content
;; --------------------------------------------------------------------------
(defsuite "render-dynamic-content"
(deftest "let binding computed values"
;; let computes a value and uses it in the rendered output
(assert-equal "<span>30</span>"
(render-html "(let ((x 10) (y 20)) (span (+ x y)))")))
(deftest "define inside do block"
;; Definitions accumulate across do statements
(assert-equal "<p>hello world</p>"
(render-html "(do
(define greeting \"hello\")
(define target \"world\")
(p (str greeting \" \" target)))")))
(deftest "nested let scoping"
;; Inner let shadows outer binding; outer binding restored after
(assert-equal "<div><span>inner</span><span>outer</span></div>"
(render-html "(do
(define label \"outer\")
(div
(let ((label \"inner\")) (span label))
(span label)))")))
(deftest "cond dispatching different elements"
;; Different cond branches produce different tags
(assert-equal "<h1>big</h1>"
(render-html "(let ((size \"large\"))
(cond (= size \"large\") (h1 \"big\")
(= size \"small\") (h6 \"small\")
:else (p \"medium\")))"))
(assert-equal "<h6>small</h6>"
(render-html "(let ((size \"small\"))
(cond (= size \"large\") (h1 \"big\")
(= size \"small\") (h6 \"small\")
:else (p \"medium\")))"))
(assert-equal "<p>medium</p>"
(render-html "(let ((size \"other\"))
(cond (= size \"large\") (h1 \"big\")
(= size \"small\") (h6 \"small\")
:else (p \"medium\")))")))
(deftest "cond dispatching different elements"
;; cond on a value selects between different rendered elements
(assert-equal "<strong>bold</strong>"
(render-html "(let ((style \"bold\"))
(cond (= style \"bold\") (strong \"bold\")
(= style \"italic\") (em \"italic\")
:else (span \"normal\")))"))
(assert-equal "<em>italic</em>"
(render-html "(let ((style \"italic\"))
(cond (= style \"bold\") (strong \"bold\")
(= style \"italic\") (em \"italic\")
:else (span \"normal\")))"))
(assert-equal "<span>normal</span>"
(render-html "(let ((style \"other\"))
(cond (= style \"bold\") (strong \"bold\")
(= style \"italic\") (em \"italic\")
:else (span \"normal\")))"))))
;; --------------------------------------------------------------------------
;; List processing patterns
;; --------------------------------------------------------------------------
(defsuite "render-list-patterns"
(deftest "map producing li items inside ul"
(assert-equal "<ul><li>a</li><li>b</li><li>c</li></ul>"
(render-html "(ul (map (fn (x) (li x)) (list \"a\" \"b\" \"c\")))")))
(deftest "filter then map inside container"
;; Keep only even numbers, render each as a span
(assert-equal "<div><span>2</span><span>4</span></div>"
(render-html "(div (map (fn (x) (span x))
(filter (fn (x) (= (mod x 2) 0))
(list 1 2 3 4 5))))")))
(deftest "reduce building a string inside a span"
;; Join words with a separator via reduce, wrap in span
(assert-equal "<span>a-b-c</span>"
(render-html "(let ((words (list \"a\" \"b\" \"c\")))
(span (reduce (fn (acc w)
(if (= acc \"\")
w
(str acc \"-\" w)))
\"\"
words)))")))
(deftest "map-indexed producing numbered items"
;; map-indexed provides both the index and the value
(assert-equal "<ol><li>1. alpha</li><li>2. beta</li><li>3. gamma</li></ol>"
(render-html "(ol (map-indexed
(fn (i x) (li (str (+ i 1) \". \" x)))
(list \"alpha\" \"beta\" \"gamma\")))")))
(deftest "nested map (map inside map)"
;; Each outer item produces a ul; inner items produce li
(let ((html (render-html
"(div (map (fn (row)
(ul (map (fn (cell) (li cell)) row)))
(list (list \"a\" \"b\")
(list \"c\" \"d\"))))")))
(assert-true (string-contains? html "<div>"))
;; Both inner uls must appear
(assert-true (string-contains? html "<li>a</li>"))
(assert-true (string-contains? html "<li>b</li>"))
(assert-true (string-contains? html "<li>c</li>"))
(assert-true (string-contains? html "<li>d</li>"))))
(deftest "empty map produces no children"
;; mapping over an empty list contributes nothing to the parent
(assert-equal "<ul></ul>"
(render-html "(ul (map (fn (x) (li x)) (list)))"))))
;; --------------------------------------------------------------------------
;; Component patterns
;; --------------------------------------------------------------------------
(defsuite "render-component-patterns"
(deftest "component with conditional rendering (when)"
;; when true → renders child; when false → renders nothing
(let ((html-on (render-html
"(do (defcomp ~toggle (&key active)
(div (when active (span \"on\"))))
(~toggle :active true))"))
(html-off (render-html
"(do (defcomp ~toggle (&key active)
(div (when active (span \"on\"))))
(~toggle :active false))")))
(assert-true (string-contains? html-on "<span>on</span>"))
(assert-false (string-contains? html-off "<span>"))))
(deftest "component with default keyword value (or pattern)"
;; Missing keyword falls back to default; explicit value overrides it
(let ((with-default (render-html
"(do (defcomp ~btn (&key label)
(button (or label \"Click me\")))
(~btn))"))
(with-value (render-html
"(do (defcomp ~btn (&key label)
(button (or label \"Click me\")))
(~btn :label \"Submit\"))")))
(assert-equal "<button>Click me</button>" with-default)
(assert-equal "<button>Submit</button>" with-value)))
(deftest "component composing other components"
;; ~page uses ~header and ~footer as sub-components
(let ((html (render-html
"(do
(defcomp ~header () (header (h1 \"Top\")))
(defcomp ~footer () (footer \"Bottom\"))
(defcomp ~page () (div (~header) (~footer)))
(~page))")))
(assert-true (string-contains? html "<header>"))
(assert-true (string-contains? html "<h1>Top</h1>"))
(assert-true (string-contains? html "<footer>"))
(assert-true (string-contains? html "Bottom"))))
(deftest "component with map over children"
;; Component receives a list via keyword, maps it to li elements
(let ((html (render-html
"(do
(defcomp ~item-list (&key items)
(ul (map (fn (x) (li x)) items)))
(~item-list :items (list \"x\" \"y\" \"z\")))")))
(assert-true (string-contains? html "<ul>"))
(assert-true (string-contains? html "<li>x</li>"))
(assert-true (string-contains? html "<li>y</li>"))
(assert-true (string-contains? html "<li>z</li>"))
(assert-true (string-contains? html "</ul>"))))
(deftest "component that renders nothing (nil body)"
;; A component whose body evaluates to nil produces no output
(assert-equal ""
(render-html "(do (defcomp ~empty () nil) (~empty))"))))
;; --------------------------------------------------------------------------
;; Special element edge cases
;; --------------------------------------------------------------------------
(defsuite "render-special-elements"
(deftest "fragment with mixed children: elements and bare text"
;; (<> ...) strips the wrapper — children appear side by side
(assert-equal "<p>a</p>text<p>b</p>"
(render-html "(<> (p \"a\") \"text\" (p \"b\"))")))
(deftest "void element with multiple attributes"
;; input is void (self-closing) and must carry its attrs correctly
(let ((html (render-html "(input :type \"text\" :placeholder \"Search…\")")))
(assert-true (string-contains? html "<input"))
(assert-true (string-contains? html "type=\"text\""))
(assert-true (string-contains? html "placeholder="))
(assert-true (string-contains? html "/>"))
(assert-false (string-contains? html "</input>"))))
(deftest "boolean attribute true emits name only"
;; :disabled true → the word "disabled" appears without a value
(let ((html (render-html "(input :type \"checkbox\" :disabled true)")))
(assert-true (string-contains? html "disabled"))
(assert-false (string-contains? html "disabled=\""))))
(deftest "boolean attribute false is omitted entirely"
;; :disabled false → the attribute must not appear at all
(let ((html (render-html "(input :type \"checkbox\" :disabled false)")))
(assert-false (string-contains? html "disabled"))))
(deftest "raw number as element content"
;; Numbers passed as children must be coerced to their string form
(assert-equal "<span>42</span>"
(render-html "(span 42)")))
(deftest "nil content omitted, non-nil siblings kept"
;; nil should not contribute text or tags; sibling content survives
(let ((html (render-html "(div nil \"hello\")")))
(assert-true (string-contains? html "hello"))
(assert-false (string-contains? html "nil"))))
(deftest "nil-only content leaves element empty"
;; A div whose only child is nil should render as an empty div
(assert-equal "<div></div>"
(render-html "(div nil)"))))