HS tests: VM step limit fix, callFn error propagation, compiler emit-set fixes
- sx_vm.ml: VM timeout now compares vm_insn_count > step_limit instead of
unconditionally throwing after 65536 instructions when limit > 0
- sx_browser.ml: Expose setStepLimit/resetStepCount APIs on SxKernel;
callFn now returns {__sx_error, message} on Eval_error instead of null
- compiler.sx: emit-set handles array-index targets (host-set! instead of
nth) and 'of' property chains (dom-set-prop with chain navigation)
- hs-run-fast.js: New Node.js test runner with step-limit timeouts,
SX-level guard for error detection, insertAdjacentHTML mock,
range selection (HS_START/HS_END), wall-clock timeout in driveAsync
- hs-debug-test.js: Single-test debugger with DOM state inspection
- hs-verify.js: Assertion verification (proves pass/fail detection works)
Test results: 415/831 (50%), up from 408/831 (49%) baseline.
Fixes: set my style["color"], set X of Y, put at end of (insertAdjacentHTML).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -544,15 +544,14 @@ let api_call_fn fn_js args_js =
|
||||
sync_vm_to_env ();
|
||||
Js.Unsafe.inject (make_js_callFn_suspension request vm)
|
||||
| Eval_error msg ->
|
||||
ignore (Js.Unsafe.meth_call
|
||||
(Js.Unsafe.get Js.Unsafe.global (Js.string "console"))
|
||||
"error" [| Js.Unsafe.inject (Js.string ("[sx] callFn: " ^ msg)) |]);
|
||||
Js.Unsafe.inject Js.null
|
||||
(* Store the error message so callers can detect it *)
|
||||
let err_obj = Js.Unsafe.obj [| ("__sx_error", Js.Unsafe.inject Js._true);
|
||||
("message", Js.Unsafe.inject (Js.string msg)) |] in
|
||||
Js.Unsafe.inject err_obj
|
||||
| exn ->
|
||||
ignore (Js.Unsafe.meth_call
|
||||
(Js.Unsafe.get Js.Unsafe.global (Js.string "console"))
|
||||
"error" [| Js.Unsafe.inject (Js.string ("[sx] callFn: " ^ Printexc.to_string exn)) |]);
|
||||
Js.Unsafe.inject Js.null
|
||||
let err_obj = Js.Unsafe.obj [| ("__sx_error", Js.Unsafe.inject Js._true);
|
||||
("message", Js.Unsafe.inject (Js.string (Printexc.to_string exn))) |] in
|
||||
Js.Unsafe.inject err_obj
|
||||
|
||||
let api_is_callable fn_js =
|
||||
if Js.Unsafe.equals fn_js Js.null || Js.Unsafe.equals fn_js Js.undefined then
|
||||
@@ -1049,4 +1048,16 @@ let () =
|
||||
let log = Sx_primitives.scope_trace_drain () in
|
||||
Js.Unsafe.inject (Js.array (Array.of_list (List.map (fun s -> Js.Unsafe.inject (Js.string s)) log)))));
|
||||
|
||||
(* Step limit for timeout protection *)
|
||||
Js.Unsafe.set sx (Js.string "setStepLimit") (Js.wrap_callback (fun n ->
|
||||
let limit = Js.float_of_number (Js.Unsafe.coerce n) |> int_of_float in
|
||||
Sx_ref.step_limit := limit;
|
||||
Sx_ref.step_count := 0;
|
||||
Sx_vm.vm_reset_counters ();
|
||||
Js.Unsafe.inject Js.null));
|
||||
Js.Unsafe.set sx (Js.string "resetStepCount") (Js.wrap_callback (fun () ->
|
||||
Sx_ref.step_count := 0;
|
||||
Sx_vm.vm_reset_counters ();
|
||||
Js.Unsafe.inject Js.null));
|
||||
|
||||
Js.Unsafe.set Js.Unsafe.global (Js.string "SxKernel") sx
|
||||
|
||||
@@ -448,8 +448,9 @@ and run vm =
|
||||
let op = bc.(frame.ip) in
|
||||
frame.ip <- frame.ip + 1;
|
||||
incr _vm_insn_count;
|
||||
(* Check timeout flag set by SIGALRM *)
|
||||
if !_vm_insn_count land 0xFFFF = 0 && !Sx_ref.step_limit > 0 then
|
||||
(* Check timeout — compare VM instruction count against step limit *)
|
||||
if !_vm_insn_count land 0xFFFF = 0 && !Sx_ref.step_limit > 0
|
||||
&& !_vm_insn_count > !Sx_ref.step_limit then
|
||||
raise (Eval_error "TIMEOUT: step limit exceeded");
|
||||
(try match op with
|
||||
(* ---- Constants ---- *)
|
||||
|
||||
@@ -49,6 +49,34 @@
|
||||
((= th (quote it)) (list (quote set!) (quote it) value))
|
||||
((= th (quote query))
|
||||
(list (quote dom-set-inner-html) (hs-to-sx target) value))
|
||||
((= th (quote array-index))
|
||||
(list
|
||||
(quote host-set!)
|
||||
(hs-to-sx (nth target 1))
|
||||
(hs-to-sx (nth target 2))
|
||||
value))
|
||||
((= th (quote of))
|
||||
;; Decompose (of prop-expr target) into a set operation
|
||||
;; e.g. (of (. (ref "parentNode") "innerHTML") (query "#d1"))
|
||||
;; → set parentNode.innerHTML of #d1 → need to navigate target, then set final prop
|
||||
(let ((prop-ast (nth target 1))
|
||||
(obj-ast (nth target 2)))
|
||||
(if (and (list? prop-ast) (= (first prop-ast) dot-sym))
|
||||
;; (. base "prop") of obj → (dom-set-prop (host-get (compiled-obj) (compiled-base-name)) "prop" value)
|
||||
(let ((base (nth prop-ast 1))
|
||||
(prop-name (nth prop-ast 2)))
|
||||
(list (quote dom-set-prop)
|
||||
(list (quote host-get) (hs-to-sx obj-ast) (nth base 1))
|
||||
prop-name
|
||||
value))
|
||||
;; Simple: (ref "prop") of obj → (dom-set-prop (compiled-obj) "prop" value)
|
||||
(if (and (list? prop-ast) (= (first prop-ast) (quote ref)))
|
||||
(list (quote dom-set-prop)
|
||||
(hs-to-sx obj-ast)
|
||||
(nth prop-ast 1)
|
||||
value)
|
||||
;; Fallback
|
||||
(list (quote set!) (hs-to-sx target) value)))))
|
||||
(true (list (quote set!) (hs-to-sx target) value)))))))
|
||||
(define
|
||||
emit-on
|
||||
@@ -237,25 +265,16 @@
|
||||
(fn
|
||||
(ast)
|
||||
(let
|
||||
((prop (nth ast 1)) (value (hs-to-sx (nth ast 2))))
|
||||
(if
|
||||
(= (len ast) 5)
|
||||
(let
|
||||
((raw-tgt (nth ast 4)))
|
||||
(list
|
||||
(quote hs-transition)
|
||||
(if (nil? raw-tgt) (quote me) (hs-to-sx raw-tgt))
|
||||
prop
|
||||
value
|
||||
(nth ast 3)))
|
||||
(let
|
||||
((raw-tgt (nth ast 3)))
|
||||
(list
|
||||
(quote hs-transition)
|
||||
(if (nil? raw-tgt) (quote me) (hs-to-sx raw-tgt))
|
||||
prop
|
||||
value
|
||||
nil))))))
|
||||
((prop (hs-to-sx (nth ast 1)))
|
||||
(value (hs-to-sx (nth ast 2)))
|
||||
(dur (nth ast 3))
|
||||
(raw-tgt (nth ast 4)))
|
||||
(list
|
||||
(quote hs-transition)
|
||||
(if (nil? raw-tgt) (quote me) (hs-to-sx raw-tgt))
|
||||
prop
|
||||
value
|
||||
(if dur (hs-to-sx dur) nil)))))
|
||||
(define
|
||||
emit-make
|
||||
(fn
|
||||
@@ -400,6 +419,10 @@
|
||||
((head (first ast)))
|
||||
(cond
|
||||
((= head (quote null-literal)) nil)
|
||||
((= head (quote not))
|
||||
(list (quote not) (hs-to-sx (nth ast 1))))
|
||||
((or (= head (quote starts-with?)) (= head (quote ends-with?)) (= head (quote contains?)) (= head (quote matches?)) (= head (quote precedes?)) (= head (quote follows?)) (= head (quote exists?)))
|
||||
(cons head (map hs-to-sx (rest ast))))
|
||||
((= head (quote object-literal))
|
||||
(let
|
||||
((pairs (nth ast 1)))
|
||||
@@ -935,20 +958,26 @@
|
||||
((= head (quote transition)) (emit-transition ast))
|
||||
((= head (quote transition-from))
|
||||
(let
|
||||
((prop (nth ast 1))
|
||||
((prop (hs-to-sx (nth ast 1)))
|
||||
(from-val (hs-to-sx (nth ast 2)))
|
||||
(to-val (hs-to-sx (nth ast 3)))
|
||||
(dur (nth ast 4)))
|
||||
(dur (nth ast 4))
|
||||
(raw-tgt (nth ast 5)))
|
||||
(list
|
||||
(quote hs-transition-from)
|
||||
(quote me)
|
||||
(if (nil? raw-tgt) (quote me) (hs-to-sx raw-tgt))
|
||||
prop
|
||||
from-val
|
||||
to-val
|
||||
dur)))
|
||||
(if dur (hs-to-sx dur) nil))))
|
||||
((= head (quote repeat)) (emit-repeat ast))
|
||||
((= head (quote fetch))
|
||||
(list (quote hs-fetch) (hs-to-sx (nth ast 1)) (nth ast 2)))
|
||||
((= head (quote fetch-gql))
|
||||
(list
|
||||
(quote hs-fetch-gql)
|
||||
(nth ast 1)
|
||||
(if (nth ast 2) (hs-to-sx (nth ast 2)) nil)))
|
||||
((= head (quote call))
|
||||
(let
|
||||
((fn-expr (hs-to-sx (nth ast 1)))
|
||||
@@ -967,10 +996,15 @@
|
||||
(hs-to-sx (nth ast 2))
|
||||
(hs-to-sx (nth ast 1))))
|
||||
((= head (quote tell))
|
||||
(list
|
||||
(quote let)
|
||||
(list (list (quote me) (hs-to-sx (nth ast 1))))
|
||||
(hs-to-sx (nth ast 2))))
|
||||
(let
|
||||
((tgt (hs-to-sx (nth ast 1))))
|
||||
(list
|
||||
(quote let)
|
||||
(list
|
||||
(list (quote me) tgt)
|
||||
(list (quote you) tgt)
|
||||
(list (quote yourself) tgt))
|
||||
(hs-to-sx (nth ast 2)))))
|
||||
((= head (quote for)) (emit-for ast))
|
||||
((= head (quote take!))
|
||||
(let
|
||||
|
||||
@@ -49,6 +49,34 @@
|
||||
((= th (quote it)) (list (quote set!) (quote it) value))
|
||||
((= th (quote query))
|
||||
(list (quote dom-set-inner-html) (hs-to-sx target) value))
|
||||
((= th (quote array-index))
|
||||
(list
|
||||
(quote host-set!)
|
||||
(hs-to-sx (nth target 1))
|
||||
(hs-to-sx (nth target 2))
|
||||
value))
|
||||
((= th (quote of))
|
||||
;; Decompose (of prop-expr target) into a set operation
|
||||
;; e.g. (of (. (ref "parentNode") "innerHTML") (query "#d1"))
|
||||
;; → set parentNode.innerHTML of #d1 → need to navigate target, then set final prop
|
||||
(let ((prop-ast (nth target 1))
|
||||
(obj-ast (nth target 2)))
|
||||
(if (and (list? prop-ast) (= (first prop-ast) dot-sym))
|
||||
;; (. base "prop") of obj → (dom-set-prop (host-get (compiled-obj) (compiled-base-name)) "prop" value)
|
||||
(let ((base (nth prop-ast 1))
|
||||
(prop-name (nth prop-ast 2)))
|
||||
(list (quote dom-set-prop)
|
||||
(list (quote host-get) (hs-to-sx obj-ast) (nth base 1))
|
||||
prop-name
|
||||
value))
|
||||
;; Simple: (ref "prop") of obj → (dom-set-prop (compiled-obj) "prop" value)
|
||||
(if (and (list? prop-ast) (= (first prop-ast) (quote ref)))
|
||||
(list (quote dom-set-prop)
|
||||
(hs-to-sx obj-ast)
|
||||
(nth prop-ast 1)
|
||||
value)
|
||||
;; Fallback
|
||||
(list (quote set!) (hs-to-sx target) value)))))
|
||||
(true (list (quote set!) (hs-to-sx target) value)))))))
|
||||
(define
|
||||
emit-on
|
||||
@@ -237,25 +265,16 @@
|
||||
(fn
|
||||
(ast)
|
||||
(let
|
||||
((prop (nth ast 1)) (value (hs-to-sx (nth ast 2))))
|
||||
(if
|
||||
(= (len ast) 5)
|
||||
(let
|
||||
((raw-tgt (nth ast 4)))
|
||||
(list
|
||||
(quote hs-transition)
|
||||
(if (nil? raw-tgt) (quote me) (hs-to-sx raw-tgt))
|
||||
prop
|
||||
value
|
||||
(nth ast 3)))
|
||||
(let
|
||||
((raw-tgt (nth ast 3)))
|
||||
(list
|
||||
(quote hs-transition)
|
||||
(if (nil? raw-tgt) (quote me) (hs-to-sx raw-tgt))
|
||||
prop
|
||||
value
|
||||
nil))))))
|
||||
((prop (hs-to-sx (nth ast 1)))
|
||||
(value (hs-to-sx (nth ast 2)))
|
||||
(dur (nth ast 3))
|
||||
(raw-tgt (nth ast 4)))
|
||||
(list
|
||||
(quote hs-transition)
|
||||
(if (nil? raw-tgt) (quote me) (hs-to-sx raw-tgt))
|
||||
prop
|
||||
value
|
||||
(if dur (hs-to-sx dur) nil)))))
|
||||
(define
|
||||
emit-make
|
||||
(fn
|
||||
@@ -400,6 +419,10 @@
|
||||
((head (first ast)))
|
||||
(cond
|
||||
((= head (quote null-literal)) nil)
|
||||
((= head (quote not))
|
||||
(list (quote not) (hs-to-sx (nth ast 1))))
|
||||
((or (= head (quote starts-with?)) (= head (quote ends-with?)) (= head (quote contains?)) (= head (quote matches?)) (= head (quote precedes?)) (= head (quote follows?)) (= head (quote exists?)))
|
||||
(cons head (map hs-to-sx (rest ast))))
|
||||
((= head (quote object-literal))
|
||||
(let
|
||||
((pairs (nth ast 1)))
|
||||
@@ -688,11 +711,21 @@
|
||||
(quote hs-matches?)
|
||||
(hs-to-sx (nth ast 1))
|
||||
(hs-to-sx (nth ast 2))))
|
||||
((= head (quote matches-ignore-case?))
|
||||
(list
|
||||
(quote hs-matches-ignore-case?)
|
||||
(hs-to-sx (nth ast 1))
|
||||
(hs-to-sx (nth ast 2))))
|
||||
((= head (quote contains?))
|
||||
(list
|
||||
(quote hs-contains?)
|
||||
(hs-to-sx (nth ast 1))
|
||||
(hs-to-sx (nth ast 2))))
|
||||
((= head (quote contains-ignore-case?))
|
||||
(list
|
||||
(quote hs-contains-ignore-case?)
|
||||
(hs-to-sx (nth ast 1))
|
||||
(hs-to-sx (nth ast 2))))
|
||||
((= head (quote as))
|
||||
(list (quote hs-coerce) (hs-to-sx (nth ast 1)) (nth ast 2)))
|
||||
((= head (quote in?))
|
||||
@@ -817,6 +850,10 @@
|
||||
(list (quote dom-remove) (hs-to-sx (nth ast 1))))
|
||||
((= head (quote empty-target))
|
||||
(list (quote hs-empty-target!) (hs-to-sx (nth ast 1))))
|
||||
((= head (quote open-element))
|
||||
(list (quote hs-open!) (hs-to-sx (nth ast 1))))
|
||||
((= head (quote close-element))
|
||||
(list (quote hs-close!) (hs-to-sx (nth ast 1))))
|
||||
((= head (quote swap!))
|
||||
(let
|
||||
((lhs (nth ast 1)) (rhs (nth ast 2)))
|
||||
@@ -912,39 +949,35 @@
|
||||
(let
|
||||
((tgt (hs-to-sx (nth ast 1)))
|
||||
(strategy (if (> (len ast) 2) (nth ast 2) "display")))
|
||||
(cond
|
||||
((= strategy "opacity")
|
||||
(list (quote dom-set-style) tgt "opacity" "0"))
|
||||
((= strategy "visibility")
|
||||
(list (quote dom-set-style) tgt "visibility" "hidden"))
|
||||
(true (list (quote dom-set-style) tgt "display" "none")))))
|
||||
(list (quote hs-hide!) tgt strategy)))
|
||||
((= head (quote show))
|
||||
(let
|
||||
((tgt (hs-to-sx (nth ast 1)))
|
||||
(strategy (if (> (len ast) 2) (nth ast 2) "display")))
|
||||
(cond
|
||||
((= strategy "opacity")
|
||||
(list (quote dom-set-style) tgt "opacity" "1"))
|
||||
((= strategy "visibility")
|
||||
(list (quote dom-set-style) tgt "visibility" "visible"))
|
||||
(true (list (quote dom-set-style) tgt "display" "")))))
|
||||
(list (quote hs-show!) tgt strategy)))
|
||||
((= head (quote transition)) (emit-transition ast))
|
||||
((= head (quote transition-from))
|
||||
(let
|
||||
((prop (nth ast 1))
|
||||
((prop (hs-to-sx (nth ast 1)))
|
||||
(from-val (hs-to-sx (nth ast 2)))
|
||||
(to-val (hs-to-sx (nth ast 3)))
|
||||
(dur (nth ast 4)))
|
||||
(dur (nth ast 4))
|
||||
(raw-tgt (nth ast 5)))
|
||||
(list
|
||||
(quote hs-transition-from)
|
||||
(quote me)
|
||||
(if (nil? raw-tgt) (quote me) (hs-to-sx raw-tgt))
|
||||
prop
|
||||
from-val
|
||||
to-val
|
||||
dur)))
|
||||
(if dur (hs-to-sx dur) nil))))
|
||||
((= head (quote repeat)) (emit-repeat ast))
|
||||
((= head (quote fetch))
|
||||
(list (quote hs-fetch) (hs-to-sx (nth ast 1)) (nth ast 2)))
|
||||
((= head (quote fetch-gql))
|
||||
(list
|
||||
(quote hs-fetch-gql)
|
||||
(nth ast 1)
|
||||
(if (nth ast 2) (hs-to-sx (nth ast 2)) nil)))
|
||||
((= head (quote call))
|
||||
(let
|
||||
((fn-expr (hs-to-sx (nth ast 1)))
|
||||
@@ -963,10 +996,15 @@
|
||||
(hs-to-sx (nth ast 2))
|
||||
(hs-to-sx (nth ast 1))))
|
||||
((= head (quote tell))
|
||||
(list
|
||||
(quote let)
|
||||
(list (list (quote me) (hs-to-sx (nth ast 1))))
|
||||
(hs-to-sx (nth ast 2))))
|
||||
(let
|
||||
((tgt (hs-to-sx (nth ast 1))))
|
||||
(list
|
||||
(quote let)
|
||||
(list
|
||||
(list (quote me) tgt)
|
||||
(list (quote you) tgt)
|
||||
(list (quote yourself) tgt))
|
||||
(hs-to-sx (nth ast 2)))))
|
||||
((= head (quote for)) (emit-for ast))
|
||||
((= head (quote take!))
|
||||
(let
|
||||
@@ -1078,6 +1116,11 @@
|
||||
(quote hs-strict-eq)
|
||||
(hs-to-sx (nth ast 1))
|
||||
(hs-to-sx (nth ast 2))))
|
||||
((= head (quote eq-ignore-case))
|
||||
(list
|
||||
(quote hs-eq-ignore-case)
|
||||
(hs-to-sx (nth ast 1))
|
||||
(hs-to-sx (nth ast 2))))
|
||||
((= head (quote some))
|
||||
(list
|
||||
(quote some)
|
||||
|
||||
File diff suppressed because one or more lines are too long
138
tests/hs-debug-test.js
Normal file
138
tests/hs-debug-test.js
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Debug specific HS behavioral tests to trace why they fail.
|
||||
* Reuses the hs-run-fast.js setup but runs individual tests with verbose output.
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const PROJECT = path.resolve(__dirname, '..');
|
||||
const WASM_DIR = path.join(PROJECT, 'shared/static/wasm');
|
||||
const SX_DIR = path.join(WASM_DIR, 'sx');
|
||||
|
||||
// Load and run the setup from hs-run-fast.js up to the test loop
|
||||
eval(fs.readFileSync(path.join(WASM_DIR, 'sx_browser.bc.js'), 'utf8'));
|
||||
const K = globalThis.SxKernel;
|
||||
|
||||
// ─── Copy DOM mock from runner ──────────────────────────────
|
||||
class El {
|
||||
constructor(t) { this.tagName=t.toUpperCase(); this.nodeName=this.tagName; this.nodeType=1; this.id=''; this.className=''; this.classList=new CL(this); this.style={}; this.attributes={}; this.children=[]; this.childNodes=[]; this.parentElement=null; this.parentNode=null; this.textContent=''; this.innerHTML=''; this._listeners={}; this.dataset={}; this.open=false; this.value=''; this.checked=false; this.disabled=false; this.type=''; this.name=''; this.selectedIndex=-1; this.options=[]; }
|
||||
setAttribute(n,v) { this.attributes[n]=String(v); if(n==='id')this.id=v; if(n==='class'){this.className=v;this.classList._sync(v);} if(n==='value')this.value=v; if(n==='disabled')this.disabled=true; }
|
||||
getAttribute(n) { return this.attributes[n]!==undefined?this.attributes[n]:null; }
|
||||
removeAttribute(n) { delete this.attributes[n]; if(n==='disabled')this.disabled=false; }
|
||||
hasAttribute(n) { return n in this.attributes; }
|
||||
addEventListener(e,f) { if(!this._listeners[e])this._listeners[e]=[]; this._listeners[e].push(f); }
|
||||
removeEventListener(e,f) { if(this._listeners[e])this._listeners[e]=this._listeners[e].filter(x=>x!==f); }
|
||||
dispatchEvent(ev) { ev.target=ev.target||this; ev.currentTarget=this; const fns=[...(this._listeners[ev.type]||[])]; for(const f of fns){if(ev._si)break;try{f.call(this,ev);}catch(e){}} if(ev.bubbles&&!ev._sp&&this.parentElement){this.parentElement.dispatchEvent(ev);} return !ev.defaultPrevented; }
|
||||
appendChild(c) { if(c.parentElement)c.parentElement.removeChild(c); c.parentElement=this; c.parentNode=this; this.children.push(c); this.childNodes.push(c); this._syncText(); return c; }
|
||||
removeChild(c) { this.children=this.children.filter(x=>x!==c); this.childNodes=this.childNodes.filter(x=>x!==c); c.parentElement=null; c.parentNode=null; this._syncText(); return c; }
|
||||
insertBefore(n,r) { if(n.parentElement)n.parentElement.removeChild(n); const i=this.children.indexOf(r); if(i>=0){this.children.splice(i,0,n);this.childNodes.splice(i,0,n);}else{this.children.push(n);this.childNodes.push(n);} n.parentElement=this;n.parentNode=this; this._syncText(); return n; }
|
||||
replaceChild(n,o) { const i=this.children.indexOf(o); if(i>=0){this.children[i]=n;this.childNodes[i]=n;} n.parentElement=this;n.parentNode=this; o.parentElement=null;o.parentNode=null; this._syncText(); return o; }
|
||||
querySelector(s) { return fnd(this,s); } querySelectorAll(s) { return fndAll(this,s); }
|
||||
closest(s) { let e=this; while(e){if(mt(e,s))return e; e=e.parentElement;} return null; }
|
||||
matches(s) { return mt(this,s); } contains(o) { if(o===this)return true; for(const c of this.children)if(c===o||c.contains(o))return true; return false; }
|
||||
cloneNode(d) { const e=new El(this.tagName.toLowerCase()); Object.assign(e.attributes,this.attributes); e.id=this.id; e.className=this.className; e.classList._sync(this.className); Object.assign(e.style,this.style); e.textContent=this.textContent; e.innerHTML=this.innerHTML; e.value=this.value; if(d)for(const c of this.children)e.appendChild(c.cloneNode(true)); return e; }
|
||||
focus(){} blur(){} click(){this.dispatchEvent(new Ev('click',{bubbles:true}));} remove(){if(this.parentElement)this.parentElement.removeChild(this);}
|
||||
_syncText() { const t = this.children.map(c => c.textContent || '').join(''); if (t) this.textContent = t; }
|
||||
_setInnerHTML(html) { for (const c of this.children) { c.parentElement = null; c.parentNode = null; } this.children = []; this.childNodes = []; this.innerHTML = html; this.textContent = html.replace(/<[^>]*>/g, ''); }
|
||||
get firstElementChild() { return this.children[0]||null; }
|
||||
get lastElementChild() { return this.children[this.children.length-1]||null; }
|
||||
get nextElementSibling() { if(!this.parentElement)return null; const i=this.parentElement.children.indexOf(this); return this.parentElement.children[i+1]||null; }
|
||||
get previousElementSibling() { if(!this.parentElement)return null; const i=this.parentElement.children.indexOf(this); return i>0?this.parentElement.children[i-1]:null; }
|
||||
showModal(){this.open=true;this.setAttribute('open','');} show(){this.open=true;} close(){this.open=false;this.removeAttribute('open');}
|
||||
getAnimations(){return [];} getBoundingClientRect(){return{top:0,left:0,width:100,height:100,right:100,bottom:100};} scrollIntoView(){}
|
||||
get ownerDocument() { return document; }
|
||||
get offsetParent() { return this.parentElement; }
|
||||
insertAdjacentHTML(pos, html) { if(typeof html!=='string')html=String(html); if(pos==='beforeend'||pos==='beforeEnd'){this.innerHTML=(this.innerHTML||'')+html;this.textContent=(this.textContent||'')+html.replace(/<[^>]*>/g,'');}else if(pos==='afterbegin'||pos==='afterBegin'){this.innerHTML=html+(this.innerHTML||'');this.textContent=html.replace(/<[^>]*>/g,'')+(this.textContent||'');} }
|
||||
}
|
||||
class CL { constructor(e){this._el=e;this._set=new Set();} _sync(str){this._set=new Set((str||'').split(/\s+/).filter(Boolean));} add(...c){for(const x of c)this._set.add(x);this._el.className=[...this._set].join(' ');this._el.attributes['class']=this._el.className;} remove(...c){for(const x of c)this._set.delete(x);this._el.className=[...this._set].join(' ');this._el.attributes['class']=this._el.className;} toggle(c,f){if(f!==undefined){if(f)this.add(c);else this.remove(c);return f;} if(this._set.has(c)){this.remove(c);return false;}else{this.add(c);return true;}} contains(c){return this._set.has(c);} get length(){return this._set.size;} [Symbol.iterator](){return this._set[Symbol.iterator]();} }
|
||||
class Ev { constructor(t,o={}){this.type=t;this.bubbles=o.bubbles||false;this.cancelable=o.cancelable!==false;this.defaultPrevented=false;this._sp=false;this._si=false;this.target=null;this.currentTarget=null;this.detail=o.detail||null;} preventDefault(){this.defaultPrevented=true;} stopPropagation(){this._sp=true;} stopImmediatePropagation(){this._sp=true;this._si=true;} }
|
||||
function mt(e,s) { if(!e||!e.tagName)return false; s=s.trim(); if(s.startsWith('#'))return e.id===s.slice(1); if(s.startsWith('.'))return e.classList.contains(s.slice(1)); if(s.startsWith('[')){const m=s.match(/^\[([^\]=]+)(?:="([^"]*)")?\]$/);if(m)return m[2]!==undefined?e.getAttribute(m[1])===m[2]:e.hasAttribute(m[1]);} if(s.includes('.')){const[tag,cls]=s.split('.');return e.tagName.toLowerCase()===tag&&e.classList.contains(cls);} if(s.includes('#')){const[tag,id]=s.split('#');return e.tagName.toLowerCase()===tag&&e.id===id;} return e.tagName.toLowerCase()===s.toLowerCase(); }
|
||||
function fnd(e,s) { for(const c of(e.children||[])){if(mt(c,s))return c;const f=fnd(c,s);if(f)return f;} return null; }
|
||||
function fndAll(e,s) { const r=[];for(const c of(e.children||[])){if(mt(c,s))r.push(c);r.push(...fndAll(c,s));}return r; }
|
||||
const _body = new El('body'); const _html = new El('html'); _html.appendChild(_body);
|
||||
const document = { body:_body, documentElement:_html, createElement(t){return new El(t);}, createElementNS(n,t){return new El(t);}, createDocumentFragment(){const f=new El('fragment');f.nodeType=11;return f;}, createTextNode(t){return{nodeType:3,textContent:t,data:t};}, getElementById(i){return fnd(_body,'#'+i);}, querySelector(s){return fnd(_body,s);}, querySelectorAll(s){return fndAll(_body,s);}, createEvent(t){return new Ev(t);}, addEventListener(){}, removeEventListener(){} };
|
||||
globalThis.document=document; globalThis.window=globalThis; globalThis.HTMLElement=El; globalThis.Element=El;
|
||||
globalThis.Event=Ev; globalThis.CustomEvent=Ev; globalThis.NodeList=Array; globalThis.HTMLCollection=Array;
|
||||
globalThis.getComputedStyle=(e)=>e?e.style:{}; globalThis.requestAnimationFrame=(f)=>{f();return 0;};
|
||||
globalThis.cancelAnimationFrame=()=>{}; globalThis.MutationObserver=class{observe(){}disconnect(){}};
|
||||
globalThis.ResizeObserver=class{observe(){}disconnect(){}}; globalThis.IntersectionObserver=class{observe(){}disconnect(){}};
|
||||
globalThis.navigator={userAgent:'node'}; globalThis.location={href:'http://localhost/',pathname:'/',search:'',hash:''};
|
||||
globalThis.history={pushState(){},replaceState(){},back(){},forward(){}};
|
||||
|
||||
K.registerNative('host-global',a=>{const n=a[0];return(n in globalThis)?globalThis[n]:null;});
|
||||
K.registerNative('host-get',a=>{if(a[0]==null)return null;const v=a[0][a[1]];return v===undefined?null:v;});
|
||||
K.registerNative('host-set!',a=>{if(a[0]!=null){a[0][a[1]]=a[2];if(a[1]==='innerHTML'&&a[0] instanceof El){a[0]._setInnerHTML(a[2]);}if(a[1]==='textContent'&&a[0] instanceof El){a[0].textContent=String(a[2]);}} return a[2];});
|
||||
K.registerNative('host-call',a=>{const[o,m,...r]=a;if(o==null){const f=globalThis[m];return typeof f==='function'?f.apply(null,r):null;}if(o&&typeof o[m]==='function'){try{const v=o[m].apply(o,r);return v===undefined?null:v;}catch(e){return null;}}return null;});
|
||||
K.registerNative('host-new',a=>{const C=typeof a[0]==='string'?globalThis[a[0]]:a[0];return typeof C==='function'?new C(...a.slice(1)):null;});
|
||||
K.registerNative('host-callback',a=>{const fn=a[0];if(typeof fn==='function'&&fn.__sx_handle===undefined)return fn;if(fn&&fn.__sx_handle!==undefined)return function(){const r=K.callFn(fn,Array.from(arguments));if(globalThis._driveAsync)globalThis._driveAsync(r);return r;};return function(){};});
|
||||
K.registerNative('host-typeof',a=>{const o=a[0];if(o==null)return'nil';if(o instanceof El)return'element';if(o&&o.nodeType===3)return'text';if(o instanceof Ev)return'event';if(o instanceof Promise)return'promise';return typeof o;});
|
||||
K.registerNative('host-await',a=>{});
|
||||
K.registerNative('load-library!',()=>false);
|
||||
globalThis._driveAsync=function driveAsync(r,d){d=d||0;if(d>500||!r||!r.suspended)return;const req=r.request;const items=req&&(req.items||req);const op=items&&items[0];const opName=typeof op==='string'?op:(op&&op.name)||String(op);function doResume(v){try{const x=r.resume(v);driveAsync(x,d+1);}catch(e){}}if(opName==='io-sleep'||opName==='wait')doResume(null);else if(opName==='io-fetch')doResume({ok:true,text:''});else if(opName==='io-settle')doResume(null);else if(opName==='io-wait-event')doResume(null);else if(opName==='io-transition')doResume(null);};
|
||||
K.eval('(define SX_VERSION "hs-test-1.0")');K.eval('(define SX_ENGINE "ocaml-vm-sandbox")');
|
||||
K.eval('(define parse sx-parse)');K.eval('(define serialize sx-serialize)');
|
||||
|
||||
const WEB=['render','core-signals','signals','deps','router','page-helpers','freeze','dom','browser','adapter-html','adapter-sx','adapter-dom','boot-helpers','hypersx','engine','orchestration','boot'];
|
||||
const HS=['hs-tokenizer','hs-parser','hs-compiler','hs-runtime','hs-integration'];
|
||||
K.beginModuleLoad();
|
||||
for(const mod of[...WEB,...HS]){const sp=path.join(SX_DIR,mod+'.sx');const lp=path.join(PROJECT,'lib/hyperscript',mod.replace(/^hs-/,'')+'.sx');let s;try{s=fs.existsSync(sp)?fs.readFileSync(sp,'utf8'):fs.readFileSync(lp,'utf8');}catch(e){continue;}try{K.load(s);}catch(e){}}
|
||||
K.endModuleLoad();
|
||||
|
||||
// Load test registry
|
||||
K.eval('(define _test-registry (list))');K.eval('(define _test-suite "")');
|
||||
K.eval('(define push-suite (fn (name) (set! _test-suite name)))');
|
||||
K.eval('(define pop-suite (fn () (set! _test-suite "")))');
|
||||
K.eval('(define try-call (fn (thunk) (set! _test-registry (append _test-registry (list {:suite _test-suite :thunk thunk}))) {:ok true}))');
|
||||
K.eval('(define report-pass (fn (n) true))');
|
||||
K.eval('(define report-fail (fn (n e) true))');
|
||||
K.eval(`(define _run-test-thunk
|
||||
(fn (thunk)
|
||||
(guard (exn
|
||||
(true {:ok false :error (if (string? exn) exn (str exn))}))
|
||||
(thunk)
|
||||
{:ok true})))`);
|
||||
for(const f of['spec/harness.sx','spec/tests/test-framework.sx','spec/tests/test-hyperscript-behavioral.sx']){
|
||||
try{K.load(fs.readFileSync(path.join(PROJECT,f),'utf8'));}catch(e){}}
|
||||
|
||||
const testCount = K.eval('(len _test-registry)');
|
||||
console.log(`Loaded ${testCount} tests\n`);
|
||||
|
||||
// Run specific tests
|
||||
const tests = (process.argv[2] || '0,1,65,83').split(',').map(Number);
|
||||
|
||||
for (const i of tests) {
|
||||
_body.children=[]; _body.childNodes=[]; _body.innerHTML=''; _body.textContent='';
|
||||
|
||||
const suite = K.eval(`(get (nth _test-registry ${i}) "suite")`) || '';
|
||||
const name = K.eval(`(get (nth _test-registry ${i}) "name")`) || `test-${i}`;
|
||||
|
||||
K.setStepLimit(500000);
|
||||
let ok = false, err = null;
|
||||
try {
|
||||
K.eval(`(define _test-result (_run-test-thunk (get (nth _test-registry ${i}) "thunk")))`);
|
||||
const isOk = K.eval('(get _test-result "ok")');
|
||||
if (isOk === true) { ok = true; }
|
||||
else {
|
||||
const errMsg = K.eval('(get _test-result "error")');
|
||||
err = errMsg ? String(errMsg).slice(0, 150) : 'unknown error';
|
||||
}
|
||||
} catch(e) { err = (e.message || '').slice(0, 150); }
|
||||
K.setStepLimit(0);
|
||||
|
||||
console.log(`Test ${i} [${suite}] ${name}`);
|
||||
if (ok) console.log(` PASS`);
|
||||
else console.log(` FAIL: ${(err||'').slice(0,200)}`);
|
||||
|
||||
// Show DOM state
|
||||
const bodyHTML = _body.innerHTML;
|
||||
const bodyChildren = _body.children.length;
|
||||
if (bodyChildren > 0) {
|
||||
const el = _body.children[0];
|
||||
console.log(` DOM: <${el.tagName.toLowerCase()} class="${el.className}" id="${el.id}">`);
|
||||
console.log(` innerHTML: ${JSON.stringify(el.innerHTML).slice(0,100)}`);
|
||||
console.log(` textContent: ${JSON.stringify(el.textContent).slice(0,100)}`);
|
||||
console.log(` listeners: ${JSON.stringify(Object.keys(el._listeners))} (${el._listeners.click?.length || 0} click)`);
|
||||
console.log(` style: ${JSON.stringify(el.style)}`);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
299
tests/hs-run-fast.js
Normal file
299
tests/hs-run-fast.js
Normal file
@@ -0,0 +1,299 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Run HS behavioral tests — single process, synchronous, with step-limit timeout.
|
||||
* Uses the OCaml VM's built-in step_limit to break infinite loops.
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PROJECT = path.resolve(__dirname, '..');
|
||||
const WASM_DIR = path.join(PROJECT, 'shared/static/wasm');
|
||||
const SX_DIR = path.join(WASM_DIR, 'sx');
|
||||
|
||||
// Load WASM kernel
|
||||
eval(fs.readFileSync(path.join(WASM_DIR, 'sx_browser.bc.js'), 'utf8'));
|
||||
const K = globalThis.SxKernel;
|
||||
|
||||
// Step limit API — exposed from OCaml kernel
|
||||
const STEP_LIMIT = parseInt(process.env.HS_STEP_LIMIT || '500000');
|
||||
|
||||
function setStepLimit(n) { K.setStepLimit(n); }
|
||||
function resetStepCount() { K.resetStepCount(); }
|
||||
|
||||
// ─── DOM mock ──────────────────────────────────────────────────
|
||||
class El {
|
||||
constructor(t) { this.tagName=t.toUpperCase(); this.nodeName=this.tagName; this.nodeType=1; this.id=''; this.className=''; this.classList=new CL(this); this.style={}; this.attributes={}; this.children=[]; this.childNodes=[]; this.parentElement=null; this.parentNode=null; this.textContent=''; this.innerHTML=''; this._listeners={}; this.dataset={}; this.open=false; this.value=''; this.checked=false; this.disabled=false; this.type=''; this.name=''; this.selectedIndex=-1; this.options=[]; }
|
||||
setAttribute(n,v) { this.attributes[n]=String(v); if(n==='id')this.id=v; if(n==='class'){this.className=v;this.classList._sync(v);} if(n==='value')this.value=v; if(n==='disabled')this.disabled=true; }
|
||||
getAttribute(n) { return this.attributes[n]!==undefined?this.attributes[n]:null; }
|
||||
removeAttribute(n) { delete this.attributes[n]; if(n==='disabled')this.disabled=false; }
|
||||
hasAttribute(n) { return n in this.attributes; }
|
||||
addEventListener(e,f) { if(!this._listeners[e])this._listeners[e]=[]; this._listeners[e].push(f); }
|
||||
removeEventListener(e,f) { if(this._listeners[e])this._listeners[e]=this._listeners[e].filter(x=>x!==f); }
|
||||
dispatchEvent(ev) { ev.target=ev.target||this; ev.currentTarget=this; const fns=[...(this._listeners[ev.type]||[])]; for(const f of fns){if(ev._si)break;try{f.call(this,ev);}catch(e){}} if(ev.bubbles&&!ev._sp&&this.parentElement){this.parentElement.dispatchEvent(ev);} return !ev.defaultPrevented; }
|
||||
appendChild(c) { if(c.parentElement)c.parentElement.removeChild(c); c.parentElement=this; c.parentNode=this; this.children.push(c); this.childNodes.push(c); this._syncText(); return c; }
|
||||
removeChild(c) { this.children=this.children.filter(x=>x!==c); this.childNodes=this.childNodes.filter(x=>x!==c); c.parentElement=null; c.parentNode=null; this._syncText(); return c; }
|
||||
insertBefore(n,r) { if(n.parentElement)n.parentElement.removeChild(n); const i=this.children.indexOf(r); if(i>=0){this.children.splice(i,0,n);this.childNodes.splice(i,0,n);}else{this.children.push(n);this.childNodes.push(n);} n.parentElement=this;n.parentNode=this; this._syncText(); return n; }
|
||||
replaceChild(n,o) { const i=this.children.indexOf(o); if(i>=0){this.children[i]=n;this.childNodes[i]=n;} n.parentElement=this;n.parentNode=this; o.parentElement=null;o.parentNode=null; this._syncText(); return o; }
|
||||
querySelector(s) { return fnd(this,s); }
|
||||
querySelectorAll(s) { return fndAll(this,s); }
|
||||
closest(s) { let e=this; while(e){if(mt(e,s))return e; e=e.parentElement;} return null; }
|
||||
matches(s) { return mt(this,s); }
|
||||
contains(o) { if(o===this)return true; for(const c of this.children)if(c===o||c.contains(o))return true; return false; }
|
||||
cloneNode(d) { const e=new El(this.tagName.toLowerCase()); Object.assign(e.attributes,this.attributes); e.id=this.id; e.className=this.className; e.classList._sync(this.className); Object.assign(e.style,this.style); e.textContent=this.textContent; e.innerHTML=this.innerHTML; e.value=this.value; if(d)for(const c of this.children)e.appendChild(c.cloneNode(true)); return e; }
|
||||
focus(){} blur(){} click(){this.dispatchEvent(new Ev('click',{bubbles:true}));} remove(){if(this.parentElement)this.parentElement.removeChild(this);}
|
||||
_syncText() {
|
||||
// Sync textContent from children
|
||||
const t = this.children.map(c => c.textContent || '').join('');
|
||||
if (t) this.textContent = t;
|
||||
}
|
||||
_setInnerHTML(html) {
|
||||
// Clear children
|
||||
for (const c of this.children) { c.parentElement = null; c.parentNode = null; }
|
||||
this.children = []; this.childNodes = [];
|
||||
this.innerHTML = html;
|
||||
// Parse simple HTML and add children
|
||||
if (html) {
|
||||
const parsed = parseHTMLFragments(html);
|
||||
for (const c of parsed) this.appendChild(c);
|
||||
this.textContent = this.children.map(c => c.textContent || '').join('') || html.replace(/<[^>]*>/g, '');
|
||||
} else {
|
||||
this.textContent = '';
|
||||
}
|
||||
}
|
||||
get firstElementChild() { return this.children[0]||null; }
|
||||
get lastElementChild() { return this.children[this.children.length-1]||null; }
|
||||
get nextElementSibling() { if(!this.parentElement)return null; const i=this.parentElement.children.indexOf(this); return this.parentElement.children[i+1]||null; }
|
||||
get previousElementSibling() { if(!this.parentElement)return null; const i=this.parentElement.children.indexOf(this); return i>0?this.parentElement.children[i-1]:null; }
|
||||
showModal(){this.open=true;this.setAttribute('open','');} show(){this.open=true;} close(){this.open=false;this.removeAttribute('open');}
|
||||
getAnimations(){return [];} getBoundingClientRect(){return{top:0,left:0,width:100,height:100,right:100,bottom:100};} scrollIntoView(){}
|
||||
get ownerDocument() { return document; }
|
||||
get offsetParent() { return this.parentElement; }
|
||||
get offsetTop() { return 0; } get offsetLeft() { return 0; }
|
||||
get scrollTop() { return 0; } set scrollTop(v) {} get scrollLeft() { return 0; } set scrollLeft(v) {}
|
||||
get scrollHeight() { return 100; } get scrollWidth() { return 100; }
|
||||
get clientHeight() { return 100; } get clientWidth() { return 100; }
|
||||
insertAdjacentHTML(pos, html) {
|
||||
// For non-HTML content (plain text/numbers), just append to innerHTML
|
||||
if (typeof html !== 'string') html = String(html);
|
||||
if (pos === 'beforeend' || pos === 'beforeEnd') {
|
||||
this.innerHTML = (this.innerHTML || '') + html;
|
||||
this.textContent = (this.textContent || '') + html.replace(/<[^>]*>/g, '');
|
||||
} else if (pos === 'afterbegin' || pos === 'afterBegin') {
|
||||
this.innerHTML = html + (this.innerHTML || '');
|
||||
this.textContent = html.replace(/<[^>]*>/g, '') + (this.textContent || '');
|
||||
} else if (pos === 'beforebegin' || pos === 'beforeBegin') {
|
||||
if (this.parentElement) { this.parentElement.insertAdjacentHTML('beforeend', html); }
|
||||
} else if (pos === 'afterend' || pos === 'afterEnd') {
|
||||
if (this.parentElement) { this.parentElement.insertAdjacentHTML('beforeend', html); }
|
||||
}
|
||||
}
|
||||
}
|
||||
class CL { constructor(e){this._el=e;this._set=new Set();} _sync(str){this._set=new Set((str||'').split(/\s+/).filter(Boolean));} add(...c){for(const x of c)this._set.add(x);this._el.className=[...this._set].join(' ');this._el.attributes['class']=this._el.className;} remove(...c){for(const x of c)this._set.delete(x);this._el.className=[...this._set].join(' ');this._el.attributes['class']=this._el.className;} toggle(c,f){if(f!==undefined){if(f)this.add(c);else this.remove(c);return f;} if(this._set.has(c)){this.remove(c);return false;}else{this.add(c);return true;}} contains(c){return this._set.has(c);} get length(){return this._set.size;} [Symbol.iterator](){return this._set[Symbol.iterator]();} }
|
||||
class Ev { constructor(t,o={}){this.type=t;this.bubbles=o.bubbles||false;this.cancelable=o.cancelable!==false;this.defaultPrevented=false;this._sp=false;this._si=false;this.target=null;this.currentTarget=null;this.detail=o.detail||null;} preventDefault(){this.defaultPrevented=true;} stopPropagation(){this._sp=true;} stopImmediatePropagation(){this._sp=true;this._si=true;} }
|
||||
|
||||
function parseHTMLFragments(html) {
|
||||
const results = [];
|
||||
const re = /<(\w+)([^>]*?)(?:\/>|>([\s\S]*?)<\/\1>)/g;
|
||||
let m;
|
||||
let lastIndex = 0;
|
||||
while ((m = re.exec(html)) !== null) {
|
||||
// Text before this tag
|
||||
if (m.index > lastIndex) {
|
||||
const text = html.slice(lastIndex, m.index).trim();
|
||||
if (text) {
|
||||
const tn = {nodeType:3, textContent:text, data:text};
|
||||
// Can't push text nodes directly to El children; wrap if needed
|
||||
}
|
||||
}
|
||||
const tag = m[1]; const attrs = m[2]; const inner = m[3] || '';
|
||||
const el = new El(tag);
|
||||
const attrRe = /([\w-]+)="([^"]*)"/g; let am;
|
||||
while ((am = attrRe.exec(attrs))) el.setAttribute(am[1], am[2]);
|
||||
// Also handle boolean attrs like disabled
|
||||
const boolRe = /\s(\w+)(?=\s|\/|>|$)/g;
|
||||
if (inner) {
|
||||
// Recursively parse inner HTML
|
||||
const innerEls = parseHTMLFragments(inner);
|
||||
if (innerEls.length > 0) {
|
||||
for (const c of innerEls) el.appendChild(c);
|
||||
el.textContent = innerEls.map(c => c.textContent || '').join('');
|
||||
} else {
|
||||
el.textContent = inner;
|
||||
}
|
||||
el.innerHTML = inner;
|
||||
}
|
||||
results.push(el);
|
||||
lastIndex = re.lastIndex;
|
||||
}
|
||||
// If no tags found, treat as text — create a span with textContent
|
||||
if (results.length === 0 && html.trim()) {
|
||||
const el = new El('span');
|
||||
el.textContent = html.replace(/<[^>]*>/g, '');
|
||||
el.innerHTML = html;
|
||||
results.push(el);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function mt(e,s) {
|
||||
if(!e||!e.tagName)return false;
|
||||
s = s.trim();
|
||||
if(s.startsWith('#'))return e.id===s.slice(1);
|
||||
if(s.startsWith('.'))return e.classList.contains(s.slice(1));
|
||||
if(s.startsWith('[')) {
|
||||
const m = s.match(/^\[([^\]=]+)(?:="([^"]*)")?\]$/);
|
||||
if(m) return m[2] !== undefined ? e.getAttribute(m[1]) === m[2] : e.hasAttribute(m[1]);
|
||||
}
|
||||
if(s.includes('.')) { const [tag, cls] = s.split('.'); return e.tagName.toLowerCase() === tag && e.classList.contains(cls); }
|
||||
if(s.includes('#')) { const [tag, id] = s.split('#'); return e.tagName.toLowerCase() === tag && e.id === id; }
|
||||
return e.tagName.toLowerCase() === s.toLowerCase();
|
||||
}
|
||||
function fnd(e,s) { for(const c of(e.children||[])){if(mt(c,s))return c;const f=fnd(c,s);if(f)return f;} return null; }
|
||||
function fndAll(e,s) { const r=[];for(const c of(e.children||[])){if(mt(c,s))r.push(c);r.push(...fndAll(c,s));}return r; }
|
||||
|
||||
const _body = new El('body');
|
||||
const _html = new El('html');
|
||||
_html.appendChild(_body);
|
||||
const document = {
|
||||
body: _body, documentElement: _html,
|
||||
createElement(t){return new El(t);}, createElementNS(n,t){return new El(t);},
|
||||
createDocumentFragment(){const f=new El('fragment');f.nodeType=11;return f;},
|
||||
createTextNode(t){return{nodeType:3,textContent:t,data:t};},
|
||||
getElementById(i){return fnd(_body,'#'+i);},
|
||||
querySelector(s){return fnd(_body,s);}, querySelectorAll(s){return fndAll(_body,s);},
|
||||
createEvent(t){return new Ev(t);}, addEventListener(){}, removeEventListener(){},
|
||||
};
|
||||
globalThis.document=document; globalThis.window=globalThis; globalThis.HTMLElement=El; globalThis.Element=El;
|
||||
globalThis.Event=Ev; globalThis.CustomEvent=Ev; globalThis.NodeList=Array; globalThis.HTMLCollection=Array;
|
||||
globalThis.getComputedStyle=(e)=>e?e.style:{}; globalThis.requestAnimationFrame=(f)=>{f();return 0;};
|
||||
globalThis.cancelAnimationFrame=()=>{}; globalThis.MutationObserver=class{observe(){}disconnect(){}};
|
||||
globalThis.ResizeObserver=class{observe(){}disconnect(){}}; globalThis.IntersectionObserver=class{observe(){}disconnect(){}};
|
||||
globalThis.navigator={userAgent:'node'}; globalThis.location={href:'http://localhost/',pathname:'/',search:'',hash:''};
|
||||
globalThis.history={pushState(){},replaceState(){},back(){},forward(){}};
|
||||
const _origLog = console.log;
|
||||
globalThis.console = { log: () => {}, error: () => {}, warn: () => {}, info: () => {}, debug: () => {} }; // suppress ALL console noise
|
||||
const _log = _origLog; // keep reference for our own output
|
||||
|
||||
// ─── FFI ────────────────────────────────────────────────────────
|
||||
K.registerNative('host-global',a=>{const n=a[0];return(n in globalThis)?globalThis[n]:null;});
|
||||
K.registerNative('host-get',a=>{if(a[0]==null)return null;const v=a[0][a[1]];return v===undefined?null:v;});
|
||||
K.registerNative('host-set!',a=>{if(a[0]!=null){a[0][a[1]]=a[2]; /* sync innerHTML->children */ if(a[1]==='innerHTML'&&a[0] instanceof El){a[0]._setInnerHTML(a[2]);} if(a[1]==='textContent'&&a[0] instanceof El){a[0].textContent=String(a[2]);a[0].innerHTML=String(a[2]);for(const c of a[0].children){c.parentElement=null;c.parentNode=null;}a[0].children=[];a[0].childNodes=[];}} return a[2];});
|
||||
K.registerNative('host-call',a=>{if(_testDeadline&&Date.now()>_testDeadline)throw new Error('TIMEOUT: wall clock exceeded');const[o,m,...r]=a;if(o==null){const f=globalThis[m];return typeof f==='function'?f.apply(null,r):null;}if(o&&typeof o[m]==='function'){try{const v=o[m].apply(o,r);return v===undefined?null:v;}catch(e){return null;}}return null;});
|
||||
K.registerNative('host-new',a=>{const C=typeof a[0]==='string'?globalThis[a[0]]:a[0];return typeof C==='function'?new C(...a.slice(1)):null;});
|
||||
K.registerNative('host-callback',a=>{const fn=a[0];if(typeof fn==='function'&&fn.__sx_handle===undefined)return fn;if(fn&&fn.__sx_handle!==undefined)return function(){const r=K.callFn(fn,Array.from(arguments));if(globalThis._driveAsync)globalThis._driveAsync(r);return r;};return function(){};});
|
||||
K.registerNative('host-typeof',a=>{const o=a[0];if(o==null)return'nil';if(o instanceof El)return'element';if(o&&o.nodeType===3)return'text';if(o instanceof Ev)return'event';if(o instanceof Promise)return'promise';return typeof o;});
|
||||
K.registerNative('host-await',a=>{});
|
||||
K.registerNative('load-library!',()=>false);
|
||||
|
||||
let _testDeadline = 0;
|
||||
globalThis._driveAsync=function driveAsync(r,d){d=d||0;if(d>500||!r||!r.suspended)return;if(_testDeadline && Date.now()>_testDeadline)throw new Error('TIMEOUT: wall clock exceeded');const req=r.request;const items=req&&(req.items||req);const op=items&&items[0];const opName=typeof op==='string'?op:(op&&op.name)||String(op);function doResume(v){try{const x=r.resume(v);driveAsync(x,d+1);}catch(e){}}if(opName==='io-sleep'||opName==='wait')doResume(null);else if(opName==='io-fetch')doResume({ok:true,text:''});else if(opName==='io-settle')doResume(null);else if(opName==='io-wait-event')doResume(null);else if(opName==='io-transition')doResume(null);};
|
||||
|
||||
K.eval('(define SX_VERSION "hs-test-1.0")');K.eval('(define SX_ENGINE "ocaml-vm-sandbox")');
|
||||
K.eval('(define parse sx-parse)');K.eval('(define serialize sx-serialize)');
|
||||
|
||||
// ─── Load modules ──────────────────────────────────────────────
|
||||
process.stderr.write('Loading modules...\n');
|
||||
const t_mod = Date.now();
|
||||
const WEB=['render','core-signals','signals','deps','router','page-helpers','freeze','dom','browser','adapter-html','adapter-sx','adapter-dom','boot-helpers','hypersx','engine','orchestration','boot'];
|
||||
const HS=['hs-tokenizer','hs-parser','hs-compiler','hs-runtime','hs-integration'];
|
||||
K.beginModuleLoad();
|
||||
for(const mod of[...WEB,...HS]){const sp=path.join(SX_DIR,mod+'.sx');const lp=path.join(PROJECT,'lib/hyperscript',mod.replace(/^hs-/,'')+'.sx');let s;try{s=fs.existsSync(sp)?fs.readFileSync(sp,'utf8'):fs.readFileSync(lp,'utf8');}catch(e){continue;}try{K.load(s);}catch(e){process.stderr.write(`LOAD ERROR: ${mod}: ${e.message}\n`);}}
|
||||
K.endModuleLoad();
|
||||
process.stderr.write(`Modules loaded in ${Date.now()-t_mod}ms\n`);
|
||||
|
||||
// ─── Test framework ────────────────────────────────────────────
|
||||
K.eval('(define _test-registry (list))');K.eval('(define _test-suite "")');
|
||||
K.eval('(define push-suite (fn (name) (set! _test-suite name)))');
|
||||
K.eval('(define pop-suite (fn () (set! _test-suite "")))');
|
||||
K.eval('(define try-call (fn (thunk) (set! _test-registry (append _test-registry (list {:suite _test-suite :thunk thunk}))) {:ok true}))');
|
||||
K.eval('(define report-pass (fn (n) true))');
|
||||
K.eval('(define report-fail (fn (n e) true))');
|
||||
K.eval(`(define _run-test-thunk
|
||||
(fn (thunk)
|
||||
(guard (exn
|
||||
(true {:ok false :error (if (string? exn) exn (str exn))}))
|
||||
(thunk)
|
||||
{:ok true})))`);
|
||||
process.stderr.write('Loading tests...\n');
|
||||
const t_tests = Date.now();
|
||||
for(const f of['spec/harness.sx','spec/tests/test-framework.sx','spec/tests/test-hyperscript-behavioral.sx']){
|
||||
const t0=Date.now();
|
||||
try{K.load(fs.readFileSync(path.join(PROJECT,f),'utf8'));}catch(e){process.stderr.write(`TEST LOAD ERROR: ${f}: ${e.message}\n`);}
|
||||
process.stderr.write(` ${path.basename(f)}: ${Date.now()-t0}ms\n`);
|
||||
}
|
||||
process.stderr.write(`Tests loaded in ${Date.now()-t_tests}ms\n`);
|
||||
|
||||
const testCount = K.eval('(len _test-registry)');
|
||||
// Pre-read names
|
||||
const names = [];
|
||||
for(let i=0;i<testCount;i++) names.push({
|
||||
s: K.eval(`(get (nth _test-registry ${i}) "suite")`)||'',
|
||||
n: K.eval(`(get (nth _test-registry ${i}) "name")`)||`test-${i}`,
|
||||
});
|
||||
|
||||
const startTest = parseInt(process.env.HS_START || '0');
|
||||
const endTest = parseInt(process.env.HS_END || String(testCount));
|
||||
process.stdout.write(`Running tests ${startTest}-${endTest-1} of ${testCount} (step limit: ${STEP_LIMIT})...\n`);
|
||||
|
||||
let passed=0,failed=0;
|
||||
const cats={};const errTypes={};
|
||||
|
||||
for(let i=startTest;i<Math.min(endTest,testCount);i++){
|
||||
const {s:suite,n:name}=names[i];
|
||||
if(!cats[suite])cats[suite]={p:0,f:0,errs:[]};
|
||||
|
||||
// Reset body
|
||||
_body.children=[];_body.childNodes=[];_body.innerHTML='';_body.textContent='';
|
||||
|
||||
// Enable step limit for timeout protection
|
||||
setStepLimit(STEP_LIMIT);
|
||||
_testDeadline = Date.now() + 10000; // 10 second wall-clock timeout per test
|
||||
if(process.env.HS_VERBOSE)process.stderr.write(`T${i} `);
|
||||
|
||||
let ok=false,err=null;
|
||||
try{
|
||||
// Use SX-level guard to catch errors, avoiding __sxR side-channel issues
|
||||
// Returns a dict with :ok and :error keys
|
||||
K.eval(`(define _test-result (_run-test-thunk (get (nth _test-registry ${i}) "thunk")))`);
|
||||
const isOk=K.eval('(get _test-result "ok")');
|
||||
if(isOk===true){ok=true;}
|
||||
else{
|
||||
const errMsg=K.eval('(get _test-result "error")');
|
||||
err=errMsg?String(errMsg).slice(0,150):'unknown error';
|
||||
}
|
||||
}catch(e){err=(e.message||'').slice(0,150);}
|
||||
setStepLimit(0); // disable step limit between tests
|
||||
|
||||
const elapsed = Date.now() - (_testDeadline - 3000);
|
||||
if(ok){passed++;cats[suite].p++;}
|
||||
else{
|
||||
failed++;cats[suite].f++;cats[suite].errs.push({name,err});
|
||||
let t='other';
|
||||
if(err&&err.includes('TIMEOUT'))t='timeout';
|
||||
else if(err&&err.includes('NOT IMPLEMENTED'))t='stub';
|
||||
else if(err&&err.includes('Assertion'))t='assert-fail';
|
||||
else if(err&&err.includes('Expected'))t='wrong-value';
|
||||
else if(err&&err.includes('Undefined symbol'))t='undef-sym';
|
||||
else if(err&&err.includes('Unhandled'))t='unhandled';
|
||||
errTypes[t]=(errTypes[t]||0)+1;
|
||||
}
|
||||
_testDeadline = 0;
|
||||
if((i+1)%100===0)process.stdout.write(` ${i+1}/${testCount} (${passed} pass, ${failed} fail)\n`);
|
||||
if(elapsed > 5000)process.stdout.write(` SLOW: test ${i} took ${elapsed}ms [${suite}] ${name}\n`);
|
||||
if(!ok && err && err.includes('TIMEOUT'))process.stdout.write(` TIMEOUT: test ${i} [${suite}] ${name}\n`);
|
||||
if(!ok && err && err.includes('Expected') && err.includes(', got '))process.stdout.write(` WRONG: test ${i} [${suite}] ${name} — ${err}\n`);
|
||||
}
|
||||
|
||||
process.stdout.write(`\nResults: ${passed}/${passed+failed} (${(100*passed/(passed+failed)).toFixed(0)}%)\n\n`);
|
||||
process.stdout.write('By category:\n');
|
||||
for(const[cat,s]of Object.entries(cats).sort((a,b)=>{const ra=a[1].p/(a[1].p+a[1].f);const rb=b[1].p/(b[1].p+b[1].f);return rb-ra;})){
|
||||
const total=s.p+s.f;const pct=(100*s.p/total).toFixed(0);
|
||||
const mark=s.f===0?`✓ ${s.p}`:`${s.p}/${total} (${pct}%)`;
|
||||
process.stdout.write(` ${cat}: ${mark}\n`);
|
||||
}
|
||||
process.stdout.write('\nFailure types:\n');
|
||||
for(const[t,n]of Object.entries(errTypes).sort((a,b)=>b[1]-a[1])) process.stdout.write(` ${t}: ${n}\n`);
|
||||
const ue={};
|
||||
for(const[cat,s]of Object.entries(cats))for(const{err}of s.errs){const e=(err||'').slice(0,100);ue[e]=(ue[e]||0)+1;}
|
||||
process.stdout.write(`\nUnique errors (${Object.keys(ue).length}):\n`);
|
||||
for(const[e,n]of Object.entries(ue).sort((a,b)=>b[1]-a[1]).slice(0,30)) process.stdout.write(` [${n}x] ${e}\n`);
|
||||
203
tests/hs-verify.js
Normal file
203
tests/hs-verify.js
Normal file
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Verify that HS behavioral tests actually check assertions.
|
||||
* Runs a few tests manually with verbose output to prove pass/fail works.
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const PROJECT = path.resolve(__dirname, '..');
|
||||
|
||||
// Reuse the full runner's setup but run individual tests manually
|
||||
eval(fs.readFileSync(path.join(PROJECT, 'shared/static/wasm/sx_browser.bc.js'), 'utf8'));
|
||||
const K = globalThis.SxKernel;
|
||||
|
||||
// ─── DOM mock (minimal from runner) ─────────────────────────────
|
||||
class El {
|
||||
constructor(t) { this.tagName=t.toUpperCase(); this.nodeName=this.tagName; this.nodeType=1; this.id=''; this.className=''; this.classList=new CL(this); this.style={}; this.attributes={}; this.children=[]; this.childNodes=[]; this.parentElement=null; this.parentNode=null; this.textContent=''; this.innerHTML=''; this._listeners={}; this.dataset={}; this.value=''; this.checked=false; this.disabled=false; }
|
||||
setAttribute(n,v) { this.attributes[n]=String(v); if(n==='id')this.id=v; if(n==='class'){this.className=v;this.classList._sync(v);} if(n==='value')this.value=v; }
|
||||
getAttribute(n) { return this.attributes[n]!==undefined?this.attributes[n]:null; }
|
||||
removeAttribute(n) { delete this.attributes[n]; }
|
||||
hasAttribute(n) { return n in this.attributes; }
|
||||
addEventListener(e,f) { if(!this._listeners[e])this._listeners[e]=[]; this._listeners[e].push(f); }
|
||||
removeEventListener(e,f) { if(this._listeners[e])this._listeners[e]=this._listeners[e].filter(x=>x!==f); }
|
||||
dispatchEvent(ev) { ev.target=ev.target||this; ev.currentTarget=this; const fns=[...(this._listeners[ev.type]||[])]; for(const f of fns){if(ev._si)break;try{f.call(this,ev);}catch(e){}} if(ev.bubbles&&!ev._sp&&this.parentElement){this.parentElement.dispatchEvent(ev);} return !ev.defaultPrevented; }
|
||||
appendChild(c) { if(c.parentElement)c.parentElement.removeChild(c); c.parentElement=this; c.parentNode=this; this.children.push(c); this.childNodes.push(c); return c; }
|
||||
removeChild(c) { this.children=this.children.filter(x=>x!==c); this.childNodes=this.childNodes.filter(x=>x!==c); c.parentElement=null; c.parentNode=null; return c; }
|
||||
insertBefore(n,r) { if(n.parentElement)n.parentElement.removeChild(n); const i=this.children.indexOf(r); if(i>=0){this.children.splice(i,0,n);this.childNodes.splice(i,0,n);}else{this.children.push(n);this.childNodes.push(n);} n.parentElement=this;n.parentNode=this; return n; }
|
||||
querySelector(s) { return fnd(this,s); }
|
||||
querySelectorAll(s) { return fndAll(this,s); }
|
||||
closest(s) { let e=this; while(e){if(mt(e,s))return e; e=e.parentElement;} return null; }
|
||||
matches(s) { return mt(this,s); }
|
||||
contains(o) { if(o===this)return true; for(const c of this.children)if(c===o||c.contains(o))return true; return false; }
|
||||
cloneNode(d) { const e=new El(this.tagName.toLowerCase()); Object.assign(e.attributes,this.attributes); e.id=this.id; e.className=this.className; e.classList._sync(this.className); Object.assign(e.style,this.style); e.textContent=this.textContent; e.innerHTML=this.innerHTML; e.value=this.value; if(d)for(const c of this.children)e.appendChild(c.cloneNode(true)); return e; }
|
||||
focus(){} blur(){} click(){this.dispatchEvent(new Ev('click',{bubbles:true}));} remove(){if(this.parentElement)this.parentElement.removeChild(this);}
|
||||
get firstElementChild() { return this.children[0]||null; }
|
||||
get lastElementChild() { return this.children[this.children.length-1]||null; }
|
||||
get nextElementSibling() { if(!this.parentElement)return null; const i=this.parentElement.children.indexOf(this); return this.parentElement.children[i+1]||null; }
|
||||
get previousElementSibling() { if(!this.parentElement)return null; const i=this.parentElement.children.indexOf(this); return i>0?this.parentElement.children[i-1]:null; }
|
||||
showModal(){} show(){} close(){} getAnimations(){return[];} getBoundingClientRect(){return{top:0,left:0,width:100,height:100,right:100,bottom:100};} scrollIntoView(){}
|
||||
get ownerDocument() { return document; }
|
||||
_setInnerHTML(html) { for(const c of this.children){c.parentElement=null;} this.children=[]; this.childNodes=[]; this.innerHTML=html; this.textContent=html.replace(/<[^>]*>/g,''); }
|
||||
insertAdjacentHTML() {}
|
||||
}
|
||||
class CL { constructor(e){this._el=e;this._set=new Set();} _sync(str){this._set=new Set((str||'').split(/\s+/).filter(Boolean));} add(...c){for(const x of c)this._set.add(x);this._el.className=[...this._set].join(' ');this._el.attributes['class']=this._el.className;} remove(...c){for(const x of c)this._set.delete(x);this._el.className=[...this._set].join(' ');this._el.attributes['class']=this._el.className;} toggle(c,f){if(f!==undefined){if(f)this.add(c);else this.remove(c);return f;} if(this._set.has(c)){this.remove(c);return false;}else{this.add(c);return true;}} contains(c){return this._set.has(c);} get length(){return this._set.size;} [Symbol.iterator](){return this._set[Symbol.iterator]();} }
|
||||
class Ev { constructor(t,o={}){this.type=t;this.bubbles=o.bubbles||false;this.cancelable=o.cancelable!==false;this.defaultPrevented=false;this._sp=false;this._si=false;this.target=null;this.currentTarget=null;this.detail=o.detail||null;} preventDefault(){this.defaultPrevented=true;} stopPropagation(){this._sp=true;} stopImmediatePropagation(){this._sp=true;this._si=true;} }
|
||||
|
||||
function mt(e,s) { if(!e||!e.tagName)return false; s=s.trim(); if(s.startsWith('#'))return e.id===s.slice(1); if(s.startsWith('.'))return e.classList.contains(s.slice(1)); if(s.startsWith('[')){const m=s.match(/^\[([^\]=]+)(?:="([^"]*)")?\]$/);if(m)return m[2]!==undefined?e.getAttribute(m[1])===m[2]:e.hasAttribute(m[1]);} if(s.includes('.')){const[tag,cls]=s.split('.');return e.tagName.toLowerCase()===tag&&e.classList.contains(cls);} if(s.includes('#')){const[tag,id]=s.split('#');return e.tagName.toLowerCase()===tag&&e.id===id;} return e.tagName.toLowerCase()===s.toLowerCase(); }
|
||||
function fnd(e,s) { for(const c of(e.children||[])){if(mt(c,s))return c;const f=fnd(c,s);if(f)return f;} return null; }
|
||||
function fndAll(e,s) { const r=[];for(const c of(e.children||[])){if(mt(c,s))r.push(c);r.push(...fndAll(c,s));}return r; }
|
||||
|
||||
const _body = new El('body'); const _html = new El('html'); _html.appendChild(_body);
|
||||
const document = { body:_body, documentElement:_html, createElement(t){return new El(t);}, createElementNS(n,t){return new El(t);}, createDocumentFragment(){const f=new El('fragment');f.nodeType=11;return f;}, createTextNode(t){return{nodeType:3,textContent:t,data:t};}, getElementById(i){return fnd(_body,'#'+i);}, querySelector(s){return fnd(_body,s);}, querySelectorAll(s){return fndAll(_body,s);}, createEvent(t){return new Ev(t);}, addEventListener(){}, removeEventListener(){} };
|
||||
globalThis.document=document; globalThis.window=globalThis; globalThis.HTMLElement=El; globalThis.Element=El;
|
||||
globalThis.Event=Ev; globalThis.CustomEvent=Ev; globalThis.NodeList=Array; globalThis.HTMLCollection=Array;
|
||||
globalThis.getComputedStyle=(e)=>e?e.style:{}; globalThis.requestAnimationFrame=(f)=>{f();return 0;};
|
||||
globalThis.cancelAnimationFrame=()=>{}; globalThis.MutationObserver=class{observe(){}disconnect(){}};
|
||||
globalThis.ResizeObserver=class{observe(){}disconnect(){}}; globalThis.IntersectionObserver=class{observe(){}disconnect(){}};
|
||||
globalThis.navigator={userAgent:'node'}; globalThis.location={href:'http://localhost/',pathname:'/',search:'',hash:''};
|
||||
globalThis.history={pushState(){},replaceState(){},back(){},forward(){}};
|
||||
globalThis.console={log:()=>{},error:()=>{},warn:()=>{},info:()=>{},debug:()=>{}};
|
||||
|
||||
// FFI
|
||||
K.registerNative('host-global',a=>{const n=a[0];return(n in globalThis)?globalThis[n]:null;});
|
||||
K.registerNative('host-get',a=>{if(a[0]==null)return null;const v=a[0][a[1]];return v===undefined?null:v;});
|
||||
K.registerNative('host-set!',a=>{if(a[0]!=null){a[0][a[1]]=a[2];if(a[1]==='innerHTML'&&a[0] instanceof El){a[0]._setInnerHTML(a[2]);}if(a[1]==='textContent'&&a[0] instanceof El){a[0].textContent=String(a[2]);}} return a[2];});
|
||||
K.registerNative('host-call',a=>{const[o,m,...r]=a;if(o==null){const f=globalThis[m];return typeof f==='function'?f.apply(null,r):null;}if(o&&typeof o[m]==='function'){try{const v=o[m].apply(o,r);return v===undefined?null:v;}catch(e){return null;}}return null;});
|
||||
K.registerNative('host-new',a=>{const C=typeof a[0]==='string'?globalThis[a[0]]:a[0];return typeof C==='function'?new C(...a.slice(1)):null;});
|
||||
K.registerNative('host-callback',a=>{const fn=a[0];if(typeof fn==='function'&&fn.__sx_handle===undefined)return fn;if(fn&&fn.__sx_handle!==undefined)return function(){const r=K.callFn(fn,Array.from(arguments));if(globalThis._driveAsync)globalThis._driveAsync(r);return r;};return function(){};});
|
||||
K.registerNative('host-typeof',a=>{const o=a[0];if(o==null)return'nil';if(o instanceof El)return'element';if(o&&o.nodeType===3)return'text';if(o instanceof Ev)return'event';return typeof o;});
|
||||
K.registerNative('host-await',a=>{});
|
||||
K.registerNative('load-library!',()=>false);
|
||||
globalThis._driveAsync=function driveAsync(r,d){d=d||0;if(d>500||!r||!r.suspended)return;const req=r.request;const items=req&&(req.items||req);const op=items&&items[0];const opName=typeof op==='string'?op:(op&&op.name)||String(op);function doResume(v){try{const x=r.resume(v);driveAsync(x,d+1);}catch(e){}}if(opName==='io-sleep'||opName==='wait')doResume(null);else if(opName==='io-settle')doResume(null);else if(opName==='io-wait-event')doResume(null);else if(opName==='io-transition')doResume(null);};
|
||||
|
||||
K.eval('(define SX_VERSION "hs-test-1.0")');K.eval('(define SX_ENGINE "ocaml-vm-sandbox")');
|
||||
K.eval('(define parse sx-parse)');K.eval('(define serialize sx-serialize)');
|
||||
|
||||
// Load modules
|
||||
const SD = path.join(PROJECT, 'shared/static/wasm/sx');
|
||||
const WEB=['render','core-signals','signals','deps','router','page-helpers','freeze','dom','browser','adapter-html','adapter-sx','adapter-dom','boot-helpers','hypersx','engine','orchestration','boot'];
|
||||
const HS=['hs-tokenizer','hs-parser','hs-compiler','hs-runtime','hs-integration'];
|
||||
K.beginModuleLoad();
|
||||
for(const mod of[...WEB,...HS]){const sp=path.join(SD,mod+'.sx');const lp=path.join(PROJECT,'lib/hyperscript',mod.replace(/^hs-/,'')+'.sx');let s;try{s=fs.existsSync(sp)?fs.readFileSync(sp,'utf8'):fs.readFileSync(lp,'utf8');}catch(e){continue;}try{K.load(s);}catch(e){}}
|
||||
K.endModuleLoad();
|
||||
|
||||
const _log = process.stdout.write.bind(process.stdout);
|
||||
function log(s) { _log(s + '\n'); }
|
||||
|
||||
function reset() { _body.children=[]; _body.childNodes=[]; _body.innerHTML=''; _body.textContent=''; }
|
||||
|
||||
// ─── VERIFICATION TESTS ─────────────────────────────────────────
|
||||
|
||||
log('=== VERIFICATION: Do assertions actually catch failures? ===\n');
|
||||
|
||||
// Test 1: Positive — add .foo should work
|
||||
reset();
|
||||
log('1. Positive test: "on click add .foo" → assert .foo present');
|
||||
try {
|
||||
K.eval(`
|
||||
(let ((_el (dom-create-element "div")))
|
||||
(dom-set-attr _el "_" "on click add .foo")
|
||||
(dom-append (dom-body) _el)
|
||||
(hs-activate! _el)
|
||||
(dom-dispatch _el "click" nil)
|
||||
(assert (dom-has-class? _el "foo") "should have class foo"))
|
||||
`);
|
||||
log(' PASS ✓');
|
||||
} catch(e) { log(' FAIL ✗: ' + (e.message||'').slice(0,150)); }
|
||||
|
||||
// Test 2: Negative — assert wrong class (SHOULD FAIL)
|
||||
reset();
|
||||
log('\n2. Negative test: "on click add .foo" → assert .bar present (should fail!)');
|
||||
try {
|
||||
K.eval(`
|
||||
(let ((_el (dom-create-element "div")))
|
||||
(dom-set-attr _el "_" "on click add .foo")
|
||||
(dom-append (dom-body) _el)
|
||||
(hs-activate! _el)
|
||||
(dom-dispatch _el "click" nil)
|
||||
(assert (dom-has-class? _el "bar") "should have class bar"))
|
||||
`);
|
||||
log(' PASS ✗ (BUG — should have failed!)');
|
||||
} catch(e) { log(' FAIL ✓ (correctly caught): ' + (e.message||'').slice(0,100)); }
|
||||
|
||||
// Test 3: Positive — toggle class
|
||||
reset();
|
||||
log('\n3. Positive test: "on click toggle .active" → assert .active present');
|
||||
try {
|
||||
K.eval(`
|
||||
(let ((_el (dom-create-element "div")))
|
||||
(dom-set-attr _el "_" "on click toggle .active on me")
|
||||
(dom-append (dom-body) _el)
|
||||
(hs-activate! _el)
|
||||
(dom-dispatch _el "click" nil)
|
||||
(assert (dom-has-class? _el "active") "should have class active"))
|
||||
`);
|
||||
log(' PASS ✓');
|
||||
} catch(e) { log(' FAIL ✗: ' + (e.message||'').slice(0,150)); }
|
||||
|
||||
// Test 4: Negative — assert wrong value
|
||||
reset();
|
||||
log('\n4. Negative test: assert-equal with wrong value (should fail!)');
|
||||
try {
|
||||
K.eval(`(assert (= 1 2) "1 should equal 2")`);
|
||||
log(' PASS ✗ (BUG — should have failed!)');
|
||||
} catch(e) { log(' FAIL ✓ (correctly caught): ' + (e.message||'').slice(0,100)); }
|
||||
|
||||
// Test 5: set innerHTML
|
||||
reset();
|
||||
log('\n5. Positive test: "on click set my innerHTML to \'hello\'"');
|
||||
try {
|
||||
K.eval(`
|
||||
(let ((_el (dom-create-element "div")))
|
||||
(dom-set-attr _el "_" "on click set my innerHTML to 'hello'")
|
||||
(dom-append (dom-body) _el)
|
||||
(hs-activate! _el)
|
||||
(dom-dispatch _el "click" nil)
|
||||
(assert= (dom-get-prop _el "innerHTML") "hello"))
|
||||
`);
|
||||
log(' PASS ✓');
|
||||
} catch(e) { log(' FAIL ✗: ' + (e.message||'').slice(0,150)); }
|
||||
|
||||
// Test 6: remove class
|
||||
reset();
|
||||
log('\n6. Positive test: "on click remove .foo" from div with class foo');
|
||||
try {
|
||||
K.eval(`
|
||||
(let ((_el (dom-create-element "div")))
|
||||
(dom-add-class _el "foo")
|
||||
(dom-set-attr _el "_" "on click remove .foo")
|
||||
(dom-append (dom-body) _el)
|
||||
(hs-activate! _el)
|
||||
(dom-dispatch _el "click" nil)
|
||||
(assert (not (dom-has-class? _el "foo")) "should not have class foo"))
|
||||
`);
|
||||
log(' PASS ✓');
|
||||
} catch(e) { log(' FAIL ✗: ' + (e.message||'').slice(0,150)); }
|
||||
|
||||
// Test 7: Negative — remove class that wasn't added (should pass — no-op)
|
||||
reset();
|
||||
log('\n7. Check: does callFn returning null count as pass?');
|
||||
K.setStepLimit(1000);
|
||||
try {
|
||||
const r = K.callFn(K.eval('(fn () (assert false "deliberate failure"))'), []);
|
||||
log(' callFn returned: ' + JSON.stringify(r) + ' (type: ' + typeof r + ')');
|
||||
log(' WARNING: callFn did NOT throw on assertion failure!');
|
||||
} catch(e) {
|
||||
log(' callFn threw: ' + (e.message||'').slice(0,100));
|
||||
log(' OK — assertions propagate as exceptions');
|
||||
}
|
||||
K.setStepLimit(0);
|
||||
|
||||
// Test 8: What does callFn return on step limit timeout?
|
||||
log('\n8. Check: what happens on step limit timeout?');
|
||||
K.setStepLimit(50);
|
||||
try {
|
||||
const r = K.callFn(K.eval('(fn () (let loop ((i 0)) (loop (+ i 1))))'), []);
|
||||
log(' callFn returned: ' + JSON.stringify(r) + ' (type: ' + typeof r + ')');
|
||||
if (r === null) log(' WARNING: timeout returns null, not exception — runner counts as PASS!');
|
||||
} catch(e) {
|
||||
log(' callFn threw: ' + (e.message||'').slice(0,100));
|
||||
}
|
||||
K.setStepLimit(0);
|
||||
|
||||
log('\n=== DONE ===');
|
||||
Reference in New Issue
Block a user