Inline test runner: 5/5 temperature converter tests passing

Fixed three fundamental issues:
1. cek-try arg passing: handler was called with raw string instead of
   (List [String msg]), causing "lambda expects 1 args, got N" errors
2. Silent island hydration failures: hydrate-island now wraps body
   render in cek-try, displaying red error box with stack trace instead
   of empty div. No more silent failures.
3. swap! thunk leak: apply result wasn't trampolined, storing thunks
   as signal values instead of evaluated results

Also fixed: assert= uses = instead of equal? for value comparison,
assert-signal-value uses deref instead of signal-value, HTML entity
decoding in script tag test source via host-call replaceAll.

Temperature converter demo page now shows live test results:
  ✓ initial celsius is 20
  ✓ computed fahrenheit = celsius * 1.8 + 32
  ✓ +5 increments celsius
  ✓ fahrenheit updates on celsius change
  ✓ multiple clicks accumulate

1116/1116 OCaml tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 01:51:30 +00:00
parent 7a8a166326
commit b53a0fabea
8 changed files with 88 additions and 1444 deletions

View File

@@ -1,115 +1,21 @@
;; ==========================================================================
;; web/harness-reactive.sx — Signal and reactive testing extensions
;;
;; Extends spec/harness.sx with assertions for the reactive signal system.
;; Depends on spec/signals.sx (core reactive primitives).
;; No DOM dependency — works on any host.
;; ==========================================================================
(define assert-signal-value :effects () (fn ((sig :as any) expected) (let ((actual (deref sig))) (assert= actual expected (str "Expected signal value " expected ", got " actual)))))
(define assert-signal-has-subscribers :effects () (fn ((sig :as any)) (assert (> (len (signal-subscribers sig)) 0) "Expected signal to have subscribers")))
;; --------------------------------------------------------------------------
;; Signal assertions
;; --------------------------------------------------------------------------
(define assert-signal-no-subscribers :effects () (fn ((sig :as any)) (assert (= (len (signal-subscribers sig)) 0) "Expected signal to have no subscribers")))
;; Assert a signal has a specific value
(define assert-signal-value :effects []
(fn ((sig :as any) expected)
(let ((actual (signal-value sig)))
(assert= actual expected
(str "Expected signal value " expected ", got " actual)))))
(define assert-signal-subscriber-count :effects () (fn ((sig :as any) (n :as number)) (let ((actual (len (signal-subscribers sig)))) (assert= actual n (str "Expected " n " subscribers, got " actual)))))
;; Assert a signal has subscribers (i.e., something is watching it)
(define assert-signal-has-subscribers :effects []
(fn ((sig :as any))
(assert (> (len (signal-subscribers sig)) 0)
"Expected signal to have subscribers")))
(define simulate-signal-set! :effects (mutation) (fn ((sig :as any) value) (reset! sig value)))
;; Assert a signal has no subscribers
(define assert-signal-no-subscribers :effects []
(fn ((sig :as any))
(assert (= (len (signal-subscribers sig)) 0)
"Expected signal to have no subscribers")))
(define simulate-signal-swap! :effects (mutation) (fn ((sig :as any) (f :as lambda) &rest args) (apply swap! (cons sig (cons f args)))))
;; Assert a signal has exactly N subscribers
(define assert-signal-subscriber-count :effects []
(fn ((sig :as any) (n :as number))
(let ((actual (len (signal-subscribers sig))))
(assert= actual n
(str "Expected " n " subscribers, got " actual)))))
(define assert-computed-dep-count :effects () (fn ((sig :as any) (n :as number)) (let ((actual (len (signal-deps sig)))) (assert= actual n (str "Expected " n " deps, got " actual)))))
(define assert-computed-depends-on :effects () (fn ((computed-sig :as any) (dep-sig :as any)) (assert (contains? (signal-deps computed-sig) dep-sig) "Expected computed to depend on the given signal")))
;; --------------------------------------------------------------------------
;; Signal simulation
;; --------------------------------------------------------------------------
(define count-effect-runs :effects (mutation) (fn ((thunk :as lambda)) (let ((count (signal 0))) (effect (fn () (deref count))) (let ((run-count 0) (tracker (effect (fn () (set! run-count (+ run-count 1)) (cek-call thunk nil))))) run-count))))
;; Set a signal value directly (like a user action would)
(define simulate-signal-set! :effects [mutation]
(fn ((sig :as any) value)
(reset! sig value)))
(define make-test-signal :effects (mutation) (fn (initial-value) (let ((sig (signal initial-value)) (history (list))) (effect (fn () (append! history (deref sig)))) {:signal sig :history history})))
;; Swap a signal value (like a button click handler would)
(define simulate-signal-swap! :effects [mutation]
(fn ((sig :as any) (f :as lambda) &rest args)
(apply swap! (cons sig (cons f args)))))
;; --------------------------------------------------------------------------
;; Computed assertions
;; --------------------------------------------------------------------------
;; Assert a computed signal tracks the expected number of dependencies
(define assert-computed-dep-count :effects []
(fn ((sig :as any) (n :as number))
(let ((actual (len (signal-deps sig))))
(assert= actual n
(str "Expected " n " deps, got " actual)))))
;; Assert a computed signal depends on a specific signal
(define assert-computed-depends-on :effects []
(fn ((computed-sig :as any) (dep-sig :as any))
(assert (contains? (signal-deps computed-sig) dep-sig)
"Expected computed to depend on the given signal")))
;; --------------------------------------------------------------------------
;; Effect tracking
;; --------------------------------------------------------------------------
;; Run a function and count how many times an effect fires
(define count-effect-runs :effects [mutation]
(fn ((thunk :as lambda))
(let ((count (signal 0)))
(effect (fn () (deref count))) ;; subscribe to count changes
(let ((run-count 0)
(tracker (effect (fn ()
(set! run-count (+ run-count 1))
(cek-call thunk nil)))))
run-count))))
;; Create a signal + effect pair for testing, returns dict with :signal, :history
(define make-test-signal :effects [mutation]
(fn (initial-value)
(let ((sig (signal initial-value))
(history (list)))
(effect (fn ()
(append! history (deref sig))))
{:signal sig :history history})))
;; --------------------------------------------------------------------------
;; Batch assertions
;; --------------------------------------------------------------------------
;; Assert that a batch of signal writes only triggers N subscriber notifications
(define assert-batch-coalesces :effects [mutation]
(fn ((thunk :as lambda) (expected-notify-count :as number))
(let ((notify-count 0)
(sig (signal 0)))
(effect (fn ()
(deref sig)
(set! notify-count (+ notify-count 1))))
;; Initial effect run counts as 1
(set! notify-count 0)
(batch thunk)
(assert= notify-count expected-notify-count
(str "Expected " expected-notify-count " notifications, got " notify-count)))))
(define assert-batch-coalesces :effects (mutation) (fn ((thunk :as lambda) (expected-notify-count :as number)) (let ((notify-count 0) (sig (signal 0))) (effect (fn () (deref sig) (set! notify-count (+ notify-count 1)))) (set! notify-count 0) (batch thunk) (assert= notify-count expected-notify-count (str "Expected " expected-notify-count " notifications, got " notify-count)))))