HS: MutationObserver mock + on mutation dispatch (+7 tests)

Parser: parse-on-feat now consumes `of FILTER` after `mutation` event-name,
where FILTER is `attributes`/`childList`/`characterData` ident or `@a [or @b]*`
attr-token chain. Emits :of-filter dict on parts. Compiler: scan-on threads
of-filter-info; mutation event-name emits `(do (hs-on …) (hs-on-mutation-attach!
TARGET MODE ATTRS))`. Runtime: hs-on-mutation-attach! constructs a real
MutationObserver with config matched to filter and dispatches "mutation" event
with records detail. Runner: HsMutationObserver mock with global registry;
prototype hooks on El.setAttribute/appendChild/removeChild/_setInnerHTML fire
matching observers synchronously, with __hsMutationActive guard preventing
recursion. Generator: dropped 7 mutation tests from skip-list, added
evaluate(setAttribute) and evaluate(appendChild) body patterns.

hs-upstream-on: 36/70 → 43/70. Smoke 0-195 unchanged at 170/195.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 11:52:54 +00:00
parent 1340284bc8
commit 13e0254261
9 changed files with 560 additions and 261 deletions

View File

@@ -164,7 +164,8 @@
every?
catch-info
finally-info
having-info)
having-info
of-filter-info)
(cond
((<= (len items) 1)
(let
@@ -185,23 +186,44 @@
((handler (let ((uses-the-result? (fn (expr) (cond ((= expr (quote the-result)) true) ((list? expr) (some (fn (x) (uses-the-result? x)) expr)) (true false))))) (list (quote fn) (list (quote event)) (if (uses-the-result? wrapped-body) (list (quote let) (list (list (quote the-result) nil)) wrapped-body) wrapped-body)))))
(let
((on-call (if every? (list (quote hs-on-every) target event-name handler) (list (quote hs-on) target event-name handler))))
(if
(= event-name "intersection")
(list
(quote do)
on-call
(cond
((= event-name "mutation")
(list
(quote hs-on-intersection-attach!)
target
(if
having-info
(get having-info "margin")
nil)
(if
having-info
(get having-info "threshold")
nil)))
on-call)))))))))))
(quote do)
on-call
(list
(quote hs-on-mutation-attach!)
target
(if
of-filter-info
(get of-filter-info "type")
"any")
(if
of-filter-info
(let
((a (get of-filter-info "attrs")))
(if
a
(cons (quote list) a)
nil))
nil))))
((= event-name "intersection")
(list
(quote do)
on-call
(list
(quote
hs-on-intersection-attach!)
target
(if
having-info
(get having-info "margin")
nil)
(if
having-info
(get having-info "threshold")
nil))))
(true on-call))))))))))))
((= (first items) :from)
(scan-on
(rest (rest items))
@@ -210,7 +232,8 @@
every?
catch-info
finally-info
having-info))
having-info
of-filter-info))
((= (first items) :filter)
(scan-on
(rest (rest items))
@@ -219,7 +242,8 @@
every?
catch-info
finally-info
having-info))
having-info
of-filter-info))
((= (first items) :every)
(scan-on
(rest (rest items))
@@ -228,7 +252,8 @@
true
catch-info
finally-info
having-info))
having-info
of-filter-info))
((= (first items) :catch)
(scan-on
(rest (rest items))
@@ -237,7 +262,8 @@
every?
(nth items 1)
finally-info
having-info))
having-info
of-filter-info))
((= (first items) :finally)
(scan-on
(rest (rest items))
@@ -246,7 +272,8 @@
every?
catch-info
(nth items 1)
having-info))
having-info
of-filter-info))
((= (first items) :having)
(scan-on
(rest (rest items))
@@ -255,6 +282,17 @@
every?
catch-info
finally-info
(nth items 1)
of-filter-info))
((= (first items) :of-filter)
(scan-on
(rest (rest items))
source
filter
every?
catch-info
finally-info
having-info
(nth items 1)))
(true
(scan-on
@@ -264,8 +302,9 @@
every?
catch-info
finally-info
having-info)))))
(scan-on (rest parts) nil nil false nil nil nil)))))
having-info
of-filter-info)))))
(scan-on (rest parts) nil nil false nil nil nil nil)))))
(define
emit-send
(fn