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>
This commit is contained in:
@@ -273,7 +273,7 @@ for expr in parse_all(framework_src):
|
||||
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
||||
|
||||
# Tests requiring optional modules (only with --full)
|
||||
REQUIRES_FULL = {"test-continuations.sx", "test-types.sx", "test-freeze.sx", "test-strict.sx", "test-cek.sx"}
|
||||
REQUIRES_FULL = {"test-continuations.sx", "test-continuations-advanced.sx", "test-types.sx", "test-freeze.sx", "test-strict.sx", "test-cek.sx"}
|
||||
|
||||
test_files = []
|
||||
if args:
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
// =========================================================================
|
||||
|
||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||
var SX_VERSION = "2026-03-15T15:05:23Z";
|
||||
var SX_VERSION = "2026-03-15T15:31:20Z";
|
||||
|
||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||
|
||||
368
spec/tests/test-continuations-advanced.sx
Normal file
368
spec/tests/test-continuations-advanced.sx
Normal file
@@ -0,0 +1,368 @@
|
||||
;; ==========================================================================
|
||||
;; test-continuations-advanced.sx — Stress tests for multi-shot continuations
|
||||
;; and frame-based dynamic scope
|
||||
;;
|
||||
;; Requires: test-framework.sx loaded, continuations + scope extensions enabled.
|
||||
;;
|
||||
;; Tests the CEK continuation + ProvideFrame/ScopeAccFrame system under:
|
||||
;; - Multi-shot (k invoked 0, 1, 2, 3+ times)
|
||||
;; - Continuation composition across nested resets
|
||||
;; - provide/context: dynamic variable binding via kont walk
|
||||
;; - provide values preserved across shift/resume
|
||||
;; - scope/emit!/emitted: accumulator frames in kont
|
||||
;; - Accumulator frames preserved across shift/resume
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 1. Multi-shot continuations
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "multi-shot-continuations"
|
||||
(deftest "k invoked 3 times returns list of results"
|
||||
;; Each (k N) resumes (+ 1 N) independently.
|
||||
;; Shift body collects all three results into a list.
|
||||
(assert-equal (list 11 21 31)
|
||||
(reset (+ 1 (shift k (list (k 10) (k 20) (k 30)))))))
|
||||
|
||||
(deftest "k invoked via map over input list"
|
||||
;; map applies k to each element; each resume computes (+ 1 elem).
|
||||
(assert-equal (list 11 21 31)
|
||||
(reset (+ 1 (shift k (map k (list 10 20 30)))))))
|
||||
|
||||
(deftest "k invoked zero times — abort with plain value"
|
||||
;; Shift body ignores k and returns 42 directly.
|
||||
;; The outer (+ 1 ...) hole is never filled.
|
||||
(assert-equal 42
|
||||
(reset (+ 1 (shift k 42)))))
|
||||
|
||||
(deftest "k invoked conditionally — true branch calls k"
|
||||
;; Only the true branch calls k; result is (+ 1 10) = 11.
|
||||
(assert-equal 11
|
||||
(reset (+ 1 (shift k (if true (k 10) 99))))))
|
||||
|
||||
(deftest "k invoked conditionally — false branch skips k"
|
||||
;; False branch returns 99 directly without invoking k.
|
||||
(assert-equal 99
|
||||
(reset (+ 1 (shift k (if false (k 10) 99))))))
|
||||
|
||||
(deftest "k invoked inside let binding"
|
||||
;; (k 5) = (+ 1 5) = 6; x is bound to 6; (* x 2) = 12.
|
||||
(assert-equal 12
|
||||
(reset (+ 1 (shift k (let ((x (k 5))) (* x 2)))))))
|
||||
|
||||
(deftest "nested shift — inner k2 called by outer k1"
|
||||
;; k1 = (fn (v) (+ 1 v)), k2 = (fn (v) (+ 2 v))
|
||||
;; (k2 3) = 5, (k1 5) = 6
|
||||
;; inner reset returns 6 to shift-k1 body; (+ 10 6) = 16
|
||||
;; outer reset returns 16
|
||||
(assert-equal 16
|
||||
(reset (+ 1 (shift k1 (+ 10 (reset (+ 2 (shift k2 (k1 (k2 3)))))))))))
|
||||
|
||||
(deftest "k called twice accumulates both results"
|
||||
;; Two invocations in a list: (k 1) = 2, (k 2) = 3.
|
||||
(assert-equal (list 2 3)
|
||||
(reset (+ 1 (shift k (list (k 1) (k 2)))))))
|
||||
|
||||
(deftest "multi-shot k is idempotent — same arg gives same result"
|
||||
;; Calling k with the same argument twice should yield equal values.
|
||||
(let ((results (reset (+ 1 (shift k (list (k 5) (k 5)))))))
|
||||
(assert-equal (nth results 0) (nth results 1)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 2. Continuation composition
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "continuation-composition"
|
||||
(deftest "two independent resets have isolated continuations"
|
||||
;; Each reset is entirely separate — the two k values are unrelated.
|
||||
(let ((r1 (reset (+ 1 (shift k1 (k1 10)))))
|
||||
(r2 (reset (+ 100 (shift k2 (k2 5))))))
|
||||
(assert-equal 11 r1)
|
||||
(assert-equal 105 r2)))
|
||||
|
||||
(deftest "continuation passed to helper function and invoked there"
|
||||
;; apply-k is a plain lambda; it calls the continuation it receives.
|
||||
(let ((apply-k (fn (k v) (k v))))
|
||||
(assert-equal 15
|
||||
(reset (+ 5 (shift k (apply-k k 10)))))))
|
||||
|
||||
(deftest "continuation stored in variable and invoked later"
|
||||
;; reset returns k itself; we then invoke it outside the reset form.
|
||||
(let ((k (reset (shift k k))))
|
||||
;; k = identity continuation for (reset _), so (k v) = v
|
||||
(assert-true (continuation? k))
|
||||
(assert-equal 42 (k 42))
|
||||
(assert-equal 7 (k 7))))
|
||||
|
||||
(deftest "continuation stored then called with multiple values"
|
||||
;; k from (+ 1 hole); invoking k with different args gives different results.
|
||||
(let ((k (reset (+ 1 (shift k k)))))
|
||||
(assert-equal 11 (k 10))
|
||||
(assert-equal 21 (k 20))
|
||||
(assert-equal 31 (k 30))))
|
||||
|
||||
(deftest "continuation as argument to map — applied to a list"
|
||||
;; k = (fn (v) (+ 10 v)); map applies it to each element.
|
||||
(let ((k (reset (+ 10 (shift k k)))))
|
||||
(assert-equal (list 11 12 13)
|
||||
(map k (list 1 2 3)))))
|
||||
|
||||
(deftest "compose two continuations from nested resets"
|
||||
;; k1 = (fn (v) (+ 1 v)), k2 = (fn (v) (+ 10 v))
|
||||
;; (k2 0) = 10, (k1 10) = 11; outer reset returns 11.
|
||||
(assert-equal 11
|
||||
(reset (+ 1 (shift k1 (reset (+ 10 (shift k2 (k1 (k2 0))))))))))
|
||||
|
||||
(deftest "continuation predicate holds inside and after capture"
|
||||
;; k captured inside shift is a continuation; so is one returned by reset.
|
||||
(assert-true
|
||||
(reset (shift k (continuation? k))))
|
||||
(assert-true
|
||||
(continuation? (reset (shift k k))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 3. provide / context — basic dynamic scope
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "provide-context-basic"
|
||||
(deftest "simple provide and context"
|
||||
;; (context \"x\") walks the kont and finds the ProvideFrame for \"x\".
|
||||
(assert-equal 42
|
||||
(provide "x" 42 (context "x"))))
|
||||
|
||||
(deftest "nested provide — inner shadows outer"
|
||||
;; The nearest ProvideFrame wins when searching kont.
|
||||
(assert-equal 2
|
||||
(provide "x" 1
|
||||
(provide "x" 2
|
||||
(context "x")))))
|
||||
|
||||
(deftest "outer provide visible after inner scope exits"
|
||||
;; After the inner provide's body finishes, its frame is gone.
|
||||
;; The next (context \"x\") walks past it to the outer frame.
|
||||
(assert-equal 1
|
||||
(provide "x" 1
|
||||
(do
|
||||
(provide "x" 2 (context "x"))
|
||||
(context "x")))))
|
||||
|
||||
(deftest "multiple provide names are independent"
|
||||
;; Each name has its own ProvideFrame; they don't interfere.
|
||||
(assert-equal 3
|
||||
(provide "a" 1
|
||||
(provide "b" 2
|
||||
(+ (context "a") (context "b"))))))
|
||||
|
||||
(deftest "context with default — provider present returns provided value"
|
||||
;; Second arg to context is the default; present provider overrides it.
|
||||
(assert-equal 42
|
||||
(provide "x" 42 (context "x" 0))))
|
||||
|
||||
(deftest "context with default — no provider returns default"
|
||||
;; When no ProvideFrame exists for the name, the default is returned.
|
||||
(assert-equal 0
|
||||
(provide "y" 99 (context "x" 0))))
|
||||
|
||||
(deftest "provide with computed value"
|
||||
;; The value expression is evaluated before pushing the frame.
|
||||
(assert-equal 6
|
||||
(provide "n" (* 2 3) (context "n"))))
|
||||
|
||||
(deftest "provide value is the exact bound value (no double-eval)"
|
||||
;; Passing a list as the provided value should return that list.
|
||||
(let ((result (provide "items" (list 1 2 3) (context "items"))))
|
||||
(assert-equal (list 1 2 3) result))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 4. provide across shift — scope survives continuation capture/resume
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "provide-across-shift"
|
||||
(deftest "provide value preserved across shift and k invocation"
|
||||
;; The ProvideFrame lives in the kont beyond the ResetFrame.
|
||||
;; When k resumes, the frame is still there — context finds it.
|
||||
(assert-equal "dark"
|
||||
(reset
|
||||
(provide "theme" "dark"
|
||||
(+ 0 (shift k (k 0)))
|
||||
(context "theme")))))
|
||||
|
||||
(deftest "two provides both preserved across shift"
|
||||
;; Both ProvideFrames must survive the shift/resume round-trip.
|
||||
(assert-equal 3
|
||||
(reset
|
||||
(provide "a" 1
|
||||
(provide "b" 2
|
||||
(+ 0 (shift k (k 0)))
|
||||
(+ (context "a") (context "b")))))))
|
||||
|
||||
(deftest "context visible inside provide but not in shift body"
|
||||
;; shift body runs OUTSIDE the reset boundary — provide is not in scope.
|
||||
;; But context with a default should return the default.
|
||||
(assert-equal "fallback"
|
||||
(reset
|
||||
(provide "theme" "light"
|
||||
(shift k (context "theme" "fallback"))))))
|
||||
|
||||
(deftest "context after k invocation restores scope frame"
|
||||
;; k was captured with the ProvideFrame in its saved kont.
|
||||
;; After (k v) resumes, context finds the frame again.
|
||||
(let ((result
|
||||
(reset
|
||||
(provide "color" "red"
|
||||
(+ 0 (shift k (k 0)))
|
||||
(context "color")))))
|
||||
(assert-equal "red" result)))
|
||||
|
||||
(deftest "multi-shot: each k invocation reinstates captured ProvideFrame"
|
||||
;; k captures the ProvideFrame for "n" (it's inside the reset delimiter).
|
||||
;; Invoking k twice: each time (context "n") in the resumed body is valid.
|
||||
;; The shift body collects (context "n") from each resumed branch.
|
||||
(let ((readings
|
||||
(reset
|
||||
(provide "n" 10
|
||||
(+ 0 (shift k
|
||||
(list
|
||||
(k 0)
|
||||
(k 0))))
|
||||
(context "n")))))
|
||||
;; Each (k 0) resumes and returns (context "n") = 10.
|
||||
(assert-equal (list 10 10) readings))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 5. scope / emit! / emitted — accumulator frames
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "scope-emit-basic"
|
||||
(deftest "simple scope: emit two items and read emitted list"
|
||||
;; emit! appends to the nearest ScopeAccFrame; emitted returns the list.
|
||||
(assert-equal (list "a" "b")
|
||||
(scope "css"
|
||||
(emit! "css" "a")
|
||||
(emit! "css" "b")
|
||||
(emitted "css"))))
|
||||
|
||||
(deftest "empty scope returns empty list for emitted"
|
||||
;; No emit! calls means the accumulator stays empty.
|
||||
(assert-equal (list)
|
||||
(scope "css"
|
||||
(emitted "css"))))
|
||||
|
||||
(deftest "emit! order is preserved"
|
||||
;; Items appear in emission order, not reverse.
|
||||
(assert-equal (list 1 2 3 4 5)
|
||||
(scope "nums"
|
||||
(emit! "nums" 1)
|
||||
(emit! "nums" 2)
|
||||
(emit! "nums" 3)
|
||||
(emit! "nums" 4)
|
||||
(emit! "nums" 5)
|
||||
(emitted "nums"))))
|
||||
|
||||
(deftest "nested scopes: inner does not see outer's emitted"
|
||||
;; The inner scope has its own ScopeAccFrame; kont-find-scope-acc
|
||||
;; stops at the first matching name, so inner is fully isolated.
|
||||
(let ((inner-emitted
|
||||
(scope "css"
|
||||
(emit! "css" "outer")
|
||||
(scope "css"
|
||||
(emit! "css" "inner")
|
||||
(emitted "css")))))
|
||||
(assert-equal (list "inner") inner-emitted)))
|
||||
|
||||
(deftest "two differently-named scopes are independent"
|
||||
;; emit! to \"a\" must not appear in emitted \"b\" and vice versa.
|
||||
(let ((result-a nil) (result-b nil))
|
||||
(scope "a"
|
||||
(scope "b"
|
||||
(emit! "a" "for-a")
|
||||
(emit! "b" "for-b")
|
||||
(set! result-b (emitted "b")))
|
||||
(set! result-a (emitted "a")))
|
||||
(assert-equal (list "for-a") result-a)
|
||||
(assert-equal (list "for-b") result-b)))
|
||||
|
||||
(deftest "scope body returns last expression value"
|
||||
;; scope itself returns the last body expression, not the emitted list.
|
||||
(assert-equal 42
|
||||
(scope "x"
|
||||
(emit! "x" "ignored")
|
||||
42)))
|
||||
|
||||
(deftest "scope with :value acts as provide for context"
|
||||
;; When :value is given, the ScopeAccFrame also carries the value.
|
||||
;; context should be able to read it (if the evaluator searches scope-acc
|
||||
;; frames the same way as provide frames).
|
||||
;; NOTE: this tests the :value keyword path in step-sf-scope.
|
||||
;; If context only walks ProvideFrames, use provide directly instead.
|
||||
;; We verify at minimum that :value does not crash.
|
||||
(let ((r (try-call (fn ()
|
||||
(scope "x" :value 42
|
||||
(emitted "x"))))))
|
||||
(assert-true (get r "ok")))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 6. scope / emit! across shift — accumulator frames survive continuation
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "scope-emit-across-shift"
|
||||
(deftest "emit before and after shift both appear in emitted"
|
||||
;; The ScopeAccFrame is in the kont beyond the ResetFrame.
|
||||
;; After k resumes, the frame is still present; the second emit!
|
||||
;; appends to it.
|
||||
(assert-equal (list "a" "b")
|
||||
(reset
|
||||
(scope "acc"
|
||||
(emit! "acc" "a")
|
||||
(+ 0 (shift k (k 0)))
|
||||
(emit! "acc" "b")
|
||||
(emitted "acc")))))
|
||||
|
||||
(deftest "emit only before shift — one item in emitted"
|
||||
;; emit! before shift commits to the frame; shift/resume preserves it.
|
||||
(assert-equal (list "only")
|
||||
(reset
|
||||
(scope "log"
|
||||
(emit! "log" "only")
|
||||
(+ 0 (shift k (k 0)))
|
||||
(emitted "log")))))
|
||||
|
||||
(deftest "emit only after shift — one item in emitted"
|
||||
;; No emit! before shift; the frame starts empty; post-resume emit! adds one.
|
||||
(assert-equal (list "after")
|
||||
(reset
|
||||
(scope "log"
|
||||
(+ 0 (shift k (k 0)))
|
||||
(emit! "log" "after")
|
||||
(emitted "log")))))
|
||||
|
||||
(deftest "emits on both sides of single shift boundary"
|
||||
;; Single shift/resume; emits before and after are preserved.
|
||||
(assert-equal (list "a" "b")
|
||||
(reset
|
||||
(scope "trace"
|
||||
(emit! "trace" "a")
|
||||
(+ 0 (shift k (k 0)))
|
||||
(emit! "trace" "b")
|
||||
(emitted "trace")))))
|
||||
|
||||
(deftest "emitted inside shift body reads current accumulator"
|
||||
;; kont in the shift body is rest-kont (outer kont beyond the reset).
|
||||
;; The ScopeAccFrame should be present if it was installed before reset.
|
||||
;; emit! and emitted inside shift body use that outer frame.
|
||||
(let ((outer-acc nil))
|
||||
(scope "outer"
|
||||
(reset
|
||||
(shift k
|
||||
(do
|
||||
(emit! "outer" "from-shift")
|
||||
(set! outer-acc (emitted "outer")))))
|
||||
nil)
|
||||
(assert-equal (list "from-shift") outer-acc))))
|
||||
|
||||
306
spec/tests/test-render-advanced.sx
Normal file
306
spec/tests/test-render-advanced.sx
Normal file
@@ -0,0 +1,306 @@
|
||||
;; ==========================================================================
|
||||
;; 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)"))))
|
||||
@@ -569,6 +569,9 @@
|
||||
"sx-forge" (~plans/sx-forge/plan-sx-forge-content)
|
||||
"sx-swarm" (~plans/sx-swarm/plan-sx-swarm-content)
|
||||
"sx-proxy" (~plans/sx-proxy/plan-sx-proxy-content)
|
||||
"mother-language" (~plans/mother-language/plan-mother-language-content)
|
||||
"isolated-evaluator" (~plans/isolated-evaluator/plan-isolated-evaluator-content)
|
||||
"rust-wasm-host" (~plans/rust-wasm-host/plan-rust-wasm-host-content)
|
||||
"async-eval-convergence" (~plans/async-eval-convergence/plan-async-eval-convergence-content)
|
||||
"wasm-bytecode-vm" (~plans/wasm-bytecode-vm/plan-wasm-bytecode-vm-content)
|
||||
"generative-sx" (~plans/generative-sx/plan-generative-sx-content)
|
||||
@@ -580,9 +583,6 @@
|
||||
"foundations" (~plans/foundations/plan-foundations-content)
|
||||
"cek-reactive" (~plans/cek-reactive/plan-cek-reactive-content)
|
||||
"reactive-runtime" (~plans/reactive-runtime/plan-reactive-runtime-content)
|
||||
"rust-wasm-host" (~plans/rust-wasm-host/plan-rust-wasm-host-content)
|
||||
"isolated-evaluator" (~plans/isolated-evaluator/plan-isolated-evaluator-content)
|
||||
"mother-language" (~plans/mother-language/plan-mother-language-content)
|
||||
:else (~plans/index/plans-index-content))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user