;; ========================================================================== ;; 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" (assert-equal "World" (render-html "(do (defcomp ~k-single (&key title) (span title)) (~k-single :title \"World\"))"))) (deftest "multiple &key params" (assert-equal "Ada Lovelace" (render-html "(do (defcomp ~k-multi (&key first last) (span (str first \" \" last))) (~k-multi :first \"Ada\" :last \"Lovelace\"))"))) (deftest "missing &key param is nil" ;; When subtitle is nil, the span should be empty (assert-equal "" (render-html "(do (defcomp ~k-missing (&key title subtitle) (span (or subtitle \"\"))) (~k-missing :title \"Only title\"))"))) (deftest "&key param default via or" (let ((custom (render-html "(do (defcomp ~k-def (&key label) (span (or label \"default-label\"))) (~k-def :label \"custom\"))")) (default (render-html "(do (defcomp ~k-def2 (&key label) (span (or label \"default-label\"))) (~k-def2))"))) (assert-equal "custom" custom) (assert-equal "default-label" default))) (deftest "&key params can be numbers" (assert-equal "84" (render-html "(do (defcomp ~k-num (&key value) (span (* value 2))) (~k-num :value 42))"))) (deftest "&key params can be lists" (assert-equal "3" (render-html "(do (defcomp ~k-list (&key items) (span (len items))) (~k-list :items (list \"a\" \"b\" \"c\")))")))) ;; -------------------------------------------------------------------------- ;; Rest / children convention ;; -------------------------------------------------------------------------- (defsuite "defcomp-rest-children" (deftest "&rest captures positional args as content" (let ((html (render-html "(do (defcomp ~r-basic (&rest children) (div children)) (~r-basic \"a\" \"b\" \"c\"))"))) (assert-true (string-contains? html "a")) (assert-true (string-contains? html "b")) (assert-true (string-contains? html "c")))) (deftest "&rest with &key separates keywords from positional" (let ((html (render-html "(do (defcomp ~r-mixed (&key title &rest children) (div (h2 title) children)) (~r-mixed :title \"T\" (p \"c1\") (p \"c2\")))"))) (assert-true (string-contains? html "
c1
")) (assert-true (string-contains? html "c2
")))) (deftest "empty children when no positional args provided" (assert-equal "" (render-html "(do (defcomp ~r-empty (&rest children) (div children)) (~r-empty))"))) (deftest "multiple children rendered in order" (let ((html (render-html "(do (defcomp ~r-order (&rest children) (ul children)) (~r-order (li \"x\") (li \"y\") (li \"z\")))"))) (assert-true (string-contains? html "hello
" (render-html "(do (defcomp ~r-simple () (p \"hello\")) (~r-simple))"))) (deftest "component with &key renders keyword arg value" (assert-equal "Sub
")))) (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 "inside
")))) (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 "visible")) (assert-false (string-contains? html-without "")))) (deftest "component with conditional rendering via if" (assert-equal "yes
" (render-html "(do (defcomp ~r-if (&key flag) (if flag (p \"yes\") (p \"no\"))) (~r-if :flag true))")) (assert-equal "no
" (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 "fallback" (render-html "(do (defcomp ~r-default (&key label) (span (or label \"fallback\"))) (~r-default))")) (assert-equal "given" (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 "