New spec tests: test-cek-try-seq (CEK try/seq), test-htmx (htmx directive coverage, 292L), test-hs-diag, test-perform-chain (IO suspension chains). tests/hs-*.js: Node.js-side hyperscript runners for browser-mode testing (hs-behavioral-node, hs-behavioral-runner, hs-parse-audit, hs-run-timed). Vendors shared/static/scripts/htmx.min.js. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
292 lines
10 KiB
Plaintext
292 lines
10 KiB
Plaintext
;; 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)))) |