diff --git a/lib/hyperscript/runtime.sx b/lib/hyperscript/runtime.sx index 194a5c09..7704e588 100644 --- a/lib/hyperscript/runtime.sx +++ b/lib/hyperscript/runtime.sx @@ -68,13 +68,18 @@ hs-on (fn (target event-name handler) - (let - ((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"))))))) + (when + (not (nil? target)) (let - ((unlisten (dom-listen target event-name wrapped)) - (prev (or (dom-get-data target "hs-unlisteners") (list)))) - (dom-set-data target "hs-unlisteners" (append prev (list unlisten))) - unlisten)))) + ((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"))))))) + (let + ((unlisten (dom-listen target event-name wrapped)) + (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. (define diff --git a/spec/tests/test-hyperscript-behavioral.sx b/spec/tests/test-hyperscript-behavioral.sx index a1054a03..bd4aff24 100644 --- a/spec/tests/test-hyperscript-behavioral.sx +++ b/spec/tests/test-hyperscript-behavioral.sx @@ -9472,7 +9472,14 @@ (hs-activate! _el-div) )) (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" (hs-cleanup!) (let ((_el-div (dom-create-element "div"))) diff --git a/tests/hs-run-filtered.js b/tests/hs-run-filtered.js index 4ef885bb..99716053 100755 --- a/tests/hs-run-filtered.js +++ b/tests/hs-run-filtered.js @@ -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-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-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-iter?',([obj])=>obj!=null&&typeof obj[Symbol.iterator]==='function'); K.registerNative('host-to-list',([obj])=>{try{return[...obj];}catch(e){return[];}}); diff --git a/tests/playwright/generate-sx-tests.py b/tests/playwright/generate-sx-tests.py index 6e7a16c4..f603ac13 100644 --- a/tests/playwright/generate-sx-tests.py +++ b/tests/playwright/generate-sx-tests.py @@ -109,10 +109,8 @@ SKIP_TEST_NAMES = { "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 js functions", "can catch exceptions thrown in hyperscript functions", - "can ignore when target doesn't exist", - "can ignore when target doesn\\'t exist", + "can catch exceptions thrown in js functions", "can handle an or after a from clause", # upstream 'fetch' category — real DocumentFragment semantics (`its childElementCount` # after `as html`) not exercisable with our DOM mock. @@ -202,6 +200,16 @@ MANUAL_TEST_BODIES = { "basic classRef works w no match": [ ' (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 "can call functions": [ ' (hs-cleanup!)',