Convert WASM browser tests to SX deftest forms
Tests moved from inline JS assertions to web/tests/test-wasm-browser.sx using the standard deftest/defsuite/assert-equal framework. The JS driver (test_wasm_native.js) now just boots the kernel, loads modules, and runs the SX test file. 15/15 source, 15/15 bytecode. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
// test_wasm_native.js — Run WASM kernel tests in Node.js using the actual
|
||||
// WASM binary (not js_of_ocaml JS fallback). This tests the exact same
|
||||
// kernel that runs in the browser.
|
||||
// WASM binary (not js_of_ocaml JS fallback). Tests are SX deftest forms
|
||||
// in web/tests/test-wasm-browser.sx.
|
||||
//
|
||||
// Usage: node hosts/ocaml/browser/test_wasm_native.js
|
||||
// SX_TEST_BYTECODE=1 node hosts/ocaml/browser/test_wasm_native.js
|
||||
@@ -149,118 +149,37 @@ async function main() {
|
||||
}
|
||||
if (K.endModuleLoad) K.endModuleLoad();
|
||||
|
||||
// --- Test runner ---
|
||||
// --- Register test framework hooks ---
|
||||
let pass = 0, fail = 0;
|
||||
function assert(name, got, expected) {
|
||||
if (got === expected) { pass++; }
|
||||
else { fail++; console.error(`FAIL: ${name}\n got: ${JSON.stringify(got)}\n expected: ${JSON.stringify(expected)}`); }
|
||||
}
|
||||
function assertIncludes(name, got, substr) {
|
||||
if (typeof got === 'string' && got.includes(substr)) { pass++; }
|
||||
else { fail++; console.error(`FAIL: ${name}\n got: ${JSON.stringify(got)}\n expected to include: ${JSON.stringify(substr)}`); }
|
||||
}
|
||||
const suiteStack = [];
|
||||
|
||||
// --- Tests ---
|
||||
K.registerNative('report-pass', args => {
|
||||
pass++;
|
||||
return null;
|
||||
});
|
||||
K.registerNative('report-fail', args => {
|
||||
fail++;
|
||||
const suitePath = suiteStack.join(' > ');
|
||||
console.error(`FAIL: ${suitePath ? suitePath + ' > ' : ''}${args[0]}\n ${args[1]}`);
|
||||
return null;
|
||||
});
|
||||
K.registerNative('push-suite', args => {
|
||||
suiteStack.push(args[0]);
|
||||
return null;
|
||||
});
|
||||
K.registerNative('pop-suite', args => {
|
||||
suiteStack.pop();
|
||||
return null;
|
||||
});
|
||||
// try-call must return {"ok": bool, "error": string|nil} for the test framework
|
||||
K.eval('(define try-call (fn (thunk) (let ((result (cek-try thunk (fn (err) err)))) (if (and (= (type-of result) "string") (starts-with? result "Error")) {"ok" false "error" result} {"ok" true "error" nil}))))');
|
||||
|
||||
const SCOPED_TEST = '(dom-get-attr (let ((d (list))) (with-island-scope (fn (x) (append! d x)) (fn () (render-to-dom (div :class "scoped" "text") (global-env) nil)))) "class")';
|
||||
|
||||
// Basic
|
||||
assert('arithmetic', K.eval('(+ 1 2)'), 3);
|
||||
assert('div preserves keywords', K.eval('(inspect (div :class "test" "hello"))'), '(div :class "test" "hello")');
|
||||
assert('render div+class', K.eval('(render-to-html (div :class "card" "content"))'), '<div class="card">content</div>');
|
||||
// --- Load test framework + SX test file ---
|
||||
K.load(fs.readFileSync(path.join(PROJECT_ROOT, 'spec/tests/test-framework.sx'), 'utf8'));
|
||||
K.load(fs.readFileSync(path.join(PROJECT_ROOT, 'web/tests/test-wasm-browser.sx'), 'utf8'));
|
||||
|
||||
// DOM rendering
|
||||
assert('dom class attr',
|
||||
K.eval('(dom-get-attr (render-to-dom (div :class "test" "hello") (global-env) nil) "class")'),
|
||||
'test');
|
||||
|
||||
// Reactive: scoped static class
|
||||
assert('scoped static class',
|
||||
K.eval(SCOPED_TEST), 'scoped');
|
||||
|
||||
// Reactive: signal deref initial value in scope
|
||||
assert('signal attr initial value',
|
||||
K.eval('(dom-get-attr (let ((s (signal "active")) (d (list))) (with-island-scope (fn (x) (append! d x)) (fn () (render-to-dom (div :class (deref s) "content") (global-env) nil)))) "class")'),
|
||||
'active');
|
||||
|
||||
// Reactive: signal text in scope
|
||||
assertIncludes('signal text in scope',
|
||||
K.eval('(host-get (let ((s (signal 42)) (d (list))) (with-island-scope (fn (x) (append! d x)) (fn () (render-to-dom (div (deref s)) (global-env) nil)))) "outerHTML")'),
|
||||
'42');
|
||||
|
||||
// CRITICAL: define vs let closure with host objects + effect
|
||||
// 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 (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');
|
||||
|
||||
// Verify the effect body ACTUALLY EXECUTES (not just returning a value).
|
||||
// In browser WASM, the effect body silently doesn't run.
|
||||
assert('define+effect body executes',
|
||||
K.eval('(do (define test-fx-log (fn (el log) (effect (fn () (append! log "ran") (dom-set-attr el "class" "fx"))))) (let ((el (dom-create-element "div" nil)) (log (list))) (test-fx-log el log) (str (len log) ":" (first log))))'),
|
||||
'1:ran');
|
||||
|
||||
// Same thing with let works (proves it's define-specific)
|
||||
assert('let+effect+host-obj',
|
||||
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"))))) (dom-get-attr el "class"))'),
|
||||
'sx-text-center');
|
||||
|
||||
// Full hydration pattern: render-to-dom with CSSX inside island scope
|
||||
assertIncludes('render-to-dom CSSX in island scope',
|
||||
K.eval("(host-get (let ((d (list))) (with-island-scope (fn (x) (append! d x)) (fn () (render-to-dom '(div (~cssx/tw :tokens \"text-center font-bold\") \"hello\") (global-env) nil)))) \"outerHTML\")"),
|
||||
'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).
|
||||
K.eval('(define test-reactive-sig (signal "before"))');
|
||||
assert('reactive attr update',
|
||||
K.eval("(let ((d (list))) (let ((el (with-island-scope (fn (x) (append! d x)) (fn () (render-to-dom '(div :class (deref test-reactive-sig) \"content\") (global-env) nil))))) (reset! test-reactive-sig \"after\") (dom-get-attr el \"class\")))"),
|
||||
'after');
|
||||
|
||||
// =====================================================================
|
||||
// Section: Boot step bisection
|
||||
// Simulate boot steps to find which one breaks scoped rendering
|
||||
// =====================================================================
|
||||
if (process.env.SX_TEST_BOOT_BISECT === '1') {
|
||||
console.log('\n=== Boot step bisection ===');
|
||||
const bootSteps = [
|
||||
['init-css-tracking', '(init-css-tracking)'],
|
||||
['process-page-scripts', '(process-page-scripts)'],
|
||||
// process-sx-scripts needs <script type="text/sx"> in DOM — skip in Node
|
||||
];
|
||||
for (const [name, expr] of bootSteps) {
|
||||
const before = K.eval(SCOPED_TEST);
|
||||
K.eval(expr);
|
||||
const after = K.eval(SCOPED_TEST);
|
||||
console.log(` ${name}: before=${before} after=${after} ${before !== after ? '*** CHANGED ***' : 'ok'}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
// --- Summary ---
|
||||
console.log(`WASM native tests: ${pass} passed, ${fail} failed`);
|
||||
process.exit(fail > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
184
web/tests/test-wasm-browser.sx
Normal file
184
web/tests/test-wasm-browser.sx
Normal file
@@ -0,0 +1,184 @@
|
||||
(define
|
||||
test-set-attr-sep
|
||||
(fn (el name val) (effect (fn () (dom-set-attr el name val)))))
|
||||
|
||||
(define
|
||||
test-set-attr-mod
|
||||
(fn (el name val) (effect (fn () (dom-set-attr el name val)))))
|
||||
|
||||
(define test-reactive-sig (signal "before"))
|
||||
|
||||
(defsuite
|
||||
"wasm-basic"
|
||||
(deftest "arithmetic" (assert-equal 3 (+ 1 2)))
|
||||
(deftest
|
||||
"div preserves keywords"
|
||||
(assert-equal
|
||||
"(div :class \"test\" \"hello\")"
|
||||
(inspect (div :class "test" "hello"))))
|
||||
(deftest
|
||||
"render div+class"
|
||||
(assert-equal
|
||||
"<div class=\"card\">content</div>"
|
||||
(render-to-html (div :class "card" "content")))))
|
||||
|
||||
(defsuite
|
||||
"wasm-dom-rendering"
|
||||
(deftest
|
||||
"dom class attr"
|
||||
(assert-equal
|
||||
"test"
|
||||
(dom-get-attr
|
||||
(render-to-dom (div :class "test" "hello") (global-env) nil)
|
||||
"class"))))
|
||||
|
||||
(defsuite
|
||||
"wasm-scoped"
|
||||
(deftest
|
||||
"static class in island scope"
|
||||
(assert-equal
|
||||
"scoped"
|
||||
(dom-get-attr
|
||||
(let
|
||||
((d (list)))
|
||||
(with-island-scope
|
||||
(fn (x) (append! d x))
|
||||
(fn
|
||||
()
|
||||
(render-to-dom (div :class "scoped" "text") (global-env) nil))))
|
||||
"class")))
|
||||
(deftest
|
||||
"signal attr initial value"
|
||||
(assert-equal
|
||||
"active"
|
||||
(dom-get-attr
|
||||
(let
|
||||
((s (signal "active")) (d (list)))
|
||||
(with-island-scope
|
||||
(fn (x) (append! d x))
|
||||
(fn
|
||||
()
|
||||
(render-to-dom
|
||||
(div :class (deref s) "content")
|
||||
(global-env)
|
||||
nil))))
|
||||
"class")))
|
||||
(deftest
|
||||
"signal text in scope"
|
||||
(assert-true
|
||||
(contains?
|
||||
(host-get
|
||||
(let
|
||||
((s (signal 42)) (d (list)))
|
||||
(with-island-scope
|
||||
(fn (x) (append! d x))
|
||||
(fn () (render-to-dom (div (deref s)) (global-env) nil))))
|
||||
"outerHTML")
|
||||
"42"))))
|
||||
|
||||
(defsuite
|
||||
"wasm-define-effect"
|
||||
(deftest
|
||||
"define+effect+host-obj (same eval)"
|
||||
(assert-equal
|
||||
"from-define"
|
||||
(do
|
||||
(define
|
||||
test-set-attr-inline
|
||||
(fn (el name val) (effect (fn () (dom-set-attr el name val)))))
|
||||
(let
|
||||
((el (dom-create-element "div" nil)))
|
||||
(test-set-attr-inline el "class" "from-define")
|
||||
(dom-get-attr el "class")))))
|
||||
(deftest
|
||||
"define+effect body executes"
|
||||
(assert-equal
|
||||
"1:ran"
|
||||
(do
|
||||
(define
|
||||
test-fx-log-inline
|
||||
(fn
|
||||
(el log)
|
||||
(effect
|
||||
(fn () (append! log "ran") (dom-set-attr el "class" "fx")))))
|
||||
(let
|
||||
((el (dom-create-element "div" nil)) (log (list)))
|
||||
(test-fx-log-inline el log)
|
||||
(str (len log) ":" (first log))))))
|
||||
(deftest
|
||||
"let+effect+host-obj"
|
||||
(assert-equal
|
||||
"from-let"
|
||||
(let
|
||||
((test-set-attr-let (fn (el name val) (effect (fn () (dom-set-attr el name val))))))
|
||||
(let
|
||||
((el (dom-create-element "div" nil)))
|
||||
(test-set-attr-let el "class" "from-let")
|
||||
(dom-get-attr el "class"))))))
|
||||
|
||||
(defsuite
|
||||
"wasm-module-loaded"
|
||||
(deftest
|
||||
"define+effect+host-obj (separate eval)"
|
||||
(assert-equal
|
||||
"from-sep-define"
|
||||
(let
|
||||
((el (dom-create-element "div" nil)))
|
||||
(test-set-attr-sep el "class" "from-sep-define")
|
||||
(dom-get-attr el "class"))))
|
||||
(deftest
|
||||
"define+effect+host-obj (module-loaded)"
|
||||
(assert-equal
|
||||
"from-mod"
|
||||
(let
|
||||
((el (dom-create-element "div" nil)))
|
||||
(test-set-attr-mod el "class" "from-mod")
|
||||
(dom-get-attr el "class")))))
|
||||
|
||||
(defsuite
|
||||
"wasm-reactive"
|
||||
(deftest
|
||||
"reactive-spread from module"
|
||||
(assert-equal
|
||||
"sx-text-center"
|
||||
(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")))))
|
||||
(dom-get-attr el "class"))))
|
||||
(deftest
|
||||
"render-to-dom CSSX in island scope"
|
||||
(assert-true
|
||||
(contains?
|
||||
(host-get
|
||||
(let
|
||||
((d (list)))
|
||||
(with-island-scope
|
||||
(fn (x) (append! d x))
|
||||
(fn
|
||||
()
|
||||
(render-to-dom
|
||||
(quote
|
||||
(div (~cssx/tw :tokens "text-center font-bold") "hello"))
|
||||
(global-env)
|
||||
nil))))
|
||||
"outerHTML")
|
||||
"sx-text-center"))))
|
||||
|
||||
(defsuite
|
||||
"wasm-signal-propagation"
|
||||
(deftest
|
||||
"reactive attr update"
|
||||
(do
|
||||
(set! test-reactive-sig (signal "before"))
|
||||
(assert-equal
|
||||
"after"
|
||||
(let
|
||||
((d (list)))
|
||||
(let
|
||||
((el (with-island-scope (fn (x) (append! d x)) (fn () (render-to-dom (quote (div :class (deref test-reactive-sig) "content")) (global-env) nil)))))
|
||||
(reset! test-reactive-sig "after")
|
||||
(dom-get-attr el "class")))))))
|
||||
Reference in New Issue
Block a user