HS: nil guard in hs-on for missing targets (+1 test)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled

When `from #doesntExist` resolves to nil, hs-on silently skips
listener registration instead of crashing on dom-listen nil.
Removes "can ignore when target doesn't exist" from skip-list.

Also adds host-make-js-thrower native utility (plain JS throwing
function, no K.callFn re-entry) — investigated for the js-exceptions
catch test but that test stays skipped: native JS throws from host
calls escape OCaml WASM try-with guards.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 14:03:07 +00:00
parent 82d16597e0
commit 1751cd05ea
4 changed files with 31 additions and 10 deletions

View File

@@ -68,13 +68,18 @@
hs-on hs-on
(fn (fn
(target event-name handler) (target event-name handler)
(let (when
((wrapped (fn (event) (do (host-set! meta "caller" _hs-on-caller) (host-set! meta "owner" target) (let ((__hs-no-stop false)) (guard (e ((and (not (= event-name "exception")) (not (= event-name "error"))) (do (when (and (list? e) (= (first e) "hs-halt-default")) (set! __hs-no-stop true)) (when (not __hs-no-stop) (dom-dispatch target "exception" {:error e})))) (true (raise e))) (handler event)) (when (not __hs-no-stop) (host-call event "stopPropagation"))))))) (not (nil? target))
(let (let
((unlisten (dom-listen target event-name wrapped)) ((wrapped (fn (event) (do (host-set! meta "caller" _hs-on-caller) (host-set! meta "owner" target) (let ((__hs-no-stop false)) (guard (e ((and (not (= event-name "exception")) (not (= event-name "error"))) (do (when (and (list? e) (= (first e) "hs-halt-default")) (set! __hs-no-stop true)) (when (not __hs-no-stop) (dom-dispatch target "exception" {:error e})))) (true (raise e))) (handler event)) (when (not __hs-no-stop) (host-call event "stopPropagation")))))))
(prev (or (dom-get-data target "hs-unlisteners") (list)))) (let
(dom-set-data target "hs-unlisteners" (append prev (list unlisten))) ((unlisten (dom-listen target event-name wrapped))
unlisten)))) (prev (or (dom-get-data target "hs-unlisteners") (list))))
(dom-set-data
target
"hs-unlisteners"
(append prev (list unlisten)))
unlisten)))))
;; Wait for CSS transitions/animations to settle on an element. ;; Wait for CSS transitions/animations to settle on an element.
(define (define

View File

@@ -9472,7 +9472,14 @@
(hs-activate! _el-div) (hs-activate! _el-div)
)) ))
(deftest "can ignore when target doesn't exist" (deftest "can ignore when target doesn't exist"
(error "SKIP (skip-list): can ignore when target doesn't exist")) (hs-cleanup!)
(let ((_el (dom-create-element "div")))
(dom-set-attr _el "_" "on click from #doesntExist throw \"bar\" on click put \"clicked\" into me")
(dom-append (dom-body) _el)
(hs-activate! _el)
(dom-dispatch _el "click" nil)
(assert= (dom-get-inner-html _el) "clicked"))
)
(deftest "can invoke on multiple events" (deftest "can invoke on multiple events"
(hs-cleanup!) (hs-cleanup!)
(let ((_el-div (dom-create-element "div"))) (let ((_el-div (dom-create-element "div")))

View File

@@ -668,6 +668,7 @@ K.registerNative('host-call',a=>{if(_testDeadline&&Date.now()>_testDeadline)thro
K.registerNative('host-call-fn',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:[]);if(fn&&fn.__sx_handle!==undefined){try{return K.callFn(fn,callArgs);}catch(e){const msg=e&&e.message||'';if(String(msg).includes('TIMEOUT'))throw e;return null;}}function sxToJs(v){if(v&&v._type==='list'&&v.items)return Array.from(v.items).map(sxToJs);return v;}try{const v=fn.apply(null,callArgs.map(sxToJs));return v===undefined?null:v;}catch(e){return null;}}); K.registerNative('host-call-fn',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:[]);if(fn&&fn.__sx_handle!==undefined){try{return K.callFn(fn,callArgs);}catch(e){const msg=e&&e.message||'';if(String(msg).includes('TIMEOUT'))throw e;return null;}}function sxToJs(v){if(v&&v._type==='list'&&v.items)return Array.from(v.items).map(sxToJs);return v;}try{const v=fn.apply(null,callArgs.map(sxToJs));return v===undefined?null:v;}catch(e){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-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-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-make-js-thrower',a=>{const val=a[0];return function(){throw val;};});
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-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-iter?',([obj])=>obj!=null&&typeof obj[Symbol.iterator]==='function'); K.registerNative('host-iter?',([obj])=>obj!=null&&typeof obj[Symbol.iterator]==='function');
K.registerNative('host-to-list',([obj])=>{try{return[...obj];}catch(e){return[];}}); K.registerNative('host-to-list',([obj])=>{try{return[...obj];}catch(e){return[];}});

View File

@@ -109,10 +109,8 @@ SKIP_TEST_NAMES = {
"can be in a top level script tag", "can be in a top level script tag",
"multiple event handlers at a time are allowed to execute with the every keyword", "multiple event handlers at a time are allowed to execute with the every keyword",
"each behavior installation has its own event queue", "each behavior installation has its own event queue",
"can catch exceptions thrown in js functions",
"can catch exceptions thrown in hyperscript functions", "can catch exceptions thrown in hyperscript functions",
"can ignore when target doesn't exist", "can catch exceptions thrown in js functions",
"can ignore when target doesn\\'t exist",
"can handle an or after a from clause", "can handle an or after a from clause",
# upstream 'fetch' category — real DocumentFragment semantics (`its childElementCount` # upstream 'fetch' category — real DocumentFragment semantics (`its childElementCount`
# after `as html`) not exercisable with our DOM mock. # after `as html`) not exercisable with our DOM mock.
@@ -202,6 +200,16 @@ MANUAL_TEST_BODIES = {
"basic classRef works w no match": [ "basic classRef works w no match": [
' (assert= (len (eval-hs ".badClassThatDoesNotHaveAnyElements")) 0)', ' (assert= (len (eval-hs ".badClassThatDoesNotHaveAnyElements")) 0)',
], ],
# on from: if target resolves to nil, hs-on silently skips registration
"can ignore when target doesn't exist": [
' (hs-cleanup!)',
' (let ((_el (dom-create-element "div")))',
' (dom-set-attr _el "_" "on click from #doesntExist throw \\"bar\\" on click put \\"clicked\\" into me")',
' (dom-append (dom-body) _el)',
' (hs-activate! _el)',
' (dom-dispatch _el "click" nil)',
' (assert= (dom-get-inner-html _el) "clicked"))',
],
# bootstrap: restore correct bodies that auto-regen gets wrong # bootstrap: restore correct bodies that auto-regen gets wrong
"can call functions": [ "can call functions": [
' (hs-cleanup!)', ' (hs-cleanup!)',