;; ========================================================================== ;; test-unified-reactive.sx — Tests for step 10c unified reactive model ;; ;; Requires: test-framework.sx, signals.sx loaded first. ;; ;; Tests the unified reactive model where: ;; - provide stores values in reactive cells (signals internally) ;; - context reads cells; auto-subscribes inside tracking contexts ;; - peek reads cells without subscribing ;; - provide! mutates cells and notifies subscribers ;; - bind creates a tracking context — re-evaluates body on change ;; ;; signal/deref/computed/effect remain unchanged and complementary. ;; ========================================================================== ;; -------------------------------------------------------------------------- ;; provide creates reactive cells ;; -------------------------------------------------------------------------- (defsuite "provide-reactive-cell" (deftest "provide scopes value to body" (assert-equal 42 (provide :x 42 (context :x)))) (deftest "provide multiple keys" (provide :x 1 (provide :y 2 (assert-equal 3 (+ (context :x) (context :y)))))) (deftest "provide nested shadow" (provide :x 1 (assert-equal 1 (context :x)) (provide :x 2 (assert-equal 2 (context :x))) (assert-equal 1 (context :x)))) (deftest "provide nil value is valid" (provide :x nil (assert-equal nil (context :x)))) (deftest "provide dict value" (provide :data {:age 30 :name "alice"} (assert-equal "alice" (get (context :data) "name")))) (deftest "provide lambda value" (provide :handler (fn (x) (* x 2)) (assert-equal 10 ((context :handler) 5)))) (deftest "provide deep nesting" (provide :a 1 (provide :b 2 (provide :c 3 (provide :d 4 (provide :e 5 (assert-equal 15 (+ (context :a) (context :b) (context :c) (context :d) (context :e))))))))) (deftest "provide overwrites in same scope" (provide :x 1 (provide :x 2 (assert-equal 2 (context :x))))) (deftest "provide with empty body returns nil" (assert-equal nil (provide :x 1)))) ;; -------------------------------------------------------------------------- ;; context reads — cold and tracked ;; -------------------------------------------------------------------------- (defsuite "context-read" (deftest "context returns provided value" (provide :name "alice" (assert-equal "alice" (context :name)))) (deftest "context missing key returns nil" (assert-equal nil (context :nonexistent))) (deftest "context missing key with default" (assert-equal "fallback" (context :nonexistent "fallback"))) (deftest "context finds nearest provide" (provide :x "outer" (provide :x "inner" (assert-equal "inner" (context :x))))) (deftest "context in let binding" (provide :x 10 (let ((v (context :x))) (assert-equal 10 v)))) (deftest "context in lambda" (provide :x 42 (let ((f (fn () (context :x)))) (assert-equal 42 (f))))) (deftest "context in map" (provide :prefix "item-" (assert-equal (list "item-a" "item-b") (map (fn (x) (str (context :prefix) x)) (list "a" "b"))))) (deftest "context with keyword name" (provide :my-key 99 (assert-equal 99 (context :my-key))))) ;; -------------------------------------------------------------------------- ;; peek — cold read, never subscribes ;; -------------------------------------------------------------------------- (defsuite "peek-cold-read" (deftest "peek returns current value" (provide :x 42 (assert-equal 42 (peek :x)))) (deftest "peek missing key returns nil" (assert-equal nil (peek :nonexistent))) (deftest "peek missing key with default" (assert-equal "default" (peek :nonexistent "default"))) (deftest "peek finds nearest provide" (provide :x "outer" (provide :x "inner" (assert-equal "inner" (peek :x))))) (deftest "peek sees updated value after provide!" (provide :x 1 (provide! :x 2) (assert-equal 2 (peek :x)))) (deftest "peek does not subscribe" (provide :x 1 (provide :y 0 (let ((count (signal 0))) (bind (do (let ((peeked (peek :x))) (reset! count (+ 1 (deref count)))) nil)) (assert-equal 1 (deref count)) (provide! :x 2) (assert-equal 1 (deref count))))))) ;; -------------------------------------------------------------------------- ;; provide! — mutate and notify ;; -------------------------------------------------------------------------- (defsuite "provide-mutation" (deftest "provide! updates value" (provide :x 1 (provide! :x 2) (assert-equal 2 (context :x)))) (deftest "provide! multiple times" (provide :x 1 (provide! :x 2) (provide! :x 3) (assert-equal 3 (context :x)))) (deftest "provide! nil is valid" (provide :x 1 (provide! :x nil) (assert-equal nil (context :x)))) (deftest "provide! inner scope does not affect outer" (provide :x 1 (provide :x 10 (provide! :x 20)) (assert-equal 1 (context :x)))) (deftest "provide! to string value" (provide :msg "hello" (provide! :msg "world") (assert-equal "world" (context :msg)))) (deftest "provide! to list value" (provide :items (list 1 2) (provide! :items (list 1 2 3)) (assert-equal (list 1 2 3) (context :items)))) (deftest "provide! with computed new value" (provide :count 0 (provide! :count (+ 1 (peek :count))) (assert-equal 1 (context :count)) (provide! :count (+ 1 (peek :count))) (assert-equal 2 (context :count))))) ;; -------------------------------------------------------------------------- ;; bind — tracking context ;; -------------------------------------------------------------------------- (defsuite "bind-tracking" (deftest "bind returns initial value" (provide :x 10 (assert-equal 10 (bind (context :x))))) (deftest "bind with expression" (provide :x 3 (assert-equal 9 (bind (* (context :x) (context :x)))))) (deftest "bind re-evaluates on provide!" (provide :x 1 (let ((log (signal (list)))) (bind (do (swap! log (fn (l) (append l (list (context :x))))) nil)) (assert-equal (list 1) (deref log)) (provide! :x 2) (assert-equal (list 1 2) (deref log))))) (deftest "bind tracks multiple keys" (provide :x 1 (provide :y 10 (let ((log (signal (list)))) (bind (do (swap! log (fn (l) (append l (list (+ (context :x) (context :y)))))) nil)) (assert-equal (list 11) (deref log)) (provide! :x 2) (assert-equal (list 11 12) (deref log)) (provide! :y 20) (assert-equal (list 11 12 22) (deref log)))))) (deftest "bind does not fire on unrelated provide!" (provide :x 1 (provide :y 100 (let ((count (signal 0))) (bind (do (let ((v (context :x))) (reset! count (+ 1 (deref count)))) nil)) (assert-equal 1 (deref count)) (provide! :y 200) (assert-equal 1 (deref count)) (provide! :x 2) (assert-equal 2 (deref count)))))) (deftest "bind with let" (provide :x 5 (assert-equal "value: 5" (bind (let ((v (context :x))) (str "value: " v)))))) (deftest "bind no deps is static" (let ((count (signal 0))) (bind (do (reset! count (+ 1 (deref count))) "static")) (assert-equal 1 (deref count)))) (deftest "bind with conditional deps" (provide :flag true (provide :a "yes" (provide :b "no" (assert-equal "yes" (bind (if (context :flag) (context :a) (context :b))))))))) ;; -------------------------------------------------------------------------- ;; bind + provide! interaction — re-evaluation semantics ;; -------------------------------------------------------------------------- (defsuite "bind-provide-interaction" (deftest "bind sees latest value after provide!" (provide :x 1 (let ((latest (signal nil))) (bind (do (reset! latest (context :x)) nil)) (assert-equal 1 (deref latest)) (provide! :x 42) (assert-equal 42 (deref latest))))) (deftest "provide! from within callback pattern" (provide :count 0 (let ((increment (fn () (provide! :count (+ 1 (peek :count)))))) (let ((log (signal (list)))) (bind (do (swap! log (fn (l) (append l (list (context :count))))) nil)) (assert-equal (list 0) (deref log)) (increment) (assert-equal (list 0 1) (deref log)) (increment) (assert-equal (list 0 1 2) (deref log)))))) (deftest "multiple binds on same key" (provide :x 1 (let ((log-a (signal (list))) (log-b (signal (list)))) (bind (do (swap! log-a (fn (l) (append l (list (context :x))))) nil)) (bind (do (swap! log-b (fn (l) (append l (list (* 10 (context :x)))))) nil)) (assert-equal (list 1) (deref log-a)) (assert-equal (list 10) (deref log-b)) (provide! :x 2) (assert-equal (list 1 2) (deref log-a)) (assert-equal (list 10 20) (deref log-b))))) (deftest "provide! same value does not notify" (provide :x 1 (let ((count (signal 0))) (bind (do (let ((v (context :x))) (reset! count (+ 1 (deref count)))) nil)) (assert-equal 1 (deref count)) (provide! :x 1) (assert-equal 1 (deref count)))))) ;; -------------------------------------------------------------------------- ;; nested bind ;; -------------------------------------------------------------------------- (defsuite "bind-nesting" (deftest "nested bind tracks independently" (provide :x 1 (provide :y 10 (let ((outer-log (signal (list))) (inner-log (signal (list)))) (bind (do (swap! outer-log (fn (l) (append l (list (context :x))))) (bind (do (swap! inner-log (fn (l) (append l (list (context :y))))) nil)) nil)) (assert-equal (list 1) (deref outer-log)) (assert-equal (list 10) (deref inner-log)) (provide! :y 20) (assert-equal (list 1) (deref outer-log)) (assert-equal (list 10 20) (deref inner-log)))))) (deftest "bind inside provide scope" (provide :x 1 (provide :y 2 (let ((result (signal nil))) (bind (do (reset! result (+ (context :x) (context :y))) nil)) (assert-equal 3 (deref result)) (provide! :y 10) (assert-equal 11 (deref result))))))) ;; -------------------------------------------------------------------------- ;; batching with unified model ;; -------------------------------------------------------------------------- (defsuite "unified-batch" (deftest "batch coalesces provide! notifications" (provide :x 1 (let ((count (signal 0))) (bind (do (let ((v (context :x))) (reset! count (+ 1 (deref count)))) nil)) (assert-equal 1 (deref count)) (batch (fn () (do (provide! :x 2) (provide! :x 3) (provide! :x 4)))) (assert-equal 2 (deref count)) (assert-equal 4 (context :x))))) (deftest "batch with multiple keys" (provide :x 0 (provide :y 0 (let ((count (signal 0))) (bind (do (let ((sum (+ (context :x) (context :y)))) (reset! count (+ 1 (deref count)))) nil)) (assert-equal 1 (deref count)) (batch (fn () (do (provide! :x 10) (provide! :y 20)))) (assert-equal 2 (deref count))))))) ;; -------------------------------------------------------------------------- ;; disposal and lifecycle ;; -------------------------------------------------------------------------- (defsuite "unified-disposal" (deftest "provide scope exit cleans up" (do (provide :temp 1 (assert-equal 1 (context :temp))) (assert-equal nil (context :temp)))) (deftest "bind in provide scope disposes on exit" (provide :x 1 (let ((count (signal 0))) (provide :y 10 (bind (do (let ((v (context :y))) (reset! count (+ 1 (deref count)))) nil)) (assert-equal 1 (deref count)) (provide! :y 20) (assert-equal 2 (deref count))) (assert-equal 2 (deref count)))))) ;; -------------------------------------------------------------------------- ;; backward compatibility — signal/deref still work ;; -------------------------------------------------------------------------- (defsuite "unified-backward-compat" (deftest "signal and deref still work" (let ((s (signal 42))) (assert-equal 42 (deref s)) (reset! s 100) (assert-equal 100 (deref s)))) (deftest "computed still works" (let ((s (signal 3))) (let ((doubled (computed (fn () (* 2 (deref s)))))) (assert-equal 6 (deref doubled)) (reset! s 5) (assert-equal 10 (deref doubled))))) (deftest "effect still works" (let ((s (signal "a")) (log (signal (list)))) (effect (fn () (swap! log (fn (l) (append l (list (deref s))))))) (assert-equal (list "a") (deref log)) (reset! s "b") (assert-equal (list "a" "b") (deref log)))) (deftest "signal inside provide" (provide :label "hello" (let ((count (signal 0))) (assert-equal "hello" (context :label)) (assert-equal 0 (deref count)) (reset! count 1) (assert-equal 1 (deref count)) (assert-equal "hello" (context :label)))))) ;; -------------------------------------------------------------------------- ;; edge cases ;; -------------------------------------------------------------------------- (defsuite "desugared-reactive" (deftest "counter without signals" (provide :count 0 (provide! :count (+ 1 (context :count))) (assert-equal 1 (context :count)))) (deftest "toggle without signals" (provide :on false (provide! :on (not (peek :on))) (assert-equal true (context :on)))) (deftest "derived value via bind replaces computed" (provide :x 3 (assert-equal 6 (bind (* 2 (context :x)))))) (deftest "bind re-eval replaces computed+effect" (provide :x 1 (let ((log (signal (list)))) (bind (do (swap! log (fn (l) (append l (list (context :x))))) nil)) (assert-equal (list 1) (deref log)) (provide! :x 2) (assert-equal (list 1 2) (deref log))))) (deftest "read-modify-write with peek" (provide :items (list "a" "b") (provide! :items (append (peek :items) (list "c"))) (assert-equal (list "a" "b" "c") (context :items)))) (deftest "nested provide replaces multiple signals" (provide :x 1 (provide :y 2 (assert-equal 3 (+ (context :x) (context :y)))))) (deftest "provide! with bind replaces swap!+computed" (provide :count 0 (let ((doubled (bind (* 2 (context :count))))) (assert-equal 0 doubled) (provide! :count 5) (assert-equal 0 doubled)))) (deftest "batch works with desugared pattern" (provide :x 0 (let ((calls (signal 0))) (bind (do (swap! calls (fn (n) (+ n 1))) (context :x) nil)) (assert-equal 1 (deref calls)) (batch (fn () (provide! :x 1) (provide! :x 2) (provide! :x 3))) (assert-equal 2 (deref calls)))))) (defsuite "unified-edge-cases" (deftest "provide across lambda boundary" (provide :theme "dark" (let ((get-theme (fn () (context :theme)))) (assert-equal "dark" (get-theme))))) (deftest "provide! with peek for read-modify-write" (provide :items (list) (provide! :items (append (peek :items) (list "a"))) (provide! :items (append (peek :items) (list "b"))) (assert-equal (list "a" "b") (context :items)))) (deftest "context in higher-order form" (provide :multiplier 3 (assert-equal (list 3 6 9) (map (fn (x) (* x (context :multiplier))) (list 1 2 3))))) (deftest "context in filter" (provide :threshold 5 (assert-equal (list 6 7 8) (filter (fn (x) (> x (context :threshold))) (list 3 4 5 6 7 8))))) (deftest "provide string key coercion" (provide :my-key 42 (assert-equal 42 (context :my-key)))) (deftest "guard inside bind" (provide :x 1 (assert-equal 1 (bind (guard (exn (true -1)) (context :x)))))) (deftest "bind with string-append" (provide :first "hello" (provide :second "world" (assert-equal "hello world" (bind (str (context :first) " " (context :second))))))))