signals.sx, engine.sx, orchestration.sx, boot.sx, router.sx, deps.sx, forms.sx, page-helpers.sx, adapters, boundary files → web/ Web tests → web/tests/ Test runners updated with _SPEC_TESTS and _WEB_TESTS paths. All 89 tests pass (20 signal + 43 CEK + 26 CEK reactive). Both bootstrappers build fully (5993 Python lines, 387KB JS). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
217 lines
7.3 KiB
Plaintext
217 lines
7.3 KiB
Plaintext
;; ==========================================================================
|
|
;; test-signals.sx — Tests for signals and reactive islands
|
|
;;
|
|
;; Requires: test-framework.sx loaded first.
|
|
;; Modules tested: signals.sx, eval.sx (defisland)
|
|
;;
|
|
;; Note: Multi-expression lambda bodies are wrapped in (do ...) for
|
|
;; compatibility with the hand-written evaluator which only supports
|
|
;; single-expression lambda bodies.
|
|
;; ==========================================================================
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Signal creation and basic read/write
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defsuite "signal basics"
|
|
(deftest "signal creates a reactive container"
|
|
(let ((s (signal 42)))
|
|
(assert-true (signal? s))
|
|
(assert-equal 42 (deref s))))
|
|
|
|
(deftest "deref on non-signal passes through"
|
|
(assert-equal 5 (deref 5))
|
|
(assert-equal "hello" (deref "hello"))
|
|
(assert-nil (deref nil)))
|
|
|
|
(deftest "reset! changes value"
|
|
(let ((s (signal 0)))
|
|
(reset! s 10)
|
|
(assert-equal 10 (deref s))))
|
|
|
|
(deftest "reset! does not notify when value unchanged"
|
|
(let ((s (signal 5))
|
|
(count (signal 0)))
|
|
(effect (fn () (do (deref s) (swap! count inc))))
|
|
;; Effect runs once on creation → count=1
|
|
(let ((c1 (deref count)))
|
|
(reset! s 5) ;; same value — no notification
|
|
(assert-equal c1 (deref count)))))
|
|
|
|
(deftest "swap! applies function to current value"
|
|
(let ((s (signal 10)))
|
|
(swap! s inc)
|
|
(assert-equal 11 (deref s))))
|
|
|
|
(deftest "swap! passes extra args"
|
|
(let ((s (signal 10)))
|
|
(swap! s + 5)
|
|
(assert-equal 15 (deref s)))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Computed signals
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defsuite "computed"
|
|
(deftest "computed derives initial value"
|
|
(let ((a (signal 3))
|
|
(b (signal 4))
|
|
(sum (computed (fn () (+ (deref a) (deref b))))))
|
|
(assert-equal 7 (deref sum))))
|
|
|
|
(deftest "computed updates when dependency changes"
|
|
(let ((a (signal 2))
|
|
(doubled (computed (fn () (* 2 (deref a))))))
|
|
(assert-equal 4 (deref doubled))
|
|
(reset! a 5)
|
|
(assert-equal 10 (deref doubled))))
|
|
|
|
(deftest "computed chains"
|
|
(let ((base (signal 1))
|
|
(doubled (computed (fn () (* 2 (deref base)))))
|
|
(quadrupled (computed (fn () (* 2 (deref doubled))))))
|
|
(assert-equal 4 (deref quadrupled))
|
|
(reset! base 3)
|
|
(assert-equal 12 (deref quadrupled)))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Effects
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defsuite "effects"
|
|
(deftest "effect runs immediately"
|
|
(let ((ran (signal false)))
|
|
(effect (fn () (reset! ran true)))
|
|
(assert-true (deref ran))))
|
|
|
|
(deftest "effect re-runs when dependency changes"
|
|
(let ((source (signal "a"))
|
|
(log (signal (list))))
|
|
(effect (fn ()
|
|
(swap! log (fn (l) (append l (deref source))))))
|
|
;; Initial run logs "a"
|
|
(assert-equal (list "a") (deref log))
|
|
;; Change triggers re-run
|
|
(reset! source "b")
|
|
(assert-equal (list "a" "b") (deref log))))
|
|
|
|
(deftest "effect dispose stops tracking"
|
|
(let ((source (signal 0))
|
|
(count (signal 0)))
|
|
(let ((dispose (effect (fn () (do
|
|
(deref source)
|
|
(swap! count inc))))))
|
|
;; Effect ran once
|
|
(assert-equal 1 (deref count))
|
|
;; Trigger
|
|
(reset! source 1)
|
|
(assert-equal 2 (deref count))
|
|
;; Dispose
|
|
(dispose)
|
|
;; Should NOT trigger
|
|
(reset! source 2)
|
|
(assert-equal 2 (deref count)))))
|
|
|
|
(deftest "effect cleanup runs before re-run"
|
|
(let ((source (signal 0))
|
|
(cleanups (signal 0)))
|
|
(effect (fn () (do
|
|
(deref source)
|
|
(fn () (swap! cleanups inc))))) ;; return cleanup fn
|
|
;; No cleanup yet (first run)
|
|
(assert-equal 0 (deref cleanups))
|
|
;; Change triggers cleanup of previous run
|
|
(reset! source 1)
|
|
(assert-equal 1 (deref cleanups)))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Batch
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defsuite "batch"
|
|
(deftest "batch defers notifications"
|
|
(let ((a (signal 0))
|
|
(b (signal 0))
|
|
(run-count (signal 0)))
|
|
(effect (fn () (do
|
|
(deref a) (deref b)
|
|
(swap! run-count inc))))
|
|
;; Initial run
|
|
(assert-equal 1 (deref run-count))
|
|
;; Without batch: 2 writes → 2 effect runs
|
|
;; With batch: 2 writes → 1 effect run
|
|
(batch (fn () (do
|
|
(reset! a 1)
|
|
(reset! b 2))))
|
|
;; Should be 2 (initial + 1 batched), not 3
|
|
(assert-equal 2 (deref run-count)))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; defisland
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defsuite "defisland"
|
|
(deftest "defisland creates an island"
|
|
(defisland ~test-island (&key value)
|
|
(list "island" value))
|
|
(assert-true (island? ~test-island)))
|
|
|
|
(deftest "island is callable like component"
|
|
(defisland ~greeting (&key name)
|
|
(str "Hello, " name "!"))
|
|
(assert-equal "Hello, World!" (~greeting :name "World")))
|
|
|
|
(deftest "island accepts children"
|
|
(defisland ~wrapper (&rest children)
|
|
(list "wrap" children))
|
|
(assert-equal (list "wrap" (list "a" "b"))
|
|
(~wrapper "a" "b"))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Scope integration — reactive tracking uses scope-push!/scope-pop!
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(defsuite "scope integration"
|
|
(deftest "deref outside reactive scope does not subscribe"
|
|
(let ((s (signal 42)))
|
|
;; Reading outside any reactive context should not add subscribers
|
|
(assert-equal 42 (deref s))
|
|
(assert-equal 0 (len (signal-subscribers s)))))
|
|
|
|
(deftest "computed uses scope for tracking"
|
|
(let ((a (signal 1))
|
|
(b (signal 2))
|
|
(sum (computed (fn () (+ (deref a) (deref b))))))
|
|
;; Each signal should have exactly 1 subscriber (the computed's recompute)
|
|
(assert-equal 1 (len (signal-subscribers a)))
|
|
(assert-equal 1 (len (signal-subscribers b)))
|
|
;; Verify computed value
|
|
(assert-equal 3 (deref sum))))
|
|
|
|
(deftest "nested effects with overlapping deps use scope correctly"
|
|
(let ((shared (signal 0))
|
|
(inner-only (signal 0))
|
|
(outer-count (signal 0))
|
|
(inner-count (signal 0)))
|
|
;; Outer effect tracks shared
|
|
(effect (fn () (do (deref shared) (swap! outer-count inc))))
|
|
;; Inner effect tracks shared AND inner-only
|
|
(effect (fn () (do (deref shared) (deref inner-only) (swap! inner-count inc))))
|
|
;; Both ran once
|
|
(assert-equal 1 (deref outer-count))
|
|
(assert-equal 1 (deref inner-count))
|
|
;; Changing shared triggers both
|
|
(reset! shared 1)
|
|
(assert-equal 2 (deref outer-count))
|
|
(assert-equal 2 (deref inner-count))
|
|
;; Changing inner-only triggers only inner
|
|
(reset! inner-only 1)
|
|
(assert-equal 2 (deref outer-count))
|
|
(assert-equal 3 (deref inner-count)))))
|