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>
307 lines
14 KiB
Plaintext
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)"))))
|