From 8fd01c2ab0b0669f57f0c2cd0fdb2973b123017e Mon Sep 17 00:00:00 2001 From: giles Date: Tue, 31 Mar 2026 15:32:59 +0000 Subject: [PATCH] Add failing test: reactive-spread from module-loaded define MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit reactive-spread (module-loaded via K.load) doesn't apply CSSX classes to DOM elements. Returns null instead of "sx-text-center". This is the root cause of hydration stripping all styling — every rendering function is define-bound from module load. 12 pass, 1 fail — the reactive-spread test is the blocker. Co-Authored-By: Claude Opus 4.6 (1M context) --- hosts/ocaml/browser/test_wasm_native.js | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/hosts/ocaml/browser/test_wasm_native.js b/hosts/ocaml/browser/test_wasm_native.js index a8c84e98..f8f3aa04 100644 --- a/hosts/ocaml/browser/test_wasm_native.js +++ b/hosts/ocaml/browser/test_wasm_native.js @@ -192,7 +192,7 @@ async function main() { // This is the root cause of the hydration rendering bug. // A function defined with `define` that takes a host object (DOM element) // and uses `effect` to modify it — the effect body doesn't see the element. - assert('define+effect+host-obj', + assert('define+effect+host-obj (same eval)', K.eval('(do (define test-set-attr (fn (el name val) (effect (fn () (dom-set-attr el name val))))) (let ((el (dom-create-element "div" nil))) (test-set-attr el "class" "from-define") (dom-get-attr el "class")))'), 'from-define'); @@ -201,6 +201,27 @@ async function main() { K.eval('(let ((test-set-attr (fn (el name val) (effect (fn () (dom-set-attr el name val)))))) (let ((el (dom-create-element "div" nil))) (test-set-attr el "class" "from-let") (dom-get-attr el "class")))'), 'from-let'); + // CRITICAL: define in separate eval (matches real module-load pattern). + // This is how reactive-spread/reactive-attr work: defined at module load, + // called later during hydration. The effect closure must capture host objects. + K.eval('(define test-set-attr-sep (fn (el name val) (effect (fn () (dom-set-attr el name val)))))'); + assert('define+effect+host-obj (separate eval)', + K.eval('(let ((el (dom-create-element "div" nil))) (test-set-attr-sep el "class" "from-sep-define") (dom-get-attr el "class"))'), + 'from-sep-define'); + + // Module-loaded define (via K.load, same as real module loading). + // reactive-spread is loaded this way — test that effect fires. + K.load('(define test-set-attr-mod (fn (el name val) (effect (fn () (dom-set-attr el name val)))))'); + assert('define+effect+host-obj (module-loaded)', + K.eval('(let ((el (dom-create-element "div" nil))) (test-set-attr-mod el "class" "from-mod") (dom-get-attr el "class"))'), + 'from-mod'); + + // The actual reactive-spread pattern: module-loaded function creates effect + // that calls cek-call on a render-fn returning a spread, then applies attrs. + assert('reactive-spread from module', + K.eval('(let ((el (dom-create-element "div" nil)) (d (list))) (with-island-scope (fn (x) (append! d x)) (fn () (reactive-spread el (fn () (~cssx/tw :tokens "text-center"))))) (host-get el "className"))'), + 'sx-text-center'); + // Reactive: signal update propagation // Note: render-to-dom needs the UNEVALUATED expression (as in real browser boot // where expressions come from parsing). Use quote to prevent eager eval of (deref s).