HS: intersection observer mock + on intersection (+3 tests)

Applied from worktree-agent-ad6e17cbc4ea0c94b (commit 0a0fe314)
with manual re-apply onto post-cluster-26 HEAD:

- Parser: parse-on-feat collects `having margin X threshold Y`
  clauses between `from X` and the body; packs them into a
  `:having {"margin" M "threshold" T}` dict on the parts list.
- Compiler: scan-on threads a new `having-info` parameter through
  all recursions; when event-name is "intersection", wraps the
  hs-on call with `(do on-call (hs-on-intersection-attach! target
  margin threshold))`.
- Runtime: hs-on-intersection-attach! constructs an
  IntersectionObserver with {rootMargin, threshold} options and a
  callback that dispatches an "intersection" DOM event carrying
  {intersecting, entry} detail.
- Runner: HsIntersectionObserver mock fires the callback
  synchronously on observe() with isIntersecting=true so handlers
  run during activation; ignores margin/threshold (tests assert
  only that the handler fires).

Suite hs-upstream-on: 33/70 -> 36/70 (on intersection: 0/3 -> 3/3).
Smoke 0-195 unchanged at 165/195.
This commit is contained in:
2026-04-24 10:44:01 +00:00
parent cee9ae7f22
commit 0c31dd2735
7 changed files with 350 additions and 213 deletions

View File

@@ -354,7 +354,26 @@ class HsResizeObserver {
}
}
globalThis.ResizeObserver=HsResizeObserver; globalThis.ResizeObserverEntry=class{};
globalThis.IntersectionObserver=class{observe(){}disconnect(){}};
// HsIntersectionObserver — cluster-27 intersection mock. Fires the callback
// synchronously on observe() with isIntersecting=true so `on intersection`
// handlers run during activation. `margin`/`threshold` options are parsed
// but ignored (tests only assert the handler fires).
class HsIntersectionObserver {
constructor(cb, opts) { this._cb = cb; this._opts = opts || {}; this._els = new Set(); }
observe(el) {
if (!el) return;
this._els.add(el);
const entry = { target: el, isIntersecting: true, intersectionRatio: 1,
boundingClientRect: (el.getBoundingClientRect && el.getBoundingClientRect()) || {},
intersectionRect: {}, rootBounds: null, time: 0 };
try { this._cb([entry], this); } catch (e) {}
}
unobserve(el) { if (el) this._els.delete(el); }
disconnect() { this._els.clear(); }
takeRecords() { return []; }
}
globalThis.IntersectionObserver = HsIntersectionObserver;
globalThis.IntersectionObserverEntry = class {};
globalThis.navigator={userAgent:'node'}; globalThis.location={href:'http://localhost/',pathname:'/',search:'',hash:''};
globalThis.history={pushState(){},replaceState(){},back(){},forward(){}};
globalThis.getSelection=()=>({toString:()=>(globalThis.__test_selection||'')});