use std::collections::HashMap;
use crate::abort::AbortSignal;
use crate::dom::Dom;
use crate::error::{DomError, Result};
use crate::event::{Event, EventPhase};
use crate::node_id::NodeId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ListenerId {
pub node: NodeId,
pub seq: u32,
}
pub(crate) struct Listener<Ext: 'static> {
seq: u32,
event_type: String,
capture: bool,
once: bool,
signal: Option<AbortSignal>,
handler: Option<EventHandler<Ext>>,
}
pub(crate) type EventHandler<Ext> = Box<dyn FnMut(&mut EventCtx<'_, Ext>) + 'static>;
pub struct EventCtx<'a, Ext: 'static> {
pub event: &'a mut Event,
pub dom: &'a mut Dom<Ext>,
}
impl<Ext: 'static> EventCtx<'_, Ext> {
pub fn request_redraw(&mut self) {
self.event.redraw_requested = true;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ActivationPhase {
Pre,
Post { canceled: bool },
}
pub type ActivationHook<Ext> =
Box<dyn FnMut(&mut Dom<Ext>, NodeId, &Event, ActivationPhase) + 'static>;
pub(crate) struct ActivationSlot<Ext: 'static>(pub(crate) Option<ActivationHook<Ext>>);
impl<Ext: 'static> std::fmt::Debug for ActivationSlot<Ext> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(if self.0.is_some() {
"ActivationSlot(Some(hook))"
} else {
"ActivationSlot(None)"
})
}
}
#[derive(Debug, Clone, Default)]
pub struct ListenerOptions {
pub capture: bool,
pub once: bool,
pub signal: Option<AbortSignal>,
}
impl ListenerOptions {
pub fn capture() -> Self {
Self {
capture: true,
..Default::default()
}
}
pub fn once() -> Self {
Self {
once: true,
..Default::default()
}
}
pub fn with_signal(mut self, signal: AbortSignal) -> Self {
self.signal = Some(signal);
self
}
}
#[derive(Default)]
pub(crate) struct ListenerStore<Ext: 'static> {
by_node: HashMap<NodeId, Vec<Listener<Ext>>>,
next_seq: u32,
}
impl<Ext: 'static> ListenerStore<Ext> {
fn next(&mut self) -> u32 {
let n = self.next_seq;
self.next_seq = self.next_seq.wrapping_add(1);
n
}
}
impl<Ext: 'static> std::fmt::Debug for ListenerStore<Ext> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ListenerStore")
.field("nodes_with_listeners", &self.by_node.len())
.field("next_seq", &self.next_seq)
.finish()
}
}
impl<Ext> Dom<Ext> {
pub fn add_event_listener(
&mut self,
node: NodeId,
event_type: impl Into<String>,
options: ListenerOptions,
handler: impl FnMut(&mut EventCtx<'_, Ext>) + 'static,
) -> Result<ListenerId> {
self.node_or_err(node)?;
let seq = self.listeners.next();
let listener = Listener {
seq,
event_type: event_type.into(),
capture: options.capture,
once: options.once,
signal: options.signal,
handler: Some(Box::new(handler)),
};
self.listeners
.by_node
.entry(node)
.or_default()
.push(listener);
Ok(ListenerId { node, seq })
}
pub fn remove_event_listener(&mut self, handle: ListenerId) -> bool {
let Some(vec) = self.listeners.by_node.get_mut(&handle.node) else {
return false;
};
let before = vec.len();
vec.retain(|l| l.seq != handle.seq);
let removed = vec.len() < before;
if vec.is_empty() {
self.listeners.by_node.remove(&handle.node);
}
removed
}
pub fn listener_count(&self, node: NodeId) -> usize {
self.listeners.by_node.get(&node).map_or(0, Vec::len)
}
pub fn set_activation_hook(&mut self, hook: Option<ActivationHook<Ext>>) {
self.activation_hook = ActivationSlot(hook);
}
fn run_activation_hook(&mut self, target: NodeId, event: &Event, phase: ActivationPhase) {
let Some(mut hook) = self.activation_hook.0.take() else {
return;
};
hook(self, target, event, phase);
if self.activation_hook.0.is_none() {
self.activation_hook.0 = Some(hook);
}
}
pub fn dispatch_event(&mut self, target: NodeId, event: &mut Event) -> Result<()> {
self.node_or_err(target)?;
if event.dispatching {
return Err(DomError::InvalidState("event is already being dispatched"));
}
event.dispatching = true;
event.target = Some(target);
self.run_activation_hook(target, event, ActivationPhase::Pre);
let path = self.ancestor_path(target);
if path.is_empty() {
return Err(DomError::InvalidNode(target));
}
event.phase = EventPhase::Capturing;
for &node in path.iter().take(path.len() - 1) {
if event.propagation_stopped {
break;
}
event.current_target = Some(node);
self.fire_at(node, event, PhaseFilter::Capture);
}
if !event.propagation_stopped {
event.phase = EventPhase::AtTarget;
event.current_target = Some(target);
self.fire_at(target, event, PhaseFilter::Capture);
}
if !event.propagation_stopped {
event.phase = EventPhase::AtTarget;
event.current_target = Some(target);
self.fire_at(target, event, PhaseFilter::Bubble);
}
if event.bubbles && !event.propagation_stopped {
event.phase = EventPhase::Bubbling;
for &node in path.iter().rev().skip(1) {
if event.propagation_stopped {
break;
}
event.current_target = Some(node);
self.fire_at(node, event, PhaseFilter::Bubble);
}
}
event.phase = EventPhase::None;
event.current_target = None;
event.propagation_stopped = false;
event.immediate_propagation_stopped = false;
event.dispatching = false;
let canceled = event.default_prevented();
self.run_activation_hook(target, event, ActivationPhase::Post { canceled });
Ok(())
}
fn fire_at(&mut self, node: NodeId, event: &mut Event, filter: PhaseFilter) {
if let Some(list) = self.listeners.by_node.get_mut(&node) {
list.retain(|l| !l.signal.as_ref().is_some_and(|s| s.is_aborted()));
if list.is_empty() {
self.listeners.by_node.remove(&node);
}
}
let to_fire: Vec<(u32, bool)> = match self.listeners.by_node.get(&node) {
Some(list) => list
.iter()
.filter(|l| l.event_type == event.event_type && filter.matches(l.capture))
.map(|l| (l.seq, l.once))
.collect(),
None => return,
};
for (seq, once) in to_fire {
if event.immediate_propagation_stopped {
break;
}
let Some(list) = self.listeners.by_node.get_mut(&node) else {
return;
};
let Some(i) = list.iter().position(|l| l.seq == seq) else {
continue; };
if list[i].signal.as_ref().is_some_and(|s| s.is_aborted()) {
list.remove(i);
continue;
}
let Some(mut handler) = list[i].handler.take() else {
continue;
};
let outcome = {
let mut ctx = EventCtx { event, dom: self };
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| handler(&mut ctx)))
};
if let Some(list) = self.listeners.by_node.get_mut(&node) {
if let Some(i) = list.iter().position(|l| l.seq == seq) {
if once {
list.remove(i);
} else {
list[i].handler = Some(handler);
}
}
if list.is_empty() {
self.listeners.by_node.remove(&node);
}
}
if let Err(payload) = outcome {
std::panic::resume_unwind(payload);
}
}
}
pub(crate) fn drop_listeners(&mut self, node: NodeId) {
self.listeners.by_node.remove(&node);
}
}
#[derive(Debug, Clone, Copy)]
enum PhaseFilter {
Capture,
Bubble,
}
impl PhaseFilter {
fn matches(self, listener_capture: bool) -> bool {
match self {
PhaseFilter::Capture => listener_capture,
PhaseFilter::Bubble => !listener_capture,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::Event;
use std::cell::Cell;
use std::rc::Rc;
fn build_chain() -> (Dom, NodeId, NodeId, NodeId, NodeId) {
let mut dom: Dom = Dom::new();
let root = dom.root();
let a = dom.create_element("a");
let b = dom.create_element("b");
let c = dom.create_element("c");
dom.append_child(a, b).unwrap();
dom.append_child(b, c).unwrap();
dom.append_child(root, a).unwrap();
(dom, a, b, c, root)
}
#[test]
fn panicking_listener_is_restored_after_the_panic() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let fired = Rc::new(Cell::new(0));
let f2 = fired.clone();
dom.add_event_listener(el, "click", ListenerOptions::default(), move |_| {
f2.set(f2.get() + 1);
if f2.get() == 1 {
panic!("listener bomb");
}
})
.unwrap();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut e = Event::new("click");
dom.dispatch_event(el, &mut e).unwrap();
}));
assert!(result.is_err());
assert_eq!(fired.get(), 1);
assert_eq!(dom.listener_count(el), 1);
let mut e = Event::new("click");
dom.dispatch_event(el, &mut e).unwrap();
assert_eq!(fired.get(), 2);
}
fn cap() -> ListenerOptions {
ListenerOptions {
capture: true,
..ListenerOptions::default()
}
}
fn log_listener(
log: &Rc<std::cell::RefCell<Vec<&'static str>>>,
tag: &'static str,
) -> impl FnMut(&mut EventCtx<'_, ()>) + 'static {
let log = log.clone();
move |_| log.borrow_mut().push(tag)
}
#[test]
fn at_target_capture_listeners_fire_before_bubble_listeners() {
let (mut dom, _, _, c, _) = build_chain();
let log = Rc::new(std::cell::RefCell::new(Vec::new()));
dom.add_event_listener(
c,
"click",
ListenerOptions::default(),
log_listener(&log, "b1"),
)
.unwrap();
dom.add_event_listener(c, "click", cap(), log_listener(&log, "c1"))
.unwrap();
dom.add_event_listener(
c,
"click",
ListenerOptions::default(),
log_listener(&log, "b2"),
)
.unwrap();
dom.add_event_listener(c, "click", cap(), log_listener(&log, "c2"))
.unwrap();
let mut e = Event::new("click");
dom.dispatch_event(c, &mut e).unwrap();
assert_eq!(*log.borrow(), vec!["c1", "c2", "b1", "b2"]);
}
#[test]
fn stop_propagation_in_target_capture_listener_suppresses_target_bubble_listeners() {
let (mut dom, _, _, c, _) = build_chain();
let log = Rc::new(std::cell::RefCell::new(Vec::new()));
dom.add_event_listener(
c,
"click",
ListenerOptions::default(),
log_listener(&log, "bubble"),
)
.unwrap();
{
let log = log.clone();
dom.add_event_listener(c, "click", cap(), move |ctx| {
log.borrow_mut().push("capture");
ctx.event.stop_propagation();
})
.unwrap();
}
let mut e = Event::new("click");
dom.dispatch_event(c, &mut e).unwrap();
assert_eq!(*log.borrow(), vec!["capture"]);
}
#[test]
fn at_target_phase_is_reported_for_both_capture_and_bubble_listeners() {
let (mut dom, _, _, c, _) = build_chain();
let phases = Rc::new(std::cell::RefCell::new(Vec::new()));
for opts in [cap(), ListenerOptions::default()] {
let phases = phases.clone();
dom.add_event_listener(c, "click", opts, move |ctx| {
phases.borrow_mut().push(ctx.event.phase);
})
.unwrap();
}
let mut e = Event::new("click");
dom.dispatch_event(c, &mut e).unwrap();
assert_eq!(
*phases.borrow(),
vec![EventPhase::AtTarget, EventPhase::AtTarget]
);
}
#[test]
fn propagation_flags_reset_after_dispatch_so_event_can_be_redispatched() {
let (mut dom, _, _, c, _) = build_chain();
let fired = Rc::new(Cell::new(0));
{
let fired = fired.clone();
dom.add_event_listener(c, "click", ListenerOptions::default(), move |ctx| {
fired.set(fired.get() + 1);
ctx.event.stop_immediate_propagation();
ctx.event.prevent_default();
})
.unwrap();
}
let mut e = Event::new("click");
dom.dispatch_event(c, &mut e).unwrap();
assert!(!e.is_propagation_stopped());
assert!(!e.is_immediate_propagation_stopped());
assert!(e.default_prevented(), "canceled flag persists");
dom.dispatch_event(c, &mut e).unwrap();
assert_eq!(fired.get(), 2, "second dispatch reaches the listener again");
}
#[test]
fn redispatching_an_in_flight_event_is_an_error_and_keeps_outer_flags() {
let (mut dom, a, _, c, _) = build_chain();
let inner_result = Rc::new(std::cell::RefCell::new(None));
let target_fired = Rc::new(Cell::new(false));
{
let inner_result = inner_result.clone();
dom.add_event_listener(a, "click", cap(), move |ctx| {
ctx.event.stop_propagation();
let r = ctx.dom.dispatch_event(a, ctx.event);
*inner_result.borrow_mut() = Some(r);
})
.unwrap();
}
{
let target_fired = target_fired.clone();
dom.add_event_listener(c, "click", ListenerOptions::default(), move |_| {
target_fired.set(true);
})
.unwrap();
}
let mut e = Event::new("click");
dom.dispatch_event(c, &mut e).unwrap();
assert!(matches!(
inner_result.borrow().as_ref(),
Some(Err(DomError::InvalidState(_)))
));
assert!(
!target_fired.get(),
"outer stop_propagation survived the inner attempt"
);
dom.dispatch_event(c, &mut e).unwrap();
}
#[test]
fn stale_id_is_rejected_by_dispatch_and_add_event_listener() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let el = dom.create_element("div");
dom.append_child(root, el).unwrap();
dom.remove_child_dropping(root, el).unwrap();
let _reuses_slot = dom.create_element("span");
let mut e = Event::new("click");
assert!(matches!(
dom.dispatch_event(el, &mut e).unwrap_err(),
DomError::InvalidNode(_)
));
assert!(matches!(
dom.add_event_listener(el, "click", ListenerOptions::default(), |_| {})
.unwrap_err(),
DomError::InvalidNode(_)
));
}
#[test]
fn activation_hook_runs_pre_before_listeners_and_post_after_even_when_propagation_stops() {
let (mut dom, _, _, c, _) = build_chain();
let log = Rc::new(std::cell::RefCell::new(Vec::<String>::new()));
{
let log = log.clone();
dom.set_activation_hook(Some(Box::new(move |_dom, target, event, phase| {
log.borrow_mut()
.push(format!("{:?} {} {}", phase, event.event_type, target == c));
})));
}
{
let log = log.clone();
dom.add_event_listener(c, "click", ListenerOptions::default(), move |ctx| {
log.borrow_mut().push("listener".to_string());
ctx.event.stop_propagation();
ctx.event.prevent_default();
})
.unwrap();
}
let mut e = Event::new("click");
e.cancelable = true;
dom.dispatch_event(c, &mut e).unwrap();
assert_eq!(
*log.borrow(),
vec![
"Pre click true".to_string(),
"listener".to_string(),
"Post { canceled: true } click true".to_string(),
]
);
dom.set_activation_hook(None);
dom.dispatch_event(c, &mut Event::new("click")).unwrap();
assert_eq!(log.borrow().len(), 4, "only the listener ran");
}
#[test]
fn add_and_remove_listener() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let fired = Rc::new(Cell::new(0));
let f2 = fired.clone();
let id = dom
.add_event_listener(el, "click", ListenerOptions::default(), move |_| {
f2.set(f2.get() + 1);
})
.unwrap();
assert_eq!(dom.listener_count(el), 1);
let mut e = Event::new("click");
dom.dispatch_event(el, &mut e).unwrap();
assert_eq!(fired.get(), 1);
assert!(dom.remove_event_listener(id));
assert_eq!(dom.listener_count(el), 0);
let mut e2 = Event::new("click");
dom.dispatch_event(el, &mut e2).unwrap();
assert_eq!(fired.get(), 1); }
#[test]
fn capture_target_bubble_ordering() {
let (mut dom, a, b, c, _) = build_chain();
let order = Rc::new(std::cell::RefCell::new(Vec::<&'static str>::new()));
let o = order.clone();
dom.add_event_listener(a, "click", ListenerOptions::capture(), move |_| {
o.borrow_mut().push("a-capture");
})
.unwrap();
let o = order.clone();
dom.add_event_listener(b, "click", ListenerOptions::capture(), move |_| {
o.borrow_mut().push("b-capture");
})
.unwrap();
let o = order.clone();
dom.add_event_listener(c, "click", ListenerOptions::default(), move |_| {
o.borrow_mut().push("c-target");
})
.unwrap();
let o = order.clone();
dom.add_event_listener(b, "click", ListenerOptions::default(), move |_| {
o.borrow_mut().push("b-bubble");
})
.unwrap();
let o = order.clone();
dom.add_event_listener(a, "click", ListenerOptions::default(), move |_| {
o.borrow_mut().push("a-bubble");
})
.unwrap();
let mut e = Event::new("click");
dom.dispatch_event(c, &mut e).unwrap();
assert_eq!(
*order.borrow(),
vec!["a-capture", "b-capture", "c-target", "b-bubble", "a-bubble"]
);
}
#[test]
fn stop_propagation_cuts_bubble() {
let (mut dom, a, b, c, _) = build_chain();
let a_fired = Rc::new(Cell::new(false));
let af = a_fired.clone();
dom.add_event_listener(a, "click", ListenerOptions::default(), move |_| {
af.set(true);
})
.unwrap();
dom.add_event_listener(b, "click", ListenerOptions::default(), move |ctx| {
ctx.event.stop_propagation();
})
.unwrap();
let mut e = Event::new("click");
dom.dispatch_event(c, &mut e).unwrap();
assert!(!a_fired.get());
}
#[test]
fn stop_immediate_cuts_sibling_listener_on_same_node() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let hit_first = Rc::new(Cell::new(false));
let hit_second = Rc::new(Cell::new(false));
let h1 = hit_first.clone();
let h2 = hit_second.clone();
dom.add_event_listener(el, "x", ListenerOptions::default(), move |ctx| {
h1.set(true);
ctx.event.stop_immediate_propagation();
})
.unwrap();
dom.add_event_listener(el, "x", ListenerOptions::default(), move |_| {
h2.set(true);
})
.unwrap();
let mut e = Event::new("x");
dom.dispatch_event(el, &mut e).unwrap();
assert!(hit_first.get());
assert!(!hit_second.get());
}
#[test]
fn non_bubbling_skips_bubble_phase() {
let (mut dom, a, _, c, _) = build_chain();
let a_fired = Rc::new(Cell::new(false));
let af = a_fired.clone();
dom.add_event_listener(a, "x", ListenerOptions::default(), move |_| {
af.set(true);
})
.unwrap();
let mut e = Event::new("x").with_bubbles(false);
dom.dispatch_event(c, &mut e).unwrap();
assert!(!a_fired.get());
}
#[test]
fn prevent_default_sets_flag() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
dom.add_event_listener(el, "click", ListenerOptions::default(), move |ctx| {
ctx.event.prevent_default();
})
.unwrap();
let mut e = Event::new("click");
dom.dispatch_event(el, &mut e).unwrap();
assert!(e.default_prevented());
}
#[test]
fn once_removes_after_firing() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let fired = Rc::new(Cell::new(0));
let f = fired.clone();
dom.add_event_listener(el, "x", ListenerOptions::once(), move |_| {
f.set(f.get() + 1);
})
.unwrap();
let mut e1 = Event::new("x");
dom.dispatch_event(el, &mut e1).unwrap();
let mut e2 = Event::new("x");
dom.dispatch_event(el, &mut e2).unwrap();
assert_eq!(fired.get(), 1);
assert_eq!(dom.listener_count(el), 0);
}
#[test]
fn listener_type_is_filtered() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let click = Rc::new(Cell::new(0));
let input = Rc::new(Cell::new(0));
let cc = click.clone();
let ii = input.clone();
dom.add_event_listener(el, "click", ListenerOptions::default(), move |_| {
cc.set(cc.get() + 1);
})
.unwrap();
dom.add_event_listener(el, "input", ListenerOptions::default(), move |_| {
ii.set(ii.get() + 1);
})
.unwrap();
let mut e = Event::new("click");
dom.dispatch_event(el, &mut e).unwrap();
assert_eq!(click.get(), 1);
assert_eq!(input.get(), 0);
}
#[test]
fn handler_may_read_dom() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let a = dom.create_element("a");
dom.set_attribute(a, "id", "target").unwrap();
dom.append_child(root, a).unwrap();
let seen = Rc::new(Cell::new(false));
let s = seen.clone();
dom.add_event_listener(a, "click", ListenerOptions::default(), move |ctx| {
if ctx.dom.get_element_by_id("target").is_some() {
s.set(true);
}
})
.unwrap();
let mut e = Event::new("click");
dom.dispatch_event(a, &mut e).unwrap();
assert!(seen.get());
}
#[test]
fn handler_may_mutate_dom() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
dom.add_event_listener(el, "click", ListenerOptions::default(), move |ctx| {
let added = ctx.dom.create_element("added");
ctx.dom.append_child(el, added).unwrap();
})
.unwrap();
let mut e = Event::new("click");
dom.dispatch_event(el, &mut e).unwrap();
assert_eq!(dom.node(el).child_element_count(), 1);
}
#[test]
fn dispatch_on_orphan_node_still_fires_target() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let fired = Rc::new(Cell::new(false));
let f = fired.clone();
dom.add_event_listener(el, "click", ListenerOptions::default(), move |_| {
f.set(true);
})
.unwrap();
let mut e = Event::new("click");
dom.dispatch_event(el, &mut e).unwrap();
assert!(fired.get());
}
#[test]
fn current_target_reflects_node_during_dispatch() {
let (mut dom, a, _b, c, _) = build_chain();
let ct_at_a = Rc::new(Cell::new(None));
let p = ct_at_a.clone();
dom.add_event_listener(a, "click", ListenerOptions::default(), move |ctx| {
p.set(ctx.event.current_target);
})
.unwrap();
let mut e = Event::new("click");
dom.dispatch_event(c, &mut e).unwrap();
assert_eq!(ct_at_a.get(), Some(a));
assert_eq!(e.current_target, None);
}
#[test]
fn removing_node_unregisters_listeners_via_drop_subtree() {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let _ = dom
.add_event_listener(el, "click", ListenerOptions::default(), |_| {})
.unwrap();
let root = dom.root();
dom.append_child(root, el).unwrap();
dom.drop_subtree(el).unwrap();
assert_eq!(dom.listener_count(el), 0);
}
#[test]
fn abort_signal_removes_listener_on_next_dispatch() {
use crate::AbortController;
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let fired = Rc::new(Cell::new(0));
let f = fired.clone();
let ctrl = AbortController::new();
let sig = ctrl.signal();
dom.add_event_listener(
el,
"click",
ListenerOptions::default().with_signal(sig),
move |_| {
f.set(f.get() + 1);
},
)
.unwrap();
dom.dispatch_event(el, &mut Event::new("click")).unwrap();
assert_eq!(fired.get(), 1);
ctrl.abort();
dom.dispatch_event(el, &mut Event::new("click")).unwrap();
assert_eq!(fired.get(), 1);
assert_eq!(dom.listener_count(el), 0);
}
#[test]
fn abort_signal_removes_multiple_listeners_at_once() {
use crate::AbortController;
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let ctrl = AbortController::new();
let sig = ctrl.signal();
for _ in 0..5 {
dom.add_event_listener(
el,
"click",
ListenerOptions::default().with_signal(sig.clone()),
|_| {},
)
.unwrap();
}
assert_eq!(dom.listener_count(el), 5);
ctrl.abort();
dom.dispatch_event(el, &mut Event::new("click")).unwrap();
assert_eq!(dom.listener_count(el), 0);
}
#[test]
fn abort_signal_independent_of_other_listeners() {
use crate::AbortController;
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let ctrl = AbortController::new();
let sig = ctrl.signal();
let fired_aborted = Rc::new(Cell::new(0));
let fa = fired_aborted.clone();
dom.add_event_listener(
el,
"click",
ListenerOptions::default().with_signal(sig),
move |_| {
fa.set(fa.get() + 1);
},
)
.unwrap();
let fired_normal = Rc::new(Cell::new(0));
let fn_ = fired_normal.clone();
dom.add_event_listener(el, "click", ListenerOptions::default(), move |_| {
fn_.set(fn_.get() + 1);
})
.unwrap();
ctrl.abort();
dom.dispatch_event(el, &mut Event::new("click")).unwrap();
assert_eq!(fired_aborted.get(), 0, "aborted listener must not fire");
assert_eq!(fired_normal.get(), 1, "unrelated listener still fires");
}
#[test]
fn adding_listener_with_already_aborted_signal_never_fires() {
use crate::AbortController;
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let ctrl = AbortController::new();
ctrl.abort();
let fired = Rc::new(Cell::new(0));
let f = fired.clone();
dom.add_event_listener(
el,
"click",
ListenerOptions::default().with_signal(ctrl.signal()),
move |_| {
f.set(f.get() + 1);
},
)
.unwrap();
dom.dispatch_event(el, &mut Event::new("click")).unwrap();
assert_eq!(fired.get(), 0);
assert_eq!(dom.listener_count(el), 0);
}
#[test]
fn handler_aborting_mid_dispatch_skips_later_listeners_on_same_node() {
use crate::AbortController;
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let ctrl = AbortController::new();
let order = Rc::new(std::cell::RefCell::new(Vec::<&'static str>::new()));
let order_a = order.clone();
let ctrl_a = ctrl.clone();
dom.add_event_listener(el, "click", ListenerOptions::default(), move |_| {
order_a.borrow_mut().push("a");
ctrl_a.abort();
})
.unwrap();
let order_b = order.clone();
dom.add_event_listener(
el,
"click",
ListenerOptions::default().with_signal(ctrl.signal()),
move |_| {
order_b.borrow_mut().push("b");
},
)
.unwrap();
let order_c = order.clone();
dom.add_event_listener(el, "click", ListenerOptions::default(), move |_| {
order_c.borrow_mut().push("c");
})
.unwrap();
dom.dispatch_event(el, &mut Event::new("click")).unwrap();
assert_eq!(*order.borrow(), vec!["a", "c"]);
}
#[test]
fn signal_clone_governs_same_listener() {
use crate::AbortController;
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
let ctrl = AbortController::new();
let sig1 = ctrl.signal();
let sig2 = sig1.clone();
let fired = Rc::new(Cell::new(0));
let f = fired.clone();
dom.add_event_listener(
el,
"click",
ListenerOptions::default().with_signal(sig2),
move |_| {
f.set(f.get() + 1);
},
)
.unwrap();
assert!(!sig1.is_aborted());
dom.dispatch_event(el, &mut Event::new("click")).unwrap();
assert_eq!(fired.get(), 1);
ctrl.abort();
assert!(sig1.is_aborted());
dom.dispatch_event(el, &mut Event::new("click")).unwrap();
assert_eq!(fired.get(), 1);
}
}