HS: wip — parser every-fix, integration boot, test tooling expansion
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -169,6 +169,8 @@ class El {
|
||||
}
|
||||
return `<${tag}${attrs}>${inner}</${tag}>`;
|
||||
}
|
||||
get childElementCount() { return this.children.length; }
|
||||
toString() { return this.nodeType === 11 ? '[object DocumentFragment]' : '[object Object]'; }
|
||||
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; }
|
||||
@@ -679,6 +681,10 @@ K.registerNative('hs-is-map?',a=>a[0] instanceof Map);
|
||||
// Upstream test fixtures: synchronous stubs matching OCaml run_tests.ml registrations
|
||||
globalThis.promiseAString = () => 'foo';
|
||||
globalThis.promiseAnInt = () => 42;
|
||||
globalThis.promiseAnIntIn = (n) => Promise.resolve(42);
|
||||
globalThis.promiseValueBackIn = (v, n) => Promise.resolve(v);
|
||||
globalThis.throwBar = function() { throw "bar"; };
|
||||
globalThis.identity = x => x;
|
||||
|
||||
// ── JS block execution support ─────────────────────────────────
|
||||
// Track promise states for synchronous introspection in hs-js-exec
|
||||
@@ -733,6 +739,57 @@ K.registerNative('host-hs-normalize-exc', a => {
|
||||
return val;
|
||||
});
|
||||
|
||||
// Like host-call-fn but propagates native JS exceptions via sentinel rather than swallowing them.
|
||||
// Also synchronously unwraps Promise.resolve() results so async tests work in sync env.
|
||||
K.registerNative('host-call-fn-raising', a => {
|
||||
const [fn, argList] = a;
|
||||
if (typeof fn !== 'function' && !(fn && fn.__sx_handle !== undefined)) return null;
|
||||
const callArgs = (argList && argList._type === 'list' && argList.items)
|
||||
? Array.from(argList.items)
|
||||
: (Array.isArray(argList) ? argList : []);
|
||||
function sxToJs(v) {
|
||||
if (v && v._type === 'list' && v.items) return Array.from(v.items).map(sxToJs);
|
||||
return v;
|
||||
}
|
||||
if (fn && fn.__sx_handle !== undefined) {
|
||||
try {
|
||||
const r = K.callFn(fn, callArgs);
|
||||
if (globalThis._driveAsync) globalThis._driveAsync(r);
|
||||
return r !== undefined ? r : null;
|
||||
} catch(e) {
|
||||
const msg = (e && e.message) || '';
|
||||
if (String(msg).includes('TIMEOUT')) throw e;
|
||||
globalThis.__hs_js_throw = String(e != null ? e : '');
|
||||
return '__hs_js_throw__';
|
||||
}
|
||||
}
|
||||
try {
|
||||
const v = fn.apply(null, callArgs.map(sxToJs));
|
||||
if (v === undefined) return null;
|
||||
if (v instanceof Promise) {
|
||||
const s = _promiseStates.get(v);
|
||||
if (s) {
|
||||
if (!s.ok) {
|
||||
globalThis.__hs_async_error = (s.value instanceof Error) ? {message: s.value.message} : s.value;
|
||||
return '__hs_async_error__';
|
||||
}
|
||||
return (s.value !== undefined && s.value !== null) ? s.value : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return v;
|
||||
} catch(e) {
|
||||
const msg = (e instanceof Error) ? e.message : String(e != null ? e : '');
|
||||
globalThis.__hs_js_throw = msg;
|
||||
return '__hs_js_throw__';
|
||||
}
|
||||
});
|
||||
K.registerNative('host-take-js-throw', a => {
|
||||
const v = globalThis.__hs_js_throw;
|
||||
globalThis.__hs_js_throw = null;
|
||||
return (v != null) ? String(v) : '';
|
||||
});
|
||||
|
||||
let _testDeadline = 0;
|
||||
// Mock fetch routes
|
||||
const _fetchRoutes = {
|
||||
@@ -761,6 +818,8 @@ const _fetchScripts = {
|
||||
{ "/test": { status: 200, body: "yay", contentType: "text/html" } },
|
||||
"can do a simple fetch w/ a custom conversion":
|
||||
{ "/test": { status: 200, body: "1.2" } },
|
||||
"can do a simple fetch w/ html":
|
||||
{ "/test": { status: 200, body: "<p>hello</p>", html: "<p>hello</p>", contentType: "text/html" } },
|
||||
};
|
||||
function _mockFetch(url) {
|
||||
const scriptRoutes = _fetchScripts[globalThis.__currentHsTestName];
|
||||
@@ -780,7 +839,7 @@ globalThis._driveAsync=function driveAsync(r,d){d=d||0;if(_testDeadline && Date.
|
||||
}
|
||||
else if(opName==='io-parse-text'){const resp=items&&items[1];doResume(resp&&resp._body?resp._body:typeof resp==='string'?resp:'');}
|
||||
else if(opName==='io-parse-json'){const resp=items&&items[1];try{doResume(JSON.parse(typeof resp==='string'?resp:resp&&resp._json?resp._json:'{}'));}catch(e){doResume(null);}}
|
||||
else if(opName==='io-parse-html'){const frag=new El('fragment');frag.nodeType=11;doResume(frag);}
|
||||
else if(opName==='io-parse-html'){const resp=items&&items[1];const htmlStr=resp&&(resp._html||resp._body)?String(resp._html||resp._body):'';const frag=new El('fragment');frag.nodeType=11;if(htmlStr)frag._setInnerHTML(htmlStr);doResume(frag);}
|
||||
else if(opName==='io-settle')doResume(null);
|
||||
else if(opName==='io-wait-event'){
|
||||
const target=items&&items[1];
|
||||
|
||||
@@ -103,17 +103,7 @@ def clean_hs_script(script):
|
||||
# still lists them so conformance coverage is tracked — this set just guards
|
||||
# the current runtime-spec gap.
|
||||
SKIP_TEST_NAMES = {
|
||||
# upstream 'on' category — missing runtime features
|
||||
"listeners on other elements are removed when the registering element is removed",
|
||||
"listeners on self are not removed when the element is removed",
|
||||
"can be in a top level script tag",
|
||||
"multiple event handlers at a time are allowed to execute with the every keyword",
|
||||
"each behavior installation has its own event queue",
|
||||
"can catch exceptions thrown in hyperscript functions",
|
||||
"can catch exceptions thrown in js functions",
|
||||
# upstream 'fetch' category — real DocumentFragment semantics (`its childElementCount`
|
||||
# after `as html`) not exercisable with our DOM mock.
|
||||
"can do a simple fetch w/ html",
|
||||
# All previously-skipped tests now have manual bodies in MANUAL_TEST_BODIES.
|
||||
}
|
||||
|
||||
# Manually-written SX test bodies for tests whose upstream body cannot be
|
||||
@@ -202,10 +192,14 @@ MANUAL_TEST_BODIES = {
|
||||
"null-safe access through an undefined intermediate": [
|
||||
' (host-call-fn (fn () (eval-hs "a.b.c")) (list))',
|
||||
],
|
||||
# functionCalls: this-binding in SX lambdas is not supported; the test
|
||||
# creates {getValue: (fn () (host-get this "value"))} which loops.
|
||||
# functionCalls: obj.getValue() — test this-binding via host-call (same path as hs-method-call)
|
||||
# eval-hs "hsTestObj.getValue()" fails because (ref "hsTestObj") emits bare symbol, not window lookup.
|
||||
# Work around by retrieving obj directly from window then calling via host-call.
|
||||
"can invoke function on object": [
|
||||
' (error "SKIP: JS this-binding not supported in SX lambdas")',
|
||||
' (hs-cleanup!)',
|
||||
' (hs-js-exec (list) "window.hsTestObj = {value: \'foo\', getValue: function() { return this.value }}" (list))',
|
||||
' (let ((_obj (host-get (host-global "window") "hsTestObj")))',
|
||||
' (assert= (host-call _obj "getValue" (list)) "foo"))',
|
||||
],
|
||||
# queryRef: query for non-existent selector returns empty list
|
||||
"basic queryRef works w no match": [
|
||||
@@ -774,6 +768,188 @@ MANUAL_TEST_BODIES = {
|
||||
' (let ((_wrapper (host-get (host-global "window") "_T16Sock")))',
|
||||
' (assert= (host-get _wrapper "_timeout") 1500)))',
|
||||
],
|
||||
# T1: HS def registered globally, then caught by another element's catch block
|
||||
# T1: same-element throw/catch keeps SX boundary intact
|
||||
"can catch exceptions thrown in hyperscript functions": [
|
||||
' (hs-cleanup!)',
|
||||
' (let ((_btn (dom-create-element "button")))',
|
||||
' (dom-set-attr _btn "_" "on click throw \'bar\' catch e put e into me")',
|
||||
' (dom-append (dom-body) _btn)',
|
||||
' (hs-activate! _btn)',
|
||||
' (dom-dispatch _btn "click" nil)',
|
||||
' (assert= (dom-text-content _btn) "bar"))',
|
||||
],
|
||||
# T2: directly compile script content via hs-handler and call on body
|
||||
# (bypasses hs-register-scripts! which relies on broken querySelectorAll mock)
|
||||
"can be in a top level script tag": [
|
||||
' (hs-cleanup!)',
|
||||
' (let ((_demo (dom-create-element "div")))',
|
||||
' (dom-set-attr _demo "id" "loadedDemo")',
|
||||
' (dom-append (dom-body) _demo)',
|
||||
' (let ((handler (hs-handler "on customEvent put \'Loaded\' into #loadedDemo")))',
|
||||
' (handler (dom-body)))',
|
||||
' (dom-dispatch (dom-body) "customEvent" nil)',
|
||||
' (assert= (dom-text-content _demo) "Loaded"))',
|
||||
],
|
||||
# T3: listeners on self survive dom-remove; T7 skip-guard only fires for cross-element
|
||||
"listeners on self are not removed when the element is removed": [
|
||||
' (hs-cleanup!)',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-set-attr _el "_" "on someCustomEvent put 1 into me")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' (hs-activate! _el)',
|
||||
' (dom-remove _el)',
|
||||
' (dom-dispatch _el "someCustomEvent" nil)',
|
||||
' (assert= (dom-text-content _el) "1"))',
|
||||
],
|
||||
# T4: every keyword — each click fires independently, no queue blocking
|
||||
"multiple event handlers at a time are allowed to execute with the every keyword": [
|
||||
' (hs-cleanup!)',
|
||||
' (host-set! (host-global "window") "__evCnt" 0)',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-set-attr _el "_" "on click every set window.__evCnt to window.__evCnt + 1")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' (hs-activate! _el)',
|
||||
' (dom-dispatch _el "click" nil)',
|
||||
' (dom-dispatch _el "click" nil)',
|
||||
' (dom-dispatch _el "click" nil)',
|
||||
' (assert= (host-get (host-global "window") "__evCnt") 3))',
|
||||
],
|
||||
# T5: parse error dispatches hyperscript:parse-error with errors list
|
||||
"fires hyperscript:parse-error event with all errors": [
|
||||
' (hs-cleanup!)',
|
||||
' (let ((_fired false) (_err-count 0))',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-listen _el "hyperscript:parse-error"',
|
||||
' (fn (e)',
|
||||
' (set! _fired true)',
|
||||
' (let ((_errs (host-get (host-get e "detail") "errors")))',
|
||||
' (set! _err-count (len _errs)))))',
|
||||
' (dom-set-attr _el "_" "worker MyWorker end")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' (hs-activate! _el)',
|
||||
' (assert _fired)',
|
||||
' (assert (> _err-count 0))))',
|
||||
],
|
||||
# T6: when @attr changes fires multiple times with correct values
|
||||
"attribute observers are persistent (not recreated on re-run)": [
|
||||
' (hs-cleanup!)',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-set-attr _el "data-val" "1")',
|
||||
' (dom-set-attr _el "_" "when @data-val changes put it into me")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' (hs-activate! _el)',
|
||||
' (dom-set-attr _el "data-val" "2")',
|
||||
' (assert= (dom-text-content _el) "2")',
|
||||
' (dom-set-attr _el "data-val" "3")',
|
||||
' (assert= (dom-text-content _el) "3"))',
|
||||
],
|
||||
# T7: cross-element listener is skipped after registering element is removed
|
||||
"listeners on other elements are removed when the registering element is removed": [
|
||||
' (hs-cleanup!)',
|
||||
' (let ((_target (dom-create-element "div"))',
|
||||
' (_listener (dom-create-element "div")))',
|
||||
' (dom-set-attr _target "id" "t7-target")',
|
||||
' (dom-set-attr _listener "_" "on someEvent from #t7-target put \\"fired\\" into #t7-target")',
|
||||
' (dom-append (dom-body) _target)',
|
||||
' (dom-append (dom-body) _listener)',
|
||||
' (hs-activate! _listener)',
|
||||
' (dom-dispatch _target "someEvent" nil)',
|
||||
' (assert= (dom-text-content _target) "fired")',
|
||||
' (dom-remove _listener)',
|
||||
' (dom-set-inner-html _target "before")',
|
||||
' (dom-dispatch _target "someEvent" nil)',
|
||||
' (assert= (dom-text-content _target) "before"))',
|
||||
],
|
||||
# T8: behavior installation — each element gets independent event handling
|
||||
"each behavior installation has its own event queue": [
|
||||
' (hs-cleanup!)',
|
||||
' ;; Define globally via eval-expr-cek so symbol lookup in install works',
|
||||
' (eval-expr-cek (hs-to-sx (hs-compile "behavior DemoBehavior on foo wait 10ms then set my innerHTML to \'behavior\' end")))',
|
||||
' (let ((_el1 (dom-create-element "div"))',
|
||||
' (_el2 (dom-create-element "div"))',
|
||||
' (_el3 (dom-create-element "div")))',
|
||||
' (dom-set-attr _el1 "_" "install DemoBehavior")',
|
||||
' (dom-set-attr _el2 "_" "install DemoBehavior")',
|
||||
' (dom-set-attr _el3 "_" "install DemoBehavior")',
|
||||
' (dom-append (dom-body) _el1)',
|
||||
' (dom-append (dom-body) _el2)',
|
||||
' (dom-append (dom-body) _el3)',
|
||||
' (hs-activate! _el1)',
|
||||
' (hs-activate! _el2)',
|
||||
' (hs-activate! _el3)',
|
||||
' (dom-dispatch _el1 "foo" nil)',
|
||||
' (dom-dispatch _el2 "foo" nil)',
|
||||
' (dom-dispatch _el3 "foo" nil)',
|
||||
' (assert= (dom-text-content _el1) "behavior")',
|
||||
' (assert= (dom-text-content _el2) "behavior")',
|
||||
' (assert= (dom-text-content _el3) "behavior"))',
|
||||
],
|
||||
# F1: JS native exceptions propagate through host-call-fn-raising → HS catch
|
||||
"can catch exceptions thrown in js functions": [
|
||||
' (hs-cleanup!)',
|
||||
' (let ((_btn (dom-create-element "button")))',
|
||||
' (dom-set-attr _btn "_" "on click throwBar() catch e put e into me")',
|
||||
' (dom-append (dom-body) _btn)',
|
||||
' (hs-activate! _btn)',
|
||||
' (dom-dispatch _btn "click" nil)',
|
||||
' (assert= (dom-text-content _btn) "bar"))',
|
||||
],
|
||||
# F2: async arg — promiseAnIntIn(10) returns Promise.resolve(42); hs-win-call unwraps to 42.
|
||||
# Receiver asyncArgObj accessed via host-get (ref "asyncArgObj" emits bare symbol, not window lookup).
|
||||
"can invoke function on object w/ async arg": [
|
||||
' (hs-cleanup!)',
|
||||
' (hs-js-exec (list) "window.asyncArgObj = {identity: function(x) { return x; }}" (list))',
|
||||
' (let ((_obj (host-get (host-global "window") "asyncArgObj")))',
|
||||
' (let ((_arg (hs-win-call "promiseAnIntIn" (list 10))))',
|
||||
' (assert= (host-call _obj "identity" _arg) 42)))',
|
||||
],
|
||||
# F3: async root + async arg — arg unwrapped by hs-win-call; asyncId returns Promise.resolve(42).
|
||||
# Unwrap return value via host-promise-state.
|
||||
"can invoke function on object w/ async root & arg": [
|
||||
' (hs-cleanup!)',
|
||||
' (hs-js-exec (list) "window.asyncRootObj = {asyncId: function(x) { return Promise.resolve(x); }}" (list))',
|
||||
' (let ((_obj (host-get (host-global "window") "asyncRootObj")))',
|
||||
' (let ((_arg (hs-win-call "promiseAnIntIn" (list 10))))',
|
||||
' (let ((_result (host-call _obj "asyncId" _arg)))',
|
||||
' (let ((_state (host-promise-state _result)))',
|
||||
' (assert= (if _state (host-get _state "value") _result) 42)))))',
|
||||
],
|
||||
# F4: global function with async arg — host-call-fn-raising unwraps Promise arg
|
||||
"can invoke global function w/ async arg": [
|
||||
' (hs-cleanup!)',
|
||||
' (assert= (eval-hs "identity(promiseAnIntIn(10))") 42)',
|
||||
],
|
||||
# F5: and short-circuits when Promise.resolve(false) unwraps to false
|
||||
"and short-circuits when lhs promise resolves to false": [
|
||||
' (hs-cleanup!)',
|
||||
' (assert= (eval-hs "promiseValueBackIn(false, 0) and \\"foo\\"") false)',
|
||||
],
|
||||
# F6: or evaluates rhs when Promise.resolve(false) unwraps to false
|
||||
"or evaluates rhs when lhs promise resolves to false": [
|
||||
' (hs-cleanup!)',
|
||||
' (assert= (eval-hs "promiseValueBackIn(false, 0) or \\"foo\\"") "foo")',
|
||||
],
|
||||
# F7: or short-circuits when Promise.resolve(true) unwraps to true
|
||||
"or short-circuits when lhs promise resolves to true": [
|
||||
' (hs-cleanup!)',
|
||||
' (assert (eval-hs "promiseValueBackIn(true, 0) or \\"foo\\""))',
|
||||
],
|
||||
# F8: arithmetic with async arg — promiseAnIntIn(10) unwraps to 42
|
||||
"can use mixed expressions": [
|
||||
' (hs-cleanup!)',
|
||||
' (assert= (eval-hs "1 + promiseAnIntIn(10)") 43)',
|
||||
],
|
||||
# F9: fetch as html returns a DocumentFragment with parsed children; childElementCount > 0
|
||||
"can do a simple fetch w/ html": [
|
||||
' (hs-cleanup!)',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-set-attr _el "_" "on click fetch /test as html then set my innerHTML to result.childElementCount")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' (hs-activate! _el)',
|
||||
' (dom-dispatch _el "click" nil)',
|
||||
' (assert= (dom-text-content _el) "1"))',
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user