Files
rose-ash/spec/tests/test-defcomp.sx
giles c20369b766 Add comprehensive spec tests: closures, macros, TCO, defcomp, parser
New test files expose fundamental evaluator issues:
- define doesn't create self-referencing closures (13 failures)
- let doesn't isolate scope from parent env (2 failures)
- set! doesn't walk scope chain for closed-over vars (3 failures)
- Component calls return kwargs object instead of evaluating body (10 failures)

485/516 passing (94%). Parser tests: 100% pass. Macro tests: 96% pass.
These failures map the exact work needed for tree-walk removal.

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

198 lines
7.9 KiB
Plaintext

;; ==========================================================================
;; test-defcomp.sx — Tests for component (defcomp) calling conventions
;;
;; Requires: test-framework.sx loaded first.
;; Modules tested: eval.sx (defcomp, component call), render.sx
;;
;; Component calling convention:
;; (defcomp ~name (&key k1 k2 &rest children) body...)
;; Keyword args: (~name :k1 v1 :k2 v2)
;; Children: (~name :k1 v1 child1 child2) — positional after keywords
;; Defaults: (or k1 "fallback")
;;
;; render-html takes an SX source string, evaluates + renders to HTML string.
;; For multi-form programs use (do ...) or define forms before the call.
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Basic defcomp behaviour
;; --------------------------------------------------------------------------
(defsuite "defcomp-basics"
(deftest "defcomp binds the component name"
(defcomp ~no-params ()
(span "hello"))
(assert-true (not (nil? ~no-params))))
(deftest "defcomp with positional params"
;; Components can accept plain positional params (not &key).
(defcomp ~greet (name)
(span name))
(assert-true (not (nil? ~greet))))
(deftest "defcomp body can reference defined names"
;; Body is evaluated in the defining env — outer defines are visible.
(define greeting "hi")
(defcomp ~uses-outer ()
(span greeting))
(assert-true (not (nil? ~uses-outer))))
(deftest "defcomp is a component type"
(defcomp ~typed-comp (&key x)
(div x))
;; component-affinity is available on all component values
(assert-equal "auto" (component-affinity ~typed-comp))))
;; --------------------------------------------------------------------------
;; Keyword argument (&key) convention
;; --------------------------------------------------------------------------
(defsuite "defcomp-keyword-args"
(deftest "single &key param receives keyword argument"
;; Evaluation: component body is called with title bound to "World".
(defcomp ~k-single (&key title)
title)
;; We call it and check the returned value (not HTML).
(assert-equal "World" (~k-single :title "World")))
(deftest "multiple &key params"
(defcomp ~k-multi (&key first last)
(str first " " last))
(assert-equal "Ada Lovelace" (~k-multi :first "Ada" :last "Lovelace")))
(deftest "missing &key param is nil"
(defcomp ~k-missing (&key title subtitle)
subtitle)
(assert-nil (~k-missing :title "Only title")))
(deftest "&key param default via or"
(defcomp ~k-default (&key label)
(or label "default-label"))
(assert-equal "custom" (~k-default :label "custom"))
(assert-equal "default-label" (~k-default)))
(deftest "&key params can be numbers"
(defcomp ~k-num (&key value)
(* value 2))
(assert-equal 84 (~k-num :value 42)))
(deftest "&key params can be lists"
(defcomp ~k-list (&key items)
(len items))
(assert-equal 3 (~k-list :items (list "a" "b" "c")))))
;; --------------------------------------------------------------------------
;; Rest / children convention
;; --------------------------------------------------------------------------
(defsuite "defcomp-rest-children"
(deftest "&rest captures all positional args"
(defcomp ~r-basic (&rest children)
(len children))
(assert-equal 3 (~r-basic "a" "b" "c")))
(deftest "&rest with &key separates keywords from positional"
(defcomp ~r-mixed (&key title &rest children)
(list title (len children)))
(let ((result (~r-mixed :title "T" "c1" "c2")))
(assert-equal "T" (first result))
(assert-equal 2 (nth result 1))))
(deftest "empty children when no positional args provided"
(defcomp ~r-empty (&rest children)
children)
(assert-true (empty? (~r-empty))))
(deftest "multiple children are captured in order"
(defcomp ~r-order (&rest children)
children)
(let ((kids (~r-order "x" "y" "z")))
(assert-equal "x" (nth kids 0))
(assert-equal "y" (nth kids 1))
(assert-equal "z" (nth kids 2)))))
;; --------------------------------------------------------------------------
;; Component rendering to HTML
;; --------------------------------------------------------------------------
(defsuite "defcomp-rendering"
(deftest "simplest component renders to HTML"
(assert-equal "<p>hello</p>"
(render-html "(do (defcomp ~r-simple () (p \"hello\")) (~r-simple))")))
(deftest "component with &key renders keyword arg value"
(assert-equal "<h1>Greetings</h1>"
(render-html "(do (defcomp ~r-title (&key text) (h1 text))
(~r-title :text \"Greetings\"))")))
(deftest "component with multiple &key args"
(let ((html (render-html
"(do (defcomp ~r-card (&key title subtitle)
(div :class \"card\" (h2 title) (p subtitle)))
(~r-card :title \"Hi\" :subtitle \"Sub\"))")))
(assert-true (string-contains? html "class=\"card\""))
(assert-true (string-contains? html "<h2>Hi</h2>"))
(assert-true (string-contains? html "<p>Sub</p>"))))
(deftest "nested component calls"
(let ((html (render-html
"(do
(defcomp ~r-inner (&key label) (span label))
(defcomp ~r-outer (&key text) (div (~r-inner :label text)))
(~r-outer :text \"nested\"))")))
(assert-true (string-contains? html "<div>"))
(assert-true (string-contains? html "<span>nested</span>"))))
(deftest "component with children rendered inside wrapper"
(let ((html (render-html
"(do (defcomp ~r-box (&key &rest children)
(div :class \"box\" children))
(~r-box (p \"inside\")))")))
(assert-true (string-contains? html "class=\"box\""))
(assert-true (string-contains? html "<p>inside</p>"))))
(deftest "component with conditional rendering via when"
(let ((html-with (render-html
"(do (defcomp ~r-cond (&key show)
(div (when show (span \"visible\"))))
(~r-cond :show true))"))
(html-without (render-html
"(do (defcomp ~r-cond (&key show)
(div (when show (span \"visible\"))))
(~r-cond :show false))")))
(assert-true (string-contains? html-with "<span>visible</span>"))
(assert-false (string-contains? html-without "<span>"))))
(deftest "component with conditional rendering via if"
(assert-equal "<p>yes</p>"
(render-html "(do (defcomp ~r-if (&key flag)
(if flag (p \"yes\") (p \"no\")))
(~r-if :flag true))"))
(assert-equal "<p>no</p>"
(render-html "(do (defcomp ~r-if (&key flag)
(if flag (p \"yes\") (p \"no\")))
(~r-if :flag false))")))
(deftest "component default via or renders correctly"
(assert-equal "<span>fallback</span>"
(render-html "(do (defcomp ~r-default (&key label)
(span (or label \"fallback\")))
(~r-default))"))
(assert-equal "<span>given</span>"
(render-html "(do (defcomp ~r-default (&key label)
(span (or label \"fallback\")))
(~r-default :label \"given\"))")))
(deftest "component with multiple children rendered in order"
(let ((html (render-html
"(do (defcomp ~r-multi (&rest children)
(ul children))
(~r-multi (li \"a\") (li \"b\") (li \"c\")))")))
(assert-true (string-contains? html "<li>a</li>"))
(assert-true (string-contains? html "<li>b</li>"))
(assert-true (string-contains? html "<li>c</li>")))))