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