;; test-htmx.sx — Tests for htmx 4.0 compatibility layer ;; ;; Tests the attribute-to-handler translator: pure parsing functions, ;; swap mode resolution, trigger parsing, and DOM integration via harness. ;; ── Time parsing ──────────────────────────────────────────────── (defsuite "htmx-parse-time" (deftest "parses milliseconds" (assert= (hx-parse-time "500ms") 500)) (deftest "parses seconds" (assert= (hx-parse-time "1s") 1000)) (deftest "parses fractional seconds" (assert= (hx-parse-time "0.5s") 500)) (deftest "parses minutes" (assert= (hx-parse-time "2m") 120000)) (deftest "parses bare number" (assert= (hx-parse-time "100") 100)) (deftest "returns nil for nil" (assert= (hx-parse-time nil) nil))) ;; ── Swap mode normalization (v4 aliases) ──────────────────────── (defsuite "htmx-swap-aliases" (deftest "before → beforebegin" (assert= (hx-normalize-swap-mode "before") "beforebegin")) (deftest "after → afterend" (assert= (hx-normalize-swap-mode "after") "afterend")) (deftest "prepend → afterbegin" (assert= (hx-normalize-swap-mode "prepend") "afterbegin")) (deftest "append → beforeend" (assert= (hx-normalize-swap-mode "append") "beforeend")) (deftest "innerHTML passes through" (assert= (hx-normalize-swap-mode "innerHTML") "innerHTML")) (deftest "outerHTML passes through" (assert= (hx-normalize-swap-mode "outerHTML") "outerHTML")) (deftest "delete passes through" (assert= (hx-normalize-swap-mode "delete") "delete")) (deftest "innerMorph passes through" (assert= (hx-normalize-swap-mode "innerMorph") "innerMorph")) (deftest "outerMorph passes through" (assert= (hx-normalize-swap-mode "outerMorph") "outerMorph")) (deftest "textContent passes through" (assert= (hx-normalize-swap-mode "textContent") "textContent"))) ;; ── Swap spec parsing ─────────────────────────────────────────── (defsuite "htmx-parse-swap-spec" (deftest "nil defaults to innerHTML" (let ((spec (hx-parse-swap-spec nil))) (assert= (get spec :mode) "innerHTML"))) (deftest "bare mode" (let ((spec (hx-parse-swap-spec "outerHTML"))) (assert= (get spec :mode) "outerHTML") (assert= (get spec :swap-delay) nil))) (deftest "mode with swap delay" (let ((spec (hx-parse-swap-spec "innerHTML swap:100ms"))) (assert= (get spec :mode) "innerHTML") (assert= (get spec :swap-delay) 100))) (deftest "mode with settle delay" (let ((spec (hx-parse-swap-spec "innerHTML settle:200ms"))) (assert= (get spec :settle-delay) 200))) (deftest "mode with scroll" (let ((spec (hx-parse-swap-spec "innerHTML scroll:top"))) (assert= (get spec :scroll) "top"))) (deftest "v4 alias normalized in spec" (let ((spec (hx-parse-swap-spec "append settle:500ms"))) (assert= (get spec :mode) "beforeend") (assert= (get spec :settle-delay) 500))) (deftest "full spec with multiple modifiers" (let ((spec (hx-parse-swap-spec "innerHTML swap:50ms settle:100ms scroll:top"))) (assert= (get spec :mode) "innerHTML") (assert= (get spec :swap-delay) 50) (assert= (get spec :settle-delay) 100) (assert= (get spec :scroll) "top")))) ;; ── Trigger parsing ───────────────────────────────────────────── (defsuite "htmx-parse-trigger" (deftest "simple event" (let ((spec (hx-parse-trigger "click" nil))) (assert= (get spec :event) "click") (assert= (get spec :delay) nil) (assert= (get spec :once) false))) (deftest "event with delay" (let ((spec (hx-parse-trigger "keyup delay:500ms" nil))) (assert= (get spec :event) "keyup") (assert= (get spec :delay) 500))) (deftest "event with throttle" (let ((spec (hx-parse-trigger "click throttle:1s" nil))) (assert= (get spec :event) "click") (assert= (get spec :throttle) 1000))) (deftest "event with once" (let ((spec (hx-parse-trigger "click once" nil))) (assert= (get spec :event) "click") (assert= (get spec :once) true))) (deftest "event with changed" (let ((spec (hx-parse-trigger "keyup changed delay:500ms" nil))) (assert= (get spec :event) "keyup") (assert= (get spec :changed) true) (assert= (get spec :delay) 500))) (deftest "event with from selector" (let ((spec (hx-parse-trigger "click from:body" nil))) (assert= (get spec :event) "click") (assert= (get spec :from) "body"))) (deftest "event with filter" (let ((spec (hx-parse-trigger "keyup [key=='Enter']" nil))) (assert= (get spec :event) "keyup") (assert= (get spec :filter) "[key=='Enter']"))) (deftest "every trigger" (let ((spec (hx-parse-trigger "every delay:2s" nil))) (assert= (get spec :event) "every") (assert= (get spec :delay) 2000)))) ;; ── URL encoding ──────────────────────────────────────────────── (defsuite "htmx-url-encode" (deftest "encodes single param" (assert= (url-encode-params {:q "search"}) "q=search")) (deftest "encodes numeric values" (assert= (url-encode-params {:page 1}) "page=1"))) (defsuite "htmx-status-matches" (deftest "exact match" (assert= (hx-status-matches? "404" "404") true)) (deftest "exact non-match" (assert= (hx-status-matches? "404" "500") false)) (deftest "1-digit wildcard 5xx matches 503" (assert= (hx-status-matches? "503" "5xx") true)) (deftest "1-digit wildcard 4xx does not match 503" (assert= (hx-status-matches? "503" "4xx") false)) (deftest "2-digit wildcard 50x matches 503" (assert= (hx-status-matches? "503" "50x") true)) (deftest "2-digit wildcard 50x does not match 522" (assert= (hx-status-matches? "522" "50x") false)) (deftest "2xx matches 200" (assert= (hx-status-matches? "200" "2xx") true))) (defsuite "htmx-status-modifiers" (deftest "parses swap target push" (let ((m (hx-parse-status-modifiers "swap:innerHTML target:#errors push:false"))) (assert= (get m :swap) "innerHTML") (assert= (get m :target) "#errors") (assert= (get m :push) "false"))) (deftest "parses transition" (let ((m (hx-parse-status-modifiers "swap:none transition:true"))) (assert= (get m :swap) "none") (assert= (get m :transition) "true")))) (defsuite "htmx-match-status" (deftest "exact match wins over wildcard" (let ((rules (list {:target nil :transition nil :swap "none" :select nil :push nil :specificity 1 :code "5xx" :replace nil} {:target nil :transition nil :swap "outerHTML" :select nil :push nil :specificity 3 :code "503" :replace nil}))) (assert= (get (hx-match-status 503 rules) :swap) "outerHTML"))) (deftest "2-digit wildcard wins over 1-digit" (let ((rules (list {:target nil :transition nil :swap "none" :select nil :push nil :specificity 1 :code "5xx" :replace nil} {:target nil :transition nil :swap "innerHTML" :select nil :push nil :specificity 2 :code "50x" :replace nil}))) (assert= (get (hx-match-status 501 rules) :swap) "innerHTML"))) (deftest "nil when no match" (let ((rules (list {:target nil :transition nil :swap "none" :select nil :push nil :specificity 1 :code "5xx" :replace nil}))) (assert= (hx-match-status 404 rules) nil)))) (defsuite "htmx-sync-spec" (deftest "parses selector:strategy" (let ((s (hx-parse-sync-spec "closest form:abort"))) (assert= (get s :selector) "closest form") (assert= (get s :strategy) "abort"))) (deftest "parses queue with mode" (let ((s (hx-parse-sync-spec "this:queue last"))) (assert= (get s :selector) "this") (assert= (get s :strategy) "queue") (assert= (get s :queue-mode) "last"))) (deftest "defaults to drop strategy" (let ((s (hx-parse-sync-spec "this:drop"))) (assert= (get s :strategy) "drop"))) (deftest "nil for nil input" (assert= (hx-parse-sync-spec nil) nil))) (defsuite "htmx-sse-swap-parse" (deftest "parses single spec" (let ((specs (hx-parse-sse-swap "message:#target"))) (assert= (len specs) 1) (assert= (get (first specs) :event) "message") (assert= (get (first specs) :target) "#target"))) (deftest "parses multiple specs with swap mode" (let ((specs (hx-parse-sse-swap "message:#target,update:#list:outerHTML"))) (assert= (len specs) 2) (assert= (get (nth specs 1) :event) "update") (assert= (get (nth specs 1) :swap) "outerHTML"))) (deftest "nil returns empty list" (assert= (hx-parse-sse-swap nil) (list)))) (defsuite "htmx-swap-spec-v4-modifiers" (deftest "transition modifier" (let ((spec (hx-parse-swap-spec "innerHTML transition:true"))) (assert= (get spec :transition) true) (assert= (get spec :mode) "innerHTML"))) (deftest "strip modifier" (let ((spec (hx-parse-swap-spec "outerHTML strip:true"))) (assert= (get spec :strip) true))) (deftest "target override in swap spec" (let ((spec (hx-parse-swap-spec "innerHTML target:#alt"))) (assert= (get spec :target) "#alt"))) (deftest "ignoreTitle modifier" (let ((spec (hx-parse-swap-spec "innerHTML ignoreTitle:true"))) (assert= (get spec :ignore-title) true))) (deftest "all modifiers together" (let ((spec (hx-parse-swap-spec "append swap:50ms settle:100ms scroll:top transition:true strip:true"))) (assert= (get spec :mode) "beforeend") (assert= (get spec :swap-delay) 50) (assert= (get spec :settle-delay) 100) (assert= (get spec :scroll) "top") (assert= (get spec :transition) true) (assert= (get spec :strip) true))))