diff --git a/lib/hyperscript/integration.sx b/lib/hyperscript/integration.sx index 56cafe86..43132359 100644 --- a/lib/hyperscript/integration.sx +++ b/lib/hyperscript/integration.sx @@ -87,12 +87,11 @@ (not (dom-get-data script "hs-script-loaded")) (let ((src (host-get script "innerHTML"))) - (when - (and src (not (= src ""))) - (guard - (_e (true nil)) - (eval-expr-cek (hs-to-sx-from-source src))) - (dom-set-data script "hs-script-loaded" true))))) + (guard + (_e (true nil)) + (let + ((handler (eval-expr-cek (hs-to-sx-from-source src)))) + (handler (dom-body))))))) (hs-query-all "script[type=text/hyperscript]")))) ;; ── Boot: scan entire document ────────────────────────────────── @@ -131,8 +130,13 @@ (dom-set-data el "hs-script" src) (dom-set-data el "hs-active" true) (dom-set-attr el "data-hyperscript-powered" "true") + (host-set! (host-global "window") "__hs_current_me" el) (guard - (_e (true nil)) + (_e + (true + (do + (dom-dispatch el "hyperscript:parse-error" {:errors (list _e)}) + nil))) (let ((handler (hs-handler src))) (let @@ -146,6 +150,7 @@ safe-handler (hs-query-all (or comp-name ""))) (safe-handler el)))))) + (host-set! (host-global "window") "__hs_current_me" nil) (dom-dispatch el "hyperscript:after:init" nil))))))) (define diff --git a/lib/hyperscript/parser.sx b/lib/hyperscript/parser.sx index 676b0887..3cc8dacc 100644 --- a/lib/hyperscript/parser.sx +++ b/lib/hyperscript/parser.sx @@ -3015,7 +3015,7 @@ (fn () (let - ((every? (match-kw "every")) (first? (match-kw "first"))) + ((first? (match-kw "first"))) (let ((event-name (parse-compound-event-name))) (let @@ -3079,42 +3079,44 @@ (= (tp-val) "queue")) (do (adv!) (adv!))) (let - ((having (if (or h-margin h-threshold) (dict "margin" h-margin "threshold" h-threshold) nil))) + ((every? (match-kw "every"))) (let - ((body (parse-cmd-list))) + ((having (if (or h-margin h-threshold) (dict "margin" h-margin "threshold" h-threshold) nil))) (let - ((catch-clause (if (match-kw "catch") (let ((var (let ((v (tp-val))) (adv!) v)) (handler (parse-cmd-list))) (list var handler)) nil)) - (finally-clause - (if - (match-kw "finally") - (parse-cmd-list) - nil))) - (match-kw "end") + ((body (parse-cmd-list))) (let - ((parts (list (quote on) event-name))) + ((catch-clause (if (match-kw "catch") (let ((var (let ((v (tp-val))) (adv!) v)) (handler (parse-cmd-list))) (list var handler)) nil)) + (finally-clause + (if + (match-kw "finally") + (parse-cmd-list) + nil))) + (match-kw "end") (let - ((parts (if every? (append parts (list :every true)) parts))) + ((parts (list (quote on) event-name))) (let - ((parts (if flt (append parts (list :filter flt)) parts))) + ((parts (if every? (append parts (list :every true)) parts))) (let - ((parts (if elsewhere? (append parts (list :elsewhere true)) parts))) + ((parts (if flt (append parts (list :filter flt)) parts))) (let - ((parts (if source (append parts (list :from source)) parts))) + ((parts (if elsewhere? (append parts (list :elsewhere true)) parts))) (let - ((parts (if (> (len or-sources) 0) (append parts (list :or-sources or-sources)) parts))) + ((parts (if source (append parts (list :from source)) parts))) (let - ((parts (if count-filter (append parts (list :count-filter count-filter)) parts))) + ((parts (if (> (len or-sources) 0) (append parts (list :or-sources or-sources)) parts))) (let - ((parts (if of-filter (append parts (list :of-filter of-filter)) parts))) + ((parts (if count-filter (append parts (list :count-filter count-filter)) parts))) (let - ((parts (if having (append parts (list :having having)) parts))) + ((parts (if of-filter (append parts (list :of-filter of-filter)) parts))) (let - ((parts (if catch-clause (append parts (list :catch catch-clause)) parts))) + ((parts (if having (append parts (list :having having)) parts))) (let - ((parts (if finally-clause (append parts (list :finally finally-clause)) parts))) + ((parts (if catch-clause (append parts (list :catch catch-clause)) parts))) (let - ((parts (append parts (list (if (> (len event-vars) 0) (cons (quote do) (append (map (fn (nm) (list (quote ref) nm)) event-vars) (if (and (list? body) (= (first body) (quote do))) (rest body) (list body)))) body))))) - parts))))))))))))))))))))))))) + ((parts (if finally-clause (append parts (list :finally finally-clause)) parts))) + (let + ((parts (append parts (list (if (> (len event-vars) 0) (cons (quote do) (append (map (fn (nm) (list (quote ref) nm)) event-vars) (if (and (list? body) (= (first body) (quote do))) (rest body) (list body)))) body))))) + parts)))))))))))))))))))))))))) (define parse-init-feat (fn diff --git a/shared/static/wasm/sx/hs-integration.sx b/shared/static/wasm/sx/hs-integration.sx index a1a45547..29c20c70 100644 --- a/shared/static/wasm/sx/hs-integration.sx +++ b/shared/static/wasm/sx/hs-integration.sx @@ -115,8 +115,13 @@ (dom-set-data el "hs-script" src) (dom-set-data el "hs-active" true) (dom-set-attr el "data-hyperscript-powered" "true") + (host-set! (host-global "window") "__hs_current_me" el) (guard - (_e (true nil)) + (_e + (true + (do + (dom-dispatch el "hyperscript:parse-error" {:detail {:errors (list _e)}}) + nil))) (let ((handler (hs-handler src))) (let @@ -130,6 +135,7 @@ safe-handler (hs-query-all (or comp-name ""))) (safe-handler el)))))) + (host-set! (host-global "window") "__hs_current_me" nil) (dom-dispatch el "hyperscript:after:init" nil))))))) ;; ── Boot subtree: for dynamic content ─────────────────────────── diff --git a/spec/tests/test-hyperscript-behavioral.sx b/spec/tests/test-hyperscript-behavioral.sx index 8af8016e..99dd9769 100644 --- a/spec/tests/test-hyperscript-behavioral.sx +++ b/spec/tests/test-hyperscript-behavioral.sx @@ -1891,7 +1891,20 @@ (assert= (dom-text-content (dom-query-by-id "d2")) "clicked") )) (deftest "fires hyperscript:parse-error event with all errors" - (error "SKIP (untranslated): 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)))) + ) (deftest "parse error at EOF on trailing newline does not crash" (let ((caught nil)) (guard (_e (true (set! caught (str _e)))) @@ -5712,17 +5725,34 @@ (assert= (eval-hs-locals "getObj().greet()" (list (list (quote getObj) (fn () {:greet (fn () "hi")})))) "hi") ) (deftest "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")) ) (deftest "can invoke function on object w/ async arg" - (error "SKIP (untranslated): 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))) + ) (deftest "can invoke function on object w/ async root & arg" - (error "SKIP (untranslated): 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))))) + ) (deftest "can invoke global function" (assert= (eval-hs-locals "identity(\"foo\")" (list (list (quote identity) (fn (x) x)))) "foo") ) (deftest "can invoke global function w/ async arg" - (error "SKIP (untranslated): can invoke global function w/ async arg")) + (hs-cleanup!) + (assert= (eval-hs "identity(promiseAnIntIn(10))") 42) + ) (deftest "can pass an array literal as an argument" (assert= (eval-hs-locals "sum([1, 2, 3, 4])" (list (list (quote sum) (fn (arr) (reduce (fn (a b) (+ a b)) 0 arr))))) 10) ) @@ -5869,7 +5899,9 @@ ;; ── expressions/logicalOperator (10 tests) ── (defsuite "hs-upstream-expressions/logicalOperator" (deftest "and short-circuits when lhs promise resolves to false" - (error "SKIP (untranslated): and short-circuits when lhs promise resolves to false")) + (hs-cleanup!) + (assert= (eval-hs "promiseValueBackIn(false, 0) and \"foo\"") false) + ) (deftest "and works" (assert= (eval-hs "true and false") false) ) @@ -5877,9 +5909,13 @@ (assert= (eval-hs "true and true and false") false) ) (deftest "or evaluates rhs when lhs promise resolves to false" - (error "SKIP (untranslated): or evaluates rhs when lhs promise resolves to false")) + (hs-cleanup!) + (assert= (eval-hs "promiseValueBackIn(false, 0) or \"foo\"") "foo") + ) (deftest "or short-circuits when lhs promise resolves to true" - (error "SKIP (untranslated): or short-circuits when lhs promise resolves to true")) + (hs-cleanup!) + (assert (eval-hs "promiseValueBackIn(true, 0) or \"foo\"")) + ) (deftest "or works" (assert= (eval-hs "true or false") true) ) @@ -5932,7 +5968,9 @@ (assert= (eval-hs "[1] + [2] + [3]") (list 1 2 3)) ) (deftest "can use mixed expressions" - (error "SKIP (untranslated): can use mixed expressions")) + (hs-cleanup!) + (assert= (eval-hs "1 + promiseAnIntIn(10)") 43) + ) (deftest "division works" (assert= (eval-hs "1 / 2") 0.5) ) @@ -7702,7 +7740,14 @@ (assert= (dom-text-content _el-div) "yay") )) (deftest "can do a simple fetch w/ html" - (error "SKIP (skip-list): 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")) + ) (deftest "can do a simple fetch w/ json" (hs-cleanup!) (let ((_el-div (dom-create-element "div"))) @@ -9388,7 +9433,15 @@ (hs-activate! _el-button) )) (deftest "can be in a top level script tag" - (error "SKIP (skip-list): 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")) + ) (deftest "can catch async top-level exceptions" (hs-cleanup!) (let ((_el-button (dom-create-element "button"))) @@ -9397,9 +9450,23 @@ (hs-activate! _el-button) )) (deftest "can catch exceptions thrown in hyperscript functions" - (error "SKIP (skip-list): 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")) + ) (deftest "can catch exceptions thrown in js functions" - (error "SKIP (skip-list): 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")) + ) (deftest "can catch top-level exceptions" (hs-cleanup!) (let ((_el-button (dom-create-element "button"))) @@ -9737,7 +9804,28 @@ (hs-activate! _el-d) )) (deftest "each behavior installation has its own event queue" - (error "SKIP (skip-list): 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")) + ) (deftest "exceptions in catch block don't kill the event queue" (hs-cleanup!) (let ((_el-button (dom-create-element "button"))) @@ -9792,11 +9880,43 @@ (hs-activate! _el-d1) )) (deftest "listeners on other elements are removed when the registering element is removed" - (error "SKIP (skip-list): 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")) + ) (deftest "listeners on self are not removed when the element is removed" - (error "SKIP (skip-list): 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")) + ) (deftest "multiple event handlers at a time are allowed to execute with the every keyword" - (error "SKIP (skip-list): 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)) + ) (deftest "on first click fires only once" (hs-cleanup!) (let ((_el-div (dom-create-element "div"))) @@ -14123,7 +14243,17 @@ end") (hs-activate! _el-span) )) (deftest "attribute observers are persistent (not recreated on re-run)" - (error "SKIP (untranslated): 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")) + ) (deftest "auto-tracks compound expressions" (hs-cleanup!) (let ((_el-div (dom-create-element "div"))) diff --git a/tests/hs-run-filtered.js b/tests/hs-run-filtered.js index 99716053..9fbe2011 100755 --- a/tests/hs-run-filtered.js +++ b/tests/hs-run-filtered.js @@ -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: "
hello
", html: "hello
", 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]; diff --git a/tests/playwright/generate-sx-tests.py b/tests/playwright/generate-sx-tests.py index 58db4765..36c7eb6c 100644 --- a/tests/playwright/generate-sx-tests.py +++ b/tests/playwright/generate-sx-tests.py @@ -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"))', + ], }