Files
Zachary D. Rowitsch 853235bbe6 Add Phase 3 event dispatch: function expressions, listener registry, and click propagation
Implements the event dispatch infrastructure enabling JS scripts to register
event listeners and respond to click events with DOM-spec-compliant bubbling.

Key additions:
- Function expression parsing (anonymous and named) in js_parser
- Function identity via monotonic u64 IDs on JsFunction for removeEventListener
- call_function_with_host on JsVm that isolates handler errors (stays Primed, never Failed)
- EventListenerRegistry with addEventListener/removeEventListener deduplication
- DomEvent model with preventDefault/stopPropagation support
- Event dispatch algorithm: target phase + bubble phase with listener snapshots
- 11 integration tests covering click handlers, bubbling, error recovery, and event properties

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 15:34:53 -05:00

127 lines
3.4 KiB
Rust

use crate::event_target::EventTargetId;
/// Phase of event dispatch.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventPhase {
None = 0,
Capture = 1,
Target = 2,
Bubble = 3,
}
/// Result of dispatching an event.
#[derive(Debug, Clone, Copy)]
pub struct DispatchResult {
pub default_prevented: bool,
}
/// A DOM event being dispatched.
#[derive(Debug, Clone)]
pub struct DomEvent {
pub event_type: String,
pub target: EventTargetId,
pub current_target: EventTargetId,
pub phase: EventPhase,
pub bubbles: bool,
pub cancelable: bool,
pub default_prevented: bool,
pub propagation_stopped: bool,
}
impl DomEvent {
/// Create a click event targeting the given node.
pub fn click(target: EventTargetId) -> Self {
Self {
event_type: "click".into(),
target,
current_target: target,
phase: EventPhase::None,
bubbles: true,
cancelable: true,
default_prevented: false,
propagation_stopped: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use shared::NodeId;
#[test]
fn click_event_defaults() {
let target = EventTargetId::Node(NodeId::new(1));
let event = DomEvent::click(target);
assert_eq!(event.event_type, "click");
assert_eq!(event.target, target);
assert!(event.bubbles);
assert!(event.cancelable);
assert!(!event.default_prevented);
assert!(!event.propagation_stopped);
assert_eq!(event.phase, EventPhase::None);
}
#[test]
fn prevent_default_and_stop_propagation() {
let target = EventTargetId::Node(NodeId::new(1));
let mut event = DomEvent::click(target);
event.default_prevented = true;
event.propagation_stopped = true;
assert!(event.default_prevented);
assert!(event.propagation_stopped);
}
// --- EventPhase discriminant values match the DOM spec ---
#[test]
fn event_phase_discriminant_values() {
assert_eq!(EventPhase::None as u8, 0);
assert_eq!(EventPhase::Capture as u8, 1);
assert_eq!(EventPhase::Target as u8, 2);
assert_eq!(EventPhase::Bubble as u8, 3);
}
// --- EventPhase equality ---
#[test]
fn event_phase_equality() {
assert_eq!(EventPhase::Target, EventPhase::Target);
assert_ne!(EventPhase::Capture, EventPhase::Bubble);
}
// --- DomEvent::click sets current_target == target initially ---
#[test]
fn click_event_current_target_equals_target() {
let target = EventTargetId::Node(NodeId::new(5));
let event = DomEvent::click(target);
assert_eq!(event.current_target, event.target);
}
// --- DispatchResult default_prevented field ---
#[test]
fn dispatch_result_fields() {
let r = super::DispatchResult {
default_prevented: true,
};
assert!(r.default_prevented);
let r2 = super::DispatchResult {
default_prevented: false,
};
assert!(!r2.default_prevented);
}
// --- DomEvent for a Document target ---
#[test]
fn click_event_for_document_target() {
let target = EventTargetId::Document;
let event = DomEvent::click(target);
assert_eq!(event.target, EventTargetId::Document);
assert_eq!(event.current_target, EventTargetId::Document);
assert_eq!(event.event_type, "click");
}
}