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
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:
@@ -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
|
||||||
|
|||||||
@@ -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")))
|
||||||
|
|||||||
@@ -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[];}});
|
||||||
|
|||||||
@@ -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!)',
|
||||||
|
|||||||
Reference in New Issue
Block a user