Skip to main content

rdom_core/
dispatch.rs

1//! Event listener storage + `dispatch_event` (capture → target → bubble).
2//!
3//! Listeners are held in a side `HashMap<NodeId, Vec<Listener<Ext>>>` on
4//! `Dom` — nodes without listeners don't pay any per-element cost. A
5//! listener handle (`ListenerId`) is returned from `add_event_listener`
6//! so callers can remove the exact registration later.
7//!
8//! Dispatch algorithm (mirrors the DOM spec):
9//!
10//! 1. Compute the ancestor path from target up to root (inclusive).
11//! 2. **Capture phase** — iterate path root→target (excluding target); on
12//!    each node fire listeners whose `capture == true`.
13//! 3. **Target phase** — on target fire every listener (both capture and
14//!    bubble variants).
15//! 4. **Bubble phase** — if `event.bubbles`, iterate target→root
16//!    (excluding target); on each node fire listeners whose `capture == false`.
17//!
18//! `stop_propagation` cuts at node-boundaries. `stop_immediate_propagation`
19//! cuts mid-node (no further listeners on the current node run).
20//!
21//! Handlers are `FnMut(&mut EventCtx<'_, Ext>)`. They receive a mutable
22//! reference to the Dom via the context — mutations during dispatch are
23//! allowed; the dispatcher captures the ancestor path up-front so removed
24//! nodes mid-flight don't break iteration. Listeners added during a
25//! handler fire on *subsequent* dispatches, not the current one.
26
27use std::collections::HashMap;
28
29use crate::abort::AbortSignal;
30use crate::dom::Dom;
31use crate::error::{DomError, Result};
32use crate::event::{Event, EventPhase};
33use crate::node_id::NodeId;
34
35/// Handle returned from `add_event_listener` — pass to `remove_event_listener`
36/// to detach this specific registration.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub struct ListenerId {
39    pub node: NodeId,
40    pub seq: u32,
41}
42
43/// A single registration.
44///
45/// `handler` is `Option` so the dispatcher can `take` it out (handing
46/// `&mut Dom` to the handler) and put it back after the call. `None`
47/// during a call means "currently firing".
48pub(crate) struct Listener<Ext: 'static> {
49    seq: u32,
50    event_type: String,
51    capture: bool,
52    once: bool,
53    /// If `Some`, the listener is dropped + skipped on the next
54    /// dispatch visit once `signal.is_aborted()` returns true.
55    signal: Option<AbortSignal>,
56    handler: Option<EventHandler<Ext>>,
57}
58
59/// Boxed event handler stored on a [`Listener`].
60pub(crate) type EventHandler<Ext> = Box<dyn FnMut(&mut EventCtx<'_, Ext>) + 'static>;
61
62/// Context passed to a handler: mutable Event + mutable Dom.
63pub struct EventCtx<'a, Ext: 'static> {
64    pub event: &'a mut Event,
65    pub dom: &'a mut Dom<Ext>,
66}
67
68impl<Ext: 'static> EventCtx<'_, Ext> {
69    /// Ask the rendering host to repaint after this event completes.
70    ///
71    /// Mutations made through `ctx.dom` are already seen by the host's
72    /// mutation tracker. Use this for the case the tracker *can't* see:
73    /// a handler that changed state living **outside** the DOM that the
74    /// next paint will read — e.g. a `<canvas>` whose paint callback
75    /// reads external application state. Without this, such a change
76    /// produces no repaint until something else dirties the tree.
77    ///
78    /// Sets a flag on the event ([`Event::redraw_requested`]) that the
79    /// host harvests after dispatch; `rdom-core` itself does nothing
80    /// with it.
81    pub fn request_redraw(&mut self) {
82        self.event.redraw_requested = true;
83    }
84}
85
86/// Which activation step the Dom's hook is being asked to run.
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum ActivationPhase {
89    /// Before any listener: the legacy-pre-activation step (a checkbox
90    /// flips its state here so click listeners see the new value).
91    Pre,
92    /// After dispatch. `canceled` is the event's canceled flag: run the
93    /// legacy-canceled-activation step (revert) when true, the
94    /// activation behavior proper (fire `input` / `change`, submit,
95    /// navigate) when false.
96    Post { canceled: bool },
97}
98
99/// The Dom's activation-behavior hook (DOM §2.9 steps 5.5 and 11). One
100/// per `Dom`; it receives every dispatched event and decides by target
101/// and event type whether the target has activation behavior. Runs
102/// regardless of `stopPropagation()`.
103pub type ActivationHook<Ext> =
104    Box<dyn FnMut(&mut Dom<Ext>, NodeId, &Event, ActivationPhase) + 'static>;
105
106/// The DOM "dispatch flag" of one [`Dom::dispatch_event`] call: set on
107/// construction, cleared on drop, so no return path — an early error,
108/// a listener panic unwinding through — leaves an `Event` flagged as in
109/// flight (`P7G-DISPATCH-FLAG-1`).
110struct DispatchFlag<'e>(&'e mut Event);
111
112impl<'e> DispatchFlag<'e> {
113    fn set(event: &'e mut Event) -> Self {
114        event.dispatching = true;
115        Self(event)
116    }
117}
118
119impl Drop for DispatchFlag<'_> {
120    fn drop(&mut self) {
121        self.0.dispatching = false;
122    }
123}
124
125impl std::ops::Deref for DispatchFlag<'_> {
126    type Target = Event;
127    fn deref(&self) -> &Event {
128        self.0
129    }
130}
131
132impl std::ops::DerefMut for DispatchFlag<'_> {
133    fn deref_mut(&mut self) -> &mut Event {
134        self.0
135    }
136}
137
138/// Storage for the hook with a `Debug` impl (the closure has none).
139pub(crate) struct ActivationSlot<Ext: 'static>(pub(crate) Option<ActivationHook<Ext>>);
140
141impl<Ext: 'static> std::fmt::Debug for ActivationSlot<Ext> {
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        f.write_str(if self.0.is_some() {
144            "ActivationSlot(Some(hook))"
145        } else {
146            "ActivationSlot(None)"
147        })
148    }
149}
150/// Options for `add_event_listener` — matches the DOM spec object.
151///
152/// Not `Copy` because `signal: Option<AbortSignal>` holds an `Rc`.
153/// Cloning is cheap (refcount bump).
154#[derive(Debug, Clone, Default)]
155#[non_exhaustive]
156pub struct ListenerOptions {
157    /// Fire during the capture phase instead of the bubble phase.
158    pub capture: bool,
159    /// Remove the listener after it fires once.
160    pub once: bool,
161    /// If `Some`, the listener auto-removes once the signal is
162    /// aborted. See [`AbortController`] / [`AbortSignal`] for the
163    /// lifetime-management pattern.
164    ///
165    /// [`AbortController`]: crate::AbortController
166    /// [`AbortSignal`]: crate::AbortSignal
167    pub signal: Option<AbortSignal>,
168}
169
170impl ListenerOptions {
171    pub fn capture() -> Self {
172        Self {
173            capture: true,
174            ..Default::default()
175        }
176    }
177    pub fn once() -> Self {
178        Self {
179            once: true,
180            ..Default::default()
181        }
182    }
183    /// Builder-style: set `capture`.
184    pub fn with_capture(mut self, capture: bool) -> Self {
185        self.capture = capture;
186        self
187    }
188    /// Builder-style: set `once`.
189    pub fn with_once(mut self, once: bool) -> Self {
190        self.once = once;
191        self
192    }
193    /// Builder-style: attach an abort signal. When the signal fires,
194    /// this listener is removed on the next dispatch visit.
195    pub fn with_signal(mut self, signal: AbortSignal) -> Self {
196        self.signal = Some(signal);
197        self
198    }
199}
200
201/// Per-Dom listener storage. One entry per node that has at least one
202/// registered listener.
203#[derive(Default)]
204pub(crate) struct ListenerStore<Ext: 'static> {
205    by_node: HashMap<NodeId, Vec<Listener<Ext>>>,
206    /// Monotonic sequence counter for unique ListenerIds.
207    next_seq: u32,
208}
209
210impl<Ext: 'static> ListenerStore<Ext> {
211    fn next(&mut self) -> u32 {
212        let n = self.next_seq;
213        self.next_seq = self.next_seq.wrapping_add(1);
214        n
215    }
216}
217
218impl<Ext: 'static> std::fmt::Debug for ListenerStore<Ext> {
219    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220        f.debug_struct("ListenerStore")
221            .field("nodes_with_listeners", &self.by_node.len())
222            .field("next_seq", &self.next_seq)
223            .finish()
224    }
225}
226
227// Listeners don't Clone — cloning a Dom that owns closures is nonsensical.
228// Provide a manual `Clone` impl on Dom that resets listeners to empty (see
229// dom.rs). For now ListenerStore is `Default` but not `Clone`.
230
231impl<Ext> Dom<Ext> {
232    /// Register a listener on `node` for events of type `event_type`.
233    /// Returns a `ListenerId` that can be passed to `remove_event_listener`.
234    pub fn add_event_listener(
235        &mut self,
236        node: NodeId,
237        event_type: impl Into<String>,
238        options: ListenerOptions,
239        handler: impl FnMut(&mut EventCtx<'_, Ext>) + 'static,
240    ) -> Result<ListenerId> {
241        self.node_or_err(node)?;
242        let seq = self.listeners.next();
243        let listener = Listener {
244            seq,
245            event_type: event_type.into(),
246            capture: options.capture,
247            once: options.once,
248            signal: options.signal,
249            handler: Some(Box::new(handler)),
250        };
251        // Registering with an already-aborted signal still succeeds —
252        // the listener goes into storage and gets dropped on the
253        // first dispatch visit (never fires). Matches browser
254        // behavior; tests exercise this path explicitly.
255        self.listeners
256            .by_node
257            .entry(node)
258            .or_default()
259            .push(listener);
260        Ok(ListenerId { node, seq })
261    }
262
263    /// Remove a previously-registered listener. Returns `true` if the
264    /// listener existed and was removed; `false` if it was already gone
265    /// (e.g. a `once` handler that already fired).
266    pub fn remove_event_listener(&mut self, handle: ListenerId) -> bool {
267        let Some(vec) = self.listeners.by_node.get_mut(&handle.node) else {
268            return false;
269        };
270        let before = vec.len();
271        vec.retain(|l| l.seq != handle.seq);
272        let removed = vec.len() < before;
273        if vec.is_empty() {
274            self.listeners.by_node.remove(&handle.node);
275        }
276        removed
277    }
278
279    /// How many listeners are currently registered on `node`.
280    pub fn listener_count(&self, node: NodeId) -> usize {
281        self.listeners.by_node.get(&node).map_or(0, Vec::len)
282    }
283
284    /// Install (or remove, with `None`) the Dom's activation-behavior hook.
285    /// See [`ActivationHook`]. A backend installs one hook for all its
286    /// elements with activation behavior and dispatches on tag / type
287    /// inside it.
288    pub fn set_activation_hook(&mut self, hook: Option<ActivationHook<Ext>>) {
289        self.activation_hook = ActivationSlot(hook);
290    }
291
292    /// Run the activation hook for `phase`, taking it out of `self` for
293    /// the call so the hook can receive `&mut Dom` (and re-enter
294    /// dispatch without recursion into itself). A hook installed by the
295    /// hook itself during the call replaces the old one.
296    fn run_activation_hook(&mut self, target: NodeId, event: &Event, phase: ActivationPhase) {
297        let Some(mut hook) = self.activation_hook.0.take() else {
298            return;
299        };
300        hook(self, target, event, phase);
301        if self.activation_hook.0.is_none() {
302            self.activation_hook.0 = Some(hook);
303        }
304    }
305
306    /// Dispatch `event` at `target` per DOM §2.9: one capture pass from
307    /// the root down, one bubble pass from the target up. The target
308    /// takes part in both — its capture listeners fire in the capture
309    /// pass and its non-capture listeners in the bubble pass, each
310    /// reporting `EventPhase::AtTarget` — so registration order never
311    /// interleaves them, and `stop_propagation()` in a target capture
312    /// listener suppresses the target's bubble listeners. Returns `Err`
313    /// on an invalid target.
314    ///
315    /// When dispatch ends, `phase`, `current_target`, and the two
316    /// stop-propagation flags are reset (spec step 5.9), so the same
317    /// `Event` value can be dispatched again; `default_prevented`
318    /// persists.
319    ///
320    /// Handlers may mutate the Dom via `EventCtx::dom`. The ancestor path
321    /// is computed up-front so mid-dispatch mutations don't destabilize
322    /// iteration. Listeners added during dispatch fire on *subsequent*
323    /// dispatches, not the current one.
324    pub fn dispatch_event(&mut self, target: NodeId, event: &mut Event) -> Result<()> {
325        self.node_or_err(target)?;
326        if event.dispatching {
327            // DOM §2.9 step 1: the dispatch flag is set → InvalidStateError.
328            // Letting the inner dispatch run would reset the outer
329            // dispatch's propagation flags when it finished.
330            return Err(DomError::InvalidState("event is already being dispatched"));
331        }
332        // The flag is set for the passes and cleared when the guard
333        // drops — on every return, early (`InvalidNode`) or not, and when
334        // a listener panic unwinds (`P7G-DISPATCH-FLAG-1`).
335        self.dispatch_passes(target, &mut DispatchFlag::set(event))?;
336
337        // Activation behavior (DOM §2.9 step 11): after dispatch, once,
338        // whether or not propagation was stopped; `canceled` tells the
339        // hook to run the legacy-canceled-activation step instead.
340        let canceled = event.default_prevented();
341        self.run_activation_hook(target, event, ActivationPhase::Post { canceled });
342        Ok(())
343    }
344
345    /// The part of [`Self::dispatch_event`] run with the dispatch flag
346    /// set: the pre-activation step, the capture and bubble passes and
347    /// the end-of-dispatch reset (DOM §2.9 steps 5.5–5.9).
348    fn dispatch_passes(&mut self, target: NodeId, event: &mut Event) -> Result<()> {
349        event.target = Some(target);
350
351        // Legacy-pre-activation behavior (DOM §2.9 step 5.5): before any
352        // listener sees the event.
353        self.run_activation_hook(target, event, ActivationPhase::Pre);
354
355        // Path from root → target (inclusive). Always non-empty if the
356        // node is in the arena (the node itself is the last element).
357        let path = self.ancestor_path(target);
358        if path.is_empty() {
359            return Err(DomError::InvalidNode(target));
360        }
361
362        // DOM §2.9 dispatch: one capture pass root → target, one bubble
363        // pass target → root. The target participates in *both* passes
364        // (its capture listeners in the first, its non-capture listeners
365        // in the second) and reports `AtTarget` for each. This is what
366        // every engine ships since 2019: a target capture listener runs
367        // before a target bubble listener regardless of registration
368        // order, and `stopPropagation()` in the former suppresses the
369        // latter.
370
371        // ── Capture pass ──────────────────────────────────────────
372        event.phase = EventPhase::Capturing;
373        for &node in path.iter().take(path.len() - 1) {
374            if event.propagation_stopped {
375                break;
376            }
377            event.current_target = Some(node);
378            self.fire_at(node, event, PhaseFilter::Capture);
379        }
380        if !event.propagation_stopped {
381            event.phase = EventPhase::AtTarget;
382            event.current_target = Some(target);
383            self.fire_at(target, event, PhaseFilter::Capture);
384        }
385
386        // ── Bubble pass ───────────────────────────────────────────
387        if !event.propagation_stopped {
388            event.phase = EventPhase::AtTarget;
389            event.current_target = Some(target);
390            self.fire_at(target, event, PhaseFilter::Bubble);
391        }
392        if event.bubbles && !event.propagation_stopped {
393            event.phase = EventPhase::Bubbling;
394            for &node in path.iter().rev().skip(1) {
395                if event.propagation_stopped {
396                    break;
397                }
398                event.current_target = Some(node);
399                self.fire_at(node, event, PhaseFilter::Bubble);
400            }
401        }
402
403        // DOM §2.9 step 5.9: clear the propagation flags so the same
404        // `Event` can be dispatched again. `default_prevented` (the
405        // canceled flag) deliberately persists.
406        event.phase = EventPhase::None;
407        event.current_target = None;
408        event.propagation_stopped = false;
409        event.immediate_propagation_stopped = false;
410        Ok(())
411    }
412
413    /// Run matching listeners on `node`. Filter by `filter` (capture /
414    /// bubble / all). Handlers get `&mut self` via `EventCtx::dom`.
415    ///
416    /// Implementation: snapshot the list of listener sequences to run,
417    /// then for each one re-find it by seq, mem-replace its handler with
418    /// a no-op while calling it, and restore afterwards. Handlers may add
419    /// or remove listeners on this or any other node freely.
420    fn fire_at(&mut self, node: NodeId, event: &mut Event, filter: PhaseFilter) {
421        // Opportunistic sweep: drop any aborted listeners on this
422        // node before snapshotting. Cheap (only runs when we're
423        // about to dispatch) and keeps storage bounded across abort
424        // cycles.
425        if let Some(list) = self.listeners.by_node.get_mut(&node) {
426            list.retain(|l| !l.signal.as_ref().is_some_and(|s| s.is_aborted()));
427            if list.is_empty() {
428                self.listeners.by_node.remove(&node);
429            }
430        }
431
432        // Step 1: snapshot (seq, once) of listeners to run, in order.
433        let to_fire: Vec<(u32, bool)> = match self.listeners.by_node.get(&node) {
434            Some(list) => list
435                .iter()
436                .filter(|l| l.event_type == event.event_type && filter.matches(l.capture))
437                .map(|l| (l.seq, l.once))
438                .collect(),
439            None => return,
440        };
441
442        // Step 2: fire each in order, re-locating by seq each time in case
443        // handlers added/removed entries.
444        for (seq, once) in to_fire {
445            if event.immediate_propagation_stopped {
446                break;
447            }
448            // Locate the listener.
449            let Some(list) = self.listeners.by_node.get_mut(&node) else {
450                return;
451            };
452            let Some(i) = list.iter().position(|l| l.seq == seq) else {
453                continue; // removed by a previous handler
454            };
455            // Per-listener abort check — a prior handler in this
456            // same dispatch may have aborted a signal governing
457            // subsequent listeners. Drop + skip.
458            if list[i].signal.as_ref().is_some_and(|s| s.is_aborted()) {
459                list.remove(i);
460                continue;
461            }
462            // Take the handler out so we can hand &mut self to it.
463            let Some(mut handler) = list[i].handler.take() else {
464                // Re-entrant call on the same listener — skip.
465                continue;
466            };
467
468            // Run it. A panicking handler is caught so the listener can
469            // be restored below — otherwise a host that catches the
470            // panic is left with a listener that is counted but never
471            // fires again. The payload is re-raised after restore.
472            let outcome = {
473                let mut ctx = EventCtx { event, dom: self };
474                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| handler(&mut ctx)))
475            };
476
477            // Restore (or expire, for `once`).
478            if let Some(list) = self.listeners.by_node.get_mut(&node) {
479                if let Some(i) = list.iter().position(|l| l.seq == seq) {
480                    if once {
481                        list.remove(i);
482                    } else {
483                        list[i].handler = Some(handler);
484                    }
485                }
486                if list.is_empty() {
487                    self.listeners.by_node.remove(&node);
488                }
489            }
490            // If the node was removed from by_node mid-handler we simply
491            // drop `handler`. A dropped listener stays dropped — matches
492            // `remove_event_listener` semantics.
493
494            if let Err(payload) = outcome {
495                std::panic::resume_unwind(payload);
496            }
497        }
498    }
499
500    /// Internal hook: called from `free` to drop any listeners attached
501    /// to a node that's being discarded.
502    pub(crate) fn drop_listeners(&mut self, node: NodeId) {
503        self.listeners.by_node.remove(&node);
504    }
505}
506
507/// Which listeners to fire based on their `capture` flag.
508#[derive(Debug, Clone, Copy)]
509enum PhaseFilter {
510    Capture,
511    Bubble,
512}
513
514impl PhaseFilter {
515    fn matches(self, listener_capture: bool) -> bool {
516        match self {
517            PhaseFilter::Capture => listener_capture,
518            PhaseFilter::Bubble => !listener_capture,
519        }
520    }
521}
522
523#[cfg(test)]
524mod tests {
525    use super::*;
526    use crate::event::Event;
527    use std::cell::Cell;
528    use std::rc::Rc;
529
530    /// Small helper to build a tree root → a → b → c and return ids.
531    fn build_chain() -> (Dom, NodeId, NodeId, NodeId, NodeId) {
532        let mut dom: Dom = Dom::new();
533        let root = dom.root();
534        let a = dom.create_element("a");
535        let b = dom.create_element("b");
536        let c = dom.create_element("c");
537        dom.append_child(a, b).unwrap();
538        dom.append_child(b, c).unwrap();
539        dom.append_child(root, a).unwrap();
540        (dom, a, b, c, root)
541    }
542
543    /// A panicking listener stays registered. Without this, a host that
544    /// catches the panic ends up with a listener that is counted but
545    /// never fires again — silent breakage of the consumer's wiring.
546    #[test]
547    fn panicking_listener_is_restored_after_the_panic() {
548        let mut dom: Dom = Dom::new();
549        let el = dom.create_element("div");
550        let fired = Rc::new(Cell::new(0));
551        let f2 = fired.clone();
552        dom.add_event_listener(el, "click", ListenerOptions::default(), move |_| {
553            f2.set(f2.get() + 1);
554            if f2.get() == 1 {
555                panic!("listener bomb");
556            }
557        })
558        .unwrap();
559
560        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
561            let mut e = Event::new("click");
562            dom.dispatch_event(el, &mut e).unwrap();
563        }));
564        assert!(result.is_err());
565        assert_eq!(fired.get(), 1);
566        assert_eq!(dom.listener_count(el), 1);
567
568        // Second dispatch reaches the same listener again.
569        let mut e = Event::new("click");
570        dom.dispatch_event(el, &mut e).unwrap();
571        assert_eq!(fired.get(), 2);
572    }
573
574    /// `P7G-DISPATCH-FLAG-1`: a pre-activation hook that drops the
575    /// target makes `dispatch_event` return `InvalidNode` — and leaves
576    /// the event undispatched, so the caller can retry it elsewhere
577    /// instead of getting `InvalidState`.
578    #[test]
579    fn an_early_invalid_node_return_clears_the_dispatch_flag() {
580        let (mut dom, a, _, c, _) = build_chain();
581        dom.set_activation_hook(Some(Box::new(|dom, target, _, phase| {
582            if phase == ActivationPhase::Pre && dom.contains(target) {
583                let parent = dom.node(target).parent_node().unwrap().id();
584                dom.remove_child_dropping(parent, target).unwrap();
585            }
586        })));
587        let mut e = Event::new("click");
588        assert!(matches!(
589            dom.dispatch_event(c, &mut e),
590            Err(DomError::InvalidNode(_))
591        ));
592        dom.set_activation_hook(None);
593        assert_eq!(dom.dispatch_event(a, &mut e), Ok(()), "retry succeeds");
594    }
595
596    /// `P7G-DISPATCH-FLAG-1`: a listener panic that unwinds out of
597    /// `dispatch_event` clears the dispatch flag on the way out, so the
598    /// same `Event` value can be dispatched again.
599    #[test]
600    fn a_listener_panic_clears_the_dispatch_flag() {
601        let mut dom: Dom = Dom::new();
602        let el = dom.create_element("div");
603        dom.add_event_listener(el, "boom", ListenerOptions::default(), |_| {
604            panic!("listener bomb");
605        })
606        .unwrap();
607        let mut e = Event::new("click");
608        let mut boom = Event::new("boom");
609        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
610            let _ = dom.dispatch_event(el, &mut boom);
611        }));
612        assert!(result.is_err());
613        boom.event_type = "click".into();
614        assert_eq!(dom.dispatch_event(el, &mut boom), Ok(()));
615        assert_eq!(dom.dispatch_event(el, &mut e), Ok(()));
616    }
617
618    fn cap() -> ListenerOptions {
619        ListenerOptions {
620            capture: true,
621            ..ListenerOptions::default()
622        }
623    }
624
625    fn log_listener(
626        log: &Rc<std::cell::RefCell<Vec<&'static str>>>,
627        tag: &'static str,
628    ) -> impl FnMut(&mut EventCtx<'_, ()>) + 'static {
629        let log = log.clone();
630        move |_| log.borrow_mut().push(tag)
631    }
632
633    /// DOM §2.9 (2019+, shipped in every engine): at the target, capture
634    /// listeners run in the capture pass and non-capture listeners run
635    /// in the bubble pass — registration order does not interleave them.
636    #[test]
637    fn at_target_capture_listeners_fire_before_bubble_listeners() {
638        let (mut dom, _, _, c, _) = build_chain();
639        let log = Rc::new(std::cell::RefCell::new(Vec::new()));
640        // Register bubble listeners first so registration order would
641        // put them ahead of the capture listener.
642        dom.add_event_listener(
643            c,
644            "click",
645            ListenerOptions::default(),
646            log_listener(&log, "b1"),
647        )
648        .unwrap();
649        dom.add_event_listener(c, "click", cap(), log_listener(&log, "c1"))
650            .unwrap();
651        dom.add_event_listener(
652            c,
653            "click",
654            ListenerOptions::default(),
655            log_listener(&log, "b2"),
656        )
657        .unwrap();
658        dom.add_event_listener(c, "click", cap(), log_listener(&log, "c2"))
659            .unwrap();
660
661        let mut e = Event::new("click");
662        dom.dispatch_event(c, &mut e).unwrap();
663        assert_eq!(*log.borrow(), vec!["c1", "c2", "b1", "b2"]);
664    }
665
666    /// `stopPropagation()` in a target capture listener suppresses the
667    /// target's own bubble-side listeners (they belong to the next pass).
668    #[test]
669    fn stop_propagation_in_target_capture_listener_suppresses_target_bubble_listeners() {
670        let (mut dom, _, _, c, _) = build_chain();
671        let log = Rc::new(std::cell::RefCell::new(Vec::new()));
672        dom.add_event_listener(
673            c,
674            "click",
675            ListenerOptions::default(),
676            log_listener(&log, "bubble"),
677        )
678        .unwrap();
679        {
680            let log = log.clone();
681            dom.add_event_listener(c, "click", cap(), move |ctx| {
682                log.borrow_mut().push("capture");
683                ctx.event.stop_propagation();
684            })
685            .unwrap();
686        }
687        let mut e = Event::new("click");
688        dom.dispatch_event(c, &mut e).unwrap();
689        assert_eq!(*log.borrow(), vec!["capture"]);
690    }
691
692    /// Both listeners at the target report `AT_TARGET` as the phase.
693    #[test]
694    fn at_target_phase_is_reported_for_both_capture_and_bubble_listeners() {
695        let (mut dom, _, _, c, _) = build_chain();
696        let phases = Rc::new(std::cell::RefCell::new(Vec::new()));
697        for opts in [cap(), ListenerOptions::default()] {
698            let phases = phases.clone();
699            dom.add_event_listener(c, "click", opts, move |ctx| {
700                phases.borrow_mut().push(ctx.event.phase);
701            })
702            .unwrap();
703        }
704        let mut e = Event::new("click");
705        dom.dispatch_event(c, &mut e).unwrap();
706        assert_eq!(
707            *phases.borrow(),
708            vec![EventPhase::AtTarget, EventPhase::AtTarget]
709        );
710    }
711
712    /// DOM §2.9 step 5.9: the stop-propagation flags are cleared when
713    /// dispatch ends, so the same `Event` value can be dispatched again
714    /// (only `canceled` persists).
715    #[test]
716    fn propagation_flags_reset_after_dispatch_so_event_can_be_redispatched() {
717        let (mut dom, _, _, c, _) = build_chain();
718        let fired = Rc::new(Cell::new(0));
719        {
720            let fired = fired.clone();
721            dom.add_event_listener(c, "click", ListenerOptions::default(), move |ctx| {
722                fired.set(fired.get() + 1);
723                ctx.event.stop_immediate_propagation();
724                ctx.event.prevent_default();
725            })
726            .unwrap();
727        }
728        let mut e = Event::new("click");
729        dom.dispatch_event(c, &mut e).unwrap();
730        assert!(!e.is_propagation_stopped());
731        assert!(!e.is_immediate_propagation_stopped());
732        assert!(e.default_prevented(), "canceled flag persists");
733
734        dom.dispatch_event(c, &mut e).unwrap();
735        assert_eq!(fired.get(), 2, "second dispatch reaches the listener again");
736    }
737
738    /// DOM §2.9 step 1: dispatching an event whose dispatch flag is set
739    /// throws `InvalidStateError`. Without this, the inner dispatch's
740    /// end-of-dispatch flag reset would clobber the outer dispatch's
741    /// `stop_propagation()`.
742    #[test]
743    fn redispatching_an_in_flight_event_is_an_error_and_keeps_outer_flags() {
744        let (mut dom, a, _, c, _) = build_chain();
745        let inner_result = Rc::new(std::cell::RefCell::new(None));
746        let target_fired = Rc::new(Cell::new(false));
747        {
748            let inner_result = inner_result.clone();
749            dom.add_event_listener(a, "click", cap(), move |ctx| {
750                ctx.event.stop_propagation();
751                let r = ctx.dom.dispatch_event(a, ctx.event);
752                *inner_result.borrow_mut() = Some(r);
753            })
754            .unwrap();
755        }
756        {
757            let target_fired = target_fired.clone();
758            dom.add_event_listener(c, "click", ListenerOptions::default(), move |_| {
759                target_fired.set(true);
760            })
761            .unwrap();
762        }
763        let mut e = Event::new("click");
764        dom.dispatch_event(c, &mut e).unwrap();
765        assert!(matches!(
766            inner_result.borrow().as_ref(),
767            Some(Err(DomError::InvalidState(_)))
768        ));
769        assert!(
770            !target_fired.get(),
771            "outer stop_propagation survived the inner attempt"
772        );
773        // And the event is dispatchable again once the outer dispatch ends.
774        dom.dispatch_event(c, &mut e).unwrap();
775    }
776
777    #[test]
778    fn stale_id_is_rejected_by_dispatch_and_add_event_listener() {
779        let mut dom: Dom = Dom::new();
780        let root = dom.root();
781        let el = dom.create_element("div");
782        dom.append_child(root, el).unwrap();
783        dom.remove_child_dropping(root, el).unwrap();
784        let _reuses_slot = dom.create_element("span");
785        let mut e = Event::new("click");
786        assert!(matches!(
787            dom.dispatch_event(el, &mut e).unwrap_err(),
788            DomError::InvalidNode(_)
789        ));
790        assert!(matches!(
791            dom.add_event_listener(el, "click", ListenerOptions::default(), |_| {})
792                .unwrap_err(),
793            DomError::InvalidNode(_)
794        ));
795    }
796
797    /// DOM §2.9 activation behavior: the Dom's activation hook runs its
798    /// pre-activation step before any listener sees the event and its
799    /// post step after dispatch with the canceled flag — regardless of
800    /// `stopPropagation()`, which only affects listeners.
801    #[test]
802    fn activation_hook_runs_pre_before_listeners_and_post_after_even_when_propagation_stops() {
803        let (mut dom, _, _, c, _) = build_chain();
804        let log = Rc::new(std::cell::RefCell::new(Vec::<String>::new()));
805        {
806            let log = log.clone();
807            dom.set_activation_hook(Some(Box::new(move |_dom, target, event, phase| {
808                log.borrow_mut()
809                    .push(format!("{:?} {} {}", phase, event.event_type, target == c));
810            })));
811        }
812        {
813            let log = log.clone();
814            dom.add_event_listener(c, "click", ListenerOptions::default(), move |ctx| {
815                log.borrow_mut().push("listener".to_string());
816                ctx.event.stop_propagation();
817                ctx.event.prevent_default();
818            })
819            .unwrap();
820        }
821        let mut e = Event::new("click");
822        e.cancelable = true;
823        dom.dispatch_event(c, &mut e).unwrap();
824        assert_eq!(
825            *log.borrow(),
826            vec![
827                "Pre click true".to_string(),
828                "listener".to_string(),
829                "Post { canceled: true } click true".to_string(),
830            ]
831        );
832        // The hook can be removed again.
833        dom.set_activation_hook(None);
834        dom.dispatch_event(c, &mut Event::new("click")).unwrap();
835        assert_eq!(log.borrow().len(), 4, "only the listener ran");
836    }
837
838    #[test]
839    fn add_and_remove_listener() {
840        let mut dom: Dom = Dom::new();
841        let el = dom.create_element("div");
842        let fired = Rc::new(Cell::new(0));
843        let f2 = fired.clone();
844        let id = dom
845            .add_event_listener(el, "click", ListenerOptions::default(), move |_| {
846                f2.set(f2.get() + 1);
847            })
848            .unwrap();
849        assert_eq!(dom.listener_count(el), 1);
850
851        let mut e = Event::new("click");
852        dom.dispatch_event(el, &mut e).unwrap();
853        assert_eq!(fired.get(), 1);
854
855        assert!(dom.remove_event_listener(id));
856        assert_eq!(dom.listener_count(el), 0);
857
858        let mut e2 = Event::new("click");
859        dom.dispatch_event(el, &mut e2).unwrap();
860        assert_eq!(fired.get(), 1); // unchanged
861    }
862
863    #[test]
864    fn capture_target_bubble_ordering() {
865        let (mut dom, a, b, c, _) = build_chain();
866        let order = Rc::new(std::cell::RefCell::new(Vec::<&'static str>::new()));
867
868        let o = order.clone();
869        dom.add_event_listener(a, "click", ListenerOptions::capture(), move |_| {
870            o.borrow_mut().push("a-capture");
871        })
872        .unwrap();
873        let o = order.clone();
874        dom.add_event_listener(b, "click", ListenerOptions::capture(), move |_| {
875            o.borrow_mut().push("b-capture");
876        })
877        .unwrap();
878        let o = order.clone();
879        dom.add_event_listener(c, "click", ListenerOptions::default(), move |_| {
880            o.borrow_mut().push("c-target");
881        })
882        .unwrap();
883        let o = order.clone();
884        dom.add_event_listener(b, "click", ListenerOptions::default(), move |_| {
885            o.borrow_mut().push("b-bubble");
886        })
887        .unwrap();
888        let o = order.clone();
889        dom.add_event_listener(a, "click", ListenerOptions::default(), move |_| {
890            o.borrow_mut().push("a-bubble");
891        })
892        .unwrap();
893
894        let mut e = Event::new("click");
895        dom.dispatch_event(c, &mut e).unwrap();
896
897        assert_eq!(
898            *order.borrow(),
899            vec!["a-capture", "b-capture", "c-target", "b-bubble", "a-bubble"]
900        );
901    }
902
903    #[test]
904    fn stop_propagation_cuts_bubble() {
905        let (mut dom, a, b, c, _) = build_chain();
906        let a_fired = Rc::new(Cell::new(false));
907        let af = a_fired.clone();
908        dom.add_event_listener(a, "click", ListenerOptions::default(), move |_| {
909            af.set(true);
910        })
911        .unwrap();
912        dom.add_event_listener(b, "click", ListenerOptions::default(), move |ctx| {
913            ctx.event.stop_propagation();
914        })
915        .unwrap();
916
917        let mut e = Event::new("click");
918        dom.dispatch_event(c, &mut e).unwrap();
919        assert!(!a_fired.get());
920    }
921
922    #[test]
923    fn stop_immediate_cuts_sibling_listener_on_same_node() {
924        let mut dom: Dom = Dom::new();
925        let el = dom.create_element("div");
926        let hit_first = Rc::new(Cell::new(false));
927        let hit_second = Rc::new(Cell::new(false));
928        let h1 = hit_first.clone();
929        let h2 = hit_second.clone();
930        dom.add_event_listener(el, "x", ListenerOptions::default(), move |ctx| {
931            h1.set(true);
932            ctx.event.stop_immediate_propagation();
933        })
934        .unwrap();
935        dom.add_event_listener(el, "x", ListenerOptions::default(), move |_| {
936            h2.set(true);
937        })
938        .unwrap();
939
940        let mut e = Event::new("x");
941        dom.dispatch_event(el, &mut e).unwrap();
942        assert!(hit_first.get());
943        assert!(!hit_second.get());
944    }
945
946    #[test]
947    fn non_bubbling_skips_bubble_phase() {
948        let (mut dom, a, _, c, _) = build_chain();
949        let a_fired = Rc::new(Cell::new(false));
950        let af = a_fired.clone();
951        dom.add_event_listener(a, "x", ListenerOptions::default(), move |_| {
952            af.set(true);
953        })
954        .unwrap();
955
956        let mut e = Event::new("x").with_bubbles(false);
957        dom.dispatch_event(c, &mut e).unwrap();
958        assert!(!a_fired.get());
959    }
960
961    #[test]
962    fn prevent_default_sets_flag() {
963        let mut dom: Dom = Dom::new();
964        let el = dom.create_element("div");
965        dom.add_event_listener(el, "click", ListenerOptions::default(), move |ctx| {
966            ctx.event.prevent_default();
967        })
968        .unwrap();
969        let mut e = Event::new("click");
970        dom.dispatch_event(el, &mut e).unwrap();
971        assert!(e.default_prevented());
972    }
973
974    #[test]
975    fn once_removes_after_firing() {
976        let mut dom: Dom = Dom::new();
977        let el = dom.create_element("div");
978        let fired = Rc::new(Cell::new(0));
979        let f = fired.clone();
980        dom.add_event_listener(el, "x", ListenerOptions::once(), move |_| {
981            f.set(f.get() + 1);
982        })
983        .unwrap();
984
985        let mut e1 = Event::new("x");
986        dom.dispatch_event(el, &mut e1).unwrap();
987        let mut e2 = Event::new("x");
988        dom.dispatch_event(el, &mut e2).unwrap();
989        assert_eq!(fired.get(), 1);
990        assert_eq!(dom.listener_count(el), 0);
991    }
992
993    #[test]
994    fn listener_type_is_filtered() {
995        let mut dom: Dom = Dom::new();
996        let el = dom.create_element("div");
997        let click = Rc::new(Cell::new(0));
998        let input = Rc::new(Cell::new(0));
999        let cc = click.clone();
1000        let ii = input.clone();
1001        dom.add_event_listener(el, "click", ListenerOptions::default(), move |_| {
1002            cc.set(cc.get() + 1);
1003        })
1004        .unwrap();
1005        dom.add_event_listener(el, "input", ListenerOptions::default(), move |_| {
1006            ii.set(ii.get() + 1);
1007        })
1008        .unwrap();
1009        let mut e = Event::new("click");
1010        dom.dispatch_event(el, &mut e).unwrap();
1011        assert_eq!(click.get(), 1);
1012        assert_eq!(input.get(), 0);
1013    }
1014
1015    #[test]
1016    fn handler_may_read_dom() {
1017        let mut dom: Dom = Dom::new();
1018        let root = dom.root();
1019        let a = dom.create_element("a");
1020        dom.set_attribute(a, "id", "target").unwrap();
1021        dom.append_child(root, a).unwrap();
1022        let seen = Rc::new(Cell::new(false));
1023        let s = seen.clone();
1024        dom.add_event_listener(a, "click", ListenerOptions::default(), move |ctx| {
1025            if ctx.dom.get_element_by_id("target").is_some() {
1026                s.set(true);
1027            }
1028        })
1029        .unwrap();
1030
1031        let mut e = Event::new("click");
1032        dom.dispatch_event(a, &mut e).unwrap();
1033        assert!(seen.get());
1034    }
1035
1036    #[test]
1037    fn handler_may_mutate_dom() {
1038        let mut dom: Dom = Dom::new();
1039        let el = dom.create_element("div");
1040        dom.add_event_listener(el, "click", ListenerOptions::default(), move |ctx| {
1041            let added = ctx.dom.create_element("added");
1042            ctx.dom.append_child(el, added).unwrap();
1043        })
1044        .unwrap();
1045        let mut e = Event::new("click");
1046        dom.dispatch_event(el, &mut e).unwrap();
1047        assert_eq!(dom.node(el).child_element_count(), 1);
1048    }
1049
1050    #[test]
1051    fn dispatch_on_orphan_node_still_fires_target() {
1052        let mut dom: Dom = Dom::new();
1053        let el = dom.create_element("div");
1054        let fired = Rc::new(Cell::new(false));
1055        let f = fired.clone();
1056        dom.add_event_listener(el, "click", ListenerOptions::default(), move |_| {
1057            f.set(true);
1058        })
1059        .unwrap();
1060        let mut e = Event::new("click");
1061        dom.dispatch_event(el, &mut e).unwrap();
1062        assert!(fired.get());
1063    }
1064
1065    #[test]
1066    fn current_target_reflects_node_during_dispatch() {
1067        let (mut dom, a, _b, c, _) = build_chain();
1068        let ct_at_a = Rc::new(Cell::new(None));
1069        let p = ct_at_a.clone();
1070        dom.add_event_listener(a, "click", ListenerOptions::default(), move |ctx| {
1071            p.set(ctx.event.current_target);
1072        })
1073        .unwrap();
1074        let mut e = Event::new("click");
1075        dom.dispatch_event(c, &mut e).unwrap();
1076        assert_eq!(ct_at_a.get(), Some(a));
1077        // After dispatch, current_target is reset.
1078        assert_eq!(e.current_target, None);
1079    }
1080
1081    #[test]
1082    fn removing_node_unregisters_listeners_via_drop_subtree() {
1083        let mut dom: Dom = Dom::new();
1084        let el = dom.create_element("div");
1085        let _ = dom
1086            .add_event_listener(el, "click", ListenerOptions::default(), |_| {})
1087            .unwrap();
1088        let root = dom.root();
1089        dom.append_child(root, el).unwrap();
1090        dom.drop_subtree(el).unwrap();
1091        assert_eq!(dom.listener_count(el), 0);
1092    }
1093
1094    // ── AbortSignal / AbortController ────────────────────────────────
1095
1096    #[test]
1097    fn abort_signal_removes_listener_on_next_dispatch() {
1098        use crate::AbortController;
1099
1100        let mut dom: Dom = Dom::new();
1101        let el = dom.create_element("div");
1102        let fired = Rc::new(Cell::new(0));
1103        let f = fired.clone();
1104
1105        let ctrl = AbortController::new();
1106        let sig = ctrl.signal();
1107        dom.add_event_listener(
1108            el,
1109            "click",
1110            ListenerOptions::default().with_signal(sig),
1111            move |_| {
1112                f.set(f.get() + 1);
1113            },
1114        )
1115        .unwrap();
1116
1117        // First dispatch: fires normally.
1118        dom.dispatch_event(el, &mut Event::new("click")).unwrap();
1119        assert_eq!(fired.get(), 1);
1120
1121        // Abort, then dispatch again: listener is skipped + removed.
1122        ctrl.abort();
1123        dom.dispatch_event(el, &mut Event::new("click")).unwrap();
1124        assert_eq!(fired.get(), 1);
1125        assert_eq!(dom.listener_count(el), 0);
1126    }
1127
1128    #[test]
1129    fn abort_signal_removes_multiple_listeners_at_once() {
1130        use crate::AbortController;
1131
1132        let mut dom: Dom = Dom::new();
1133        let el = dom.create_element("div");
1134        let ctrl = AbortController::new();
1135        let sig = ctrl.signal();
1136
1137        for _ in 0..5 {
1138            dom.add_event_listener(
1139                el,
1140                "click",
1141                ListenerOptions::default().with_signal(sig.clone()),
1142                |_| {},
1143            )
1144            .unwrap();
1145        }
1146        assert_eq!(dom.listener_count(el), 5);
1147
1148        ctrl.abort();
1149        dom.dispatch_event(el, &mut Event::new("click")).unwrap();
1150        assert_eq!(dom.listener_count(el), 0);
1151    }
1152
1153    #[test]
1154    fn abort_signal_independent_of_other_listeners() {
1155        use crate::AbortController;
1156
1157        let mut dom: Dom = Dom::new();
1158        let el = dom.create_element("div");
1159        let ctrl = AbortController::new();
1160        let sig = ctrl.signal();
1161
1162        let fired_aborted = Rc::new(Cell::new(0));
1163        let fa = fired_aborted.clone();
1164        dom.add_event_listener(
1165            el,
1166            "click",
1167            ListenerOptions::default().with_signal(sig),
1168            move |_| {
1169                fa.set(fa.get() + 1);
1170            },
1171        )
1172        .unwrap();
1173
1174        let fired_normal = Rc::new(Cell::new(0));
1175        let fn_ = fired_normal.clone();
1176        dom.add_event_listener(el, "click", ListenerOptions::default(), move |_| {
1177            fn_.set(fn_.get() + 1);
1178        })
1179        .unwrap();
1180
1181        ctrl.abort();
1182        dom.dispatch_event(el, &mut Event::new("click")).unwrap();
1183        assert_eq!(fired_aborted.get(), 0, "aborted listener must not fire");
1184        assert_eq!(fired_normal.get(), 1, "unrelated listener still fires");
1185    }
1186
1187    #[test]
1188    fn adding_listener_with_already_aborted_signal_never_fires() {
1189        use crate::AbortController;
1190
1191        let mut dom: Dom = Dom::new();
1192        let el = dom.create_element("div");
1193        let ctrl = AbortController::new();
1194        ctrl.abort();
1195
1196        let fired = Rc::new(Cell::new(0));
1197        let f = fired.clone();
1198        dom.add_event_listener(
1199            el,
1200            "click",
1201            ListenerOptions::default().with_signal(ctrl.signal()),
1202            move |_| {
1203                f.set(f.get() + 1);
1204            },
1205        )
1206        .unwrap();
1207
1208        dom.dispatch_event(el, &mut Event::new("click")).unwrap();
1209        assert_eq!(fired.get(), 0);
1210        assert_eq!(dom.listener_count(el), 0);
1211    }
1212
1213    #[test]
1214    fn handler_aborting_mid_dispatch_skips_later_listeners_on_same_node() {
1215        use crate::AbortController;
1216
1217        let mut dom: Dom = Dom::new();
1218        let el = dom.create_element("div");
1219        let ctrl = AbortController::new();
1220
1221        let order = Rc::new(std::cell::RefCell::new(Vec::<&'static str>::new()));
1222
1223        // Listener A: fires + aborts the controller. Shares the
1224        // controller via `.clone()` into the closure.
1225        let order_a = order.clone();
1226        let ctrl_a = ctrl.clone();
1227        dom.add_event_listener(el, "click", ListenerOptions::default(), move |_| {
1228            order_a.borrow_mut().push("a");
1229            ctrl_a.abort();
1230        })
1231        .unwrap();
1232
1233        // Listener B: governed by the signal; should be skipped
1234        // after A aborts.
1235        let order_b = order.clone();
1236        dom.add_event_listener(
1237            el,
1238            "click",
1239            ListenerOptions::default().with_signal(ctrl.signal()),
1240            move |_| {
1241                order_b.borrow_mut().push("b");
1242            },
1243        )
1244        .unwrap();
1245
1246        // Listener C: not governed by the signal; fires regardless.
1247        let order_c = order.clone();
1248        dom.add_event_listener(el, "click", ListenerOptions::default(), move |_| {
1249            order_c.borrow_mut().push("c");
1250        })
1251        .unwrap();
1252
1253        dom.dispatch_event(el, &mut Event::new("click")).unwrap();
1254        assert_eq!(*order.borrow(), vec!["a", "c"]);
1255    }
1256
1257    #[test]
1258    fn signal_clone_governs_same_listener() {
1259        use crate::AbortController;
1260
1261        let mut dom: Dom = Dom::new();
1262        let el = dom.create_element("div");
1263        let ctrl = AbortController::new();
1264        let sig1 = ctrl.signal();
1265        let sig2 = sig1.clone();
1266
1267        let fired = Rc::new(Cell::new(0));
1268        let f = fired.clone();
1269        dom.add_event_listener(
1270            el,
1271            "click",
1272            ListenerOptions::default().with_signal(sig2),
1273            move |_| {
1274                f.set(f.get() + 1);
1275            },
1276        )
1277        .unwrap();
1278
1279        // Dispatch once: fires. Use sig1 to validate (not aborted).
1280        assert!(!sig1.is_aborted());
1281        dom.dispatch_event(el, &mut Event::new("click")).unwrap();
1282        assert_eq!(fired.get(), 1);
1283
1284        // Abort via controller, dispatch again: skipped.
1285        ctrl.abort();
1286        assert!(sig1.is_aborted());
1287        dom.dispatch_event(el, &mut Event::new("click")).unwrap();
1288        assert_eq!(fired.get(), 1);
1289    }
1290}