;; ========================================================================== ;; 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 "

T

")) (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 "
  • x
  • ")) (assert-true (string-contains? html "
  • y
  • ")) (assert-true (string-contains? html "
  • z
  • "))))) ;; -------------------------------------------------------------------------- ;; Component rendering to HTML ;; -------------------------------------------------------------------------- (defsuite "defcomp-rendering" (deftest "simplest component renders to HTML" (assert-equal "

    hello

    " (render-html "(do (defcomp ~r-simple () (p \"hello\")) (~r-simple))"))) (deftest "component with &key renders keyword arg value" (assert-equal "

    Greetings

    " (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 "

    Hi

    ")) (assert-true (string-contains? html "

    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 "
    ")) (assert-true (string-contains? html "nested")))) (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 "

    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 "
  • a
  • ")) (assert-true (string-contains? html "
  • b
  • ")) (assert-true (string-contains? html "
  • c
  • ")))))