From f73ab22f2dd4fb18f23c687f83ec26ec53bafe11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABlle=20Huisman?= Date: Thu, 14 Aug 2025 14:36:03 +0200 Subject: [PATCH] feat: add events --- Cargo.lock | 7 + packages/dom/Cargo.toml | 36 ++++ packages/dom/src/config.rs | 1 + packages/dom/src/error.rs | 20 +++ packages/dom/src/events.rs | 294 +++++++++++++++++++++++++++++++ packages/dom/src/lib.rs | 2 + packages/dom/src/types/config.rs | 16 +- packages/dom/tests/events.rs | 75 ++++++++ 8 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 packages/dom/src/events.rs create mode 100644 packages/dom/tests/events.rs diff --git a/Cargo.lock b/Cargo.lock index fcbde29..7c7da66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -246,6 +246,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + [[package]] name = "shlex" version = "1.3.0" @@ -281,6 +287,7 @@ dependencies = [ "paste", "pretty-format", "regex", + "send_wrapper", "thiserror", "wasm-bindgen", "wasm-bindgen-test", diff --git a/packages/dom/Cargo.toml b/packages/dom/Cargo.toml index 9329ab1..24aca69 100644 --- a/packages/dom/Cargo.toml +++ b/packages/dom/Cargo.toml @@ -19,11 +19,24 @@ regex.workspace = true thiserror.workspace = true wasm-bindgen.workspace = true web-sys = { workspace = true, features = [ + "AnimationEvent", + "AnimationEventInit", "Attr", + "ClipboardEvent", + "ClipboardEventInit", "Comment", + "CompositionEvent", + "CompositionEventInit", "Document", "DocumentFragment", + "DragEvent", + "DragEventInit", "Element", + "Event", + "EventInit", + "EventTarget", + "FocusEvent", + "FocusEventInit", "HtmlCollection", "HtmlElement", "HtmlInputElement", @@ -31,15 +44,38 @@ web-sys = { workspace = true, features = [ "HtmlOptionElement", "HtmlOptionsCollection", "HtmlSelectElement", + "InputEvent", + "InputEventInit", + "KeyboardEvent", + "KeyboardEventInit", + "MouseEvent", + "MouseEventInit", "NamedNodeMap", "NodeList", + "PageTransitionEvent", + "PageTransitionEventInit", + "PointerEvent", + "PointerEventInit", + "PopStateEvent", + "PopStateEventInit", + "ProgressEvent", + "ProgressEventInit", "Text", + "TouchEvent", + "TouchEventInit", + "TransitionEvent", + "TransitionEventInit", + "UiEvent", + "UiEventInit", + "WheelEvent", + "WheelEventInit", "Window", ] } [dev-dependencies] indoc = "2.0.5" mockall = "0.13.0" +send_wrapper = "0.6.0" wasm-bindgen-test.workspace = true [lints.rust] diff --git a/packages/dom/src/config.rs b/packages/dom/src/config.rs index 3ac855a..22f176d 100644 --- a/packages/dom/src/config.rs +++ b/packages/dom/src/config.rs @@ -9,6 +9,7 @@ use crate::{ static CONFIG: LazyLock>> = LazyLock::new(|| { Arc::new(Mutex::new(Config { test_id_attribute: "data-testid".into(), + event_wrapper: Arc::new(|cb| cb()), default_hidden: false, default_ignore: "script, style".into(), show_original_stack_trace: false, diff --git a/packages/dom/src/error.rs b/packages/dom/src/error.rs index fa14019..70e7a4c 100644 --- a/packages/dom/src/error.rs +++ b/packages/dom/src/error.rs @@ -12,3 +12,23 @@ pub enum QueryError { #[error("{0}")] Unsupported(String), } + +#[derive(Debug, Error, PartialEq)] +pub enum FireEventError { + #[error("{0:?}")] + JsError(JsValue), +} + +#[derive(Debug, Error, PartialEq)] +pub enum CreateEventError { + #[error("{0:?}")] + JsError(JsValue), +} + +#[derive(Debug, Error, PartialEq)] +pub enum CreateOrFireEventError { + #[error(transparent)] + Create(#[from] CreateEventError), + #[error(transparent)] + Fire(#[from] FireEventError), +} diff --git a/packages/dom/src/events.rs b/packages/dom/src/events.rs new file mode 100644 index 0000000..d6c41fb --- /dev/null +++ b/packages/dom/src/events.rs @@ -0,0 +1,294 @@ +use paste::paste; +use wasm_bindgen::JsValue; +use web_sys::{ + AnimationEvent, AnimationEventInit, ClipboardEvent, ClipboardEventInit, CompositionEvent, + CompositionEventInit, DragEvent, DragEventInit, Event, EventInit, EventTarget, FocusEvent, + FocusEventInit, InputEvent, InputEventInit, KeyboardEvent, KeyboardEventInit, MouseEvent, + MouseEventInit, PageTransitionEvent, PageTransitionEventInit, PointerEvent, PointerEventInit, + PopStateEvent, PopStateEventInit, ProgressEvent, ProgressEventInit, TouchEvent, TouchEventInit, + TransitionEvent, TransitionEventInit, UiEvent, UiEventInit, WheelEvent, WheelEventInit, +}; + +use crate::{ + error::{CreateEventError, CreateOrFireEventError, FireEventError}, + get_config, +}; + +pub fn fire_event(node: &EventTarget, event: &E) -> Result { + (get_config().event_wrapper)(&|| { + node.dispatch_event(event.deref_event()) + .map_err(FireEventError::JsError) + }) +} + +pub type DefaultInitFn = dyn Fn(&::Init); + +pub struct CreateEventOptions<'a, E: EventType> { + default_init: Option<&'a DefaultInitFn>, +} + +impl<'a, E: EventType> CreateEventOptions<'a, E> { + fn default_init(mut self, value: &'a DefaultInitFn) -> Self { + self.default_init = Some(value); + self + } +} + +impl<'a, E: EventType> Default for CreateEventOptions<'a, E> { + fn default() -> Self { + Self { + default_init: Default::default(), + } + } +} + +pub fn create_event( + event_name: &str, + _node: &EventTarget, + init: Option, + options: CreateEventOptions, +) -> Result { + let event_init = init.unwrap_or_default(); + + if let Some(default_init) = options.default_init { + default_init(&event_init); + } + + E::new(event_name, &event_init).map_err(CreateEventError::JsError) +} + +pub struct CreateEvent; + +pub struct FireEvent; + +macro_rules! generate_events { + ($( ( $key:ident, $event_name:literal, $event_type:ty, { $( $init_key:ident : $init_value:literal ),* } ), )*) => { + paste! { + $( + fn [<$key default_init>](init: &[<$event_type Init>]) { + $( + if init.[]().is_none() { + init.[]($init_value); + } + )* + } + )* + + impl CreateEvent { + $( + pub fn $key(node: &EventTarget) -> Result<$event_type, CreateEventError> { + create_event($event_name, node, None, CreateEventOptions::default().default_init(&[<$key default_init>])) + } + + pub fn [<$key _with_init>](node: &EventTarget, init: [<$event_type Init>]) -> Result<$event_type, CreateEventError> { + create_event($event_name, node, Some(init), CreateEventOptions::default().default_init(&[<$key default_init>])) + } + )* + } + + impl FireEvent { + $( + pub fn $key(node: &EventTarget) -> Result { + Ok(fire_event(node, &CreateEvent::$key(node)?)?) + } + + pub fn [<$key _with_init>](node: &EventTarget, init: [<$event_type Init>]) -> Result { + Ok(fire_event(node, &CreateEvent::[<$key _with_init>](node, init)?)?) + } + )* + } + } + }; +} + +generate_events!( + // Clipboard Events + (copy, "copy", ClipboardEvent, {bubbles: true, cancelable: true, composed: true}), + (cut, "cut", ClipboardEvent, {bubbles: true, cancelable: true, composed: true}), + (paste, "paste", ClipboardEvent, {bubbles: true, cancelable: true, composed: true}), + // Composition Events + (composition_end, "compositionend", CompositionEvent, {bubbles: true, cancelable: true, composed: true}), + (composition_start, "compositionstart", CompositionEvent, {bubbles: true, cancelable: true, composed: true}), + (composition_update, "compositionupdate", CompositionEvent, {bubbles: true, cancelable: true, composed: true}), + // Keyboard Events + (key_down, "keydown", KeyboardEvent, {bubbles: true, cancelable: true, char_code: 0, composed: true}), + (key_press, "keypress", KeyboardEvent, {bubbles: true, cancelable: true, char_code: 0, composed: true}), + (key_up, "keyup", KeyboardEvent, {bubbles: true, cancelable: true, char_code: 0, composed: true}), + // Focus Events + (focus, "focus", FocusEvent, {bubbles: false, cancelable: false, composed: true}), + (blur, "blur", FocusEvent, {bubbles: false, cancelable: false, composed: true}), + (focus_in, "focusin", FocusEvent, {bubbles: true, cancelable: false, composed: true}), + (focus_out, "focusout", FocusEvent, {bubbles: true, cancelable: false, composed: true}), + // Form Events + (change, "change", Event, {bubbles: true, cancelable: false}), + (input, "input", InputEvent, {bubbles: true, cancelable: false, composed: true}), + (invalid, "invalid", Event, {bubbles: false, cancelable: true}), + (submit, "submit", Event, {bubbles: true, cancelable: true}), + (reset, "reset", Event, {bubbles: true, cancelable: true}), + // Mouse Events + (click, "click", MouseEvent, {bubbles: true, cancelable: true, button: 0, composed: true}), + (context_menu, "contextmenu", MouseEvent, {bubbles: true, cancelable: true, composed: true}), + (dbl_click, "dblclick", MouseEvent, {bubbles: true, cancelable: true, composed: true}), + (drag, "drag", DragEvent, {bubbles: true, cancelable: true, composed: true}), + (drag_end, "dragend", DragEvent, {bubbles: true, cancelable: false, composed: true}), + (drag_enter, "dragenter", DragEvent, {bubbles: true, cancelable: true, composed: true}), + (drag_exit, "dragexit", DragEvent, {bubbles: true, cancelable: false, composed: true}), + (drag_leave, "dragleave", DragEvent, {bubbles: true, cancelable: false, composed: true}), + (drag_over, "dragover", DragEvent, {bubbles: true, cancelable: true, composed: true}), + (drag_start, "dragstart", DragEvent, {bubbles: true, cancelable: true, composed: true}), + (drop, "drop", DragEvent, {bubbles: true, cancelable: true, composed: true}), + (mouse_down, "mousedown", MouseEvent, {bubbles: true, cancelable: true, composed: true}), + (mouse_enter, "mouseenter", MouseEvent, {bubbles: false, cancelable: false, composed: true}), + (mouse_leave, "mouseleave", MouseEvent, {bubbles: false, cancelable: false, composed: true}), + (mouse_move, "mousemove", MouseEvent, {bubbles: true, cancelable: true, composed: true}), + (mouse_out, "mouseout", MouseEvent, {bubbles: true, cancelable: true, composed: true}), + (mouse_over, "mouseover", MouseEvent, {bubbles: true, cancelable: true, composed: true}), + (mouse_up, "mouseup", MouseEvent, {bubbles: true, cancelable: true, composed: true}), + // Selection Events + (select, "select", Event, {bubbles: true, cancelable: false}), + // Touch Events + (touch_cancel, "touchcancel", TouchEvent, {bubbles: true, cancelable: false, composed: true}), + (touch_end, "touchend", TouchEvent, {bubbles: true, cancelable: true, composed: true}), + (touch_move, "touchmove", TouchEvent, {bubbles: true, cancelable: true, composed: true}), + (touch_start, "touchstart", TouchEvent, {bubbles: true, cancelable: true, composed: true}), + // UI Events + (resize, "resize", UiEvent, {bubbles: false, cancelable: false}), + (scroll, "scroll", UiEvent, {bubbles: false, cancelable: false}), + // Wheel Events + (wheel, "wheel", WheelEvent, {bubbles: true, cancelable: true, composed: true}), + // Media Events + (abort, "abort", Event, {bubbles: false, cancelable: false}), + (can_play, "canplay", Event, {bubbles: false, cancelable: false}), + (can_play_through, "canplaythrough", Event, {bubbles: false, cancelable: false}), + (duration_change, "durationchange", Event, {bubbles: false, cancelable: false}), + (emptied, "emptied", Event, {bubbles: false, cancelable: false}), + (encrypted, "encrypted", Event, {bubbles: false, cancelable: false}), + (ended, "ended", Event, {bubbles: false, cancelable: false}), + (loaded_data, "loadeddata", Event, {bubbles: false, cancelable: false}), + (loaded_metadata, "loadedmetadata", Event, {bubbles: false, cancelable: false}), + (load_start, "loadstart", ProgressEvent, {bubbles: false, cancelable: false}), + (pause, "pause", Event, {bubbles: false, cancelable: false}), + (play, "play", Event, {bubbles: false, cancelable: false}), + (playing, "playing", Event, {bubbles: false, cancelable: false}), + (progress, "progress", ProgressEvent, {bubbles: false, cancelable: false}), + (rate_change, "ratechange", Event, {bubbles: false, cancelable: false}), + (seeked, "seeked", Event, {bubbles: false, cancelable: false}), + (seeking, "seeking", Event, {bubbles: false, cancelable: false}), + (stalled, "stalled", Event, {bubbles: false, cancelable: false}), + (suspend, "suspend", Event, {bubbles: false, cancelable: false}), + (time_update, "timeupdate", Event, {bubbles: false, cancelable: false}), + (volume_change, "volumechange", Event, {bubbles: false, cancelable: false}), + (waiting, "waiting", Event, {bubbles: false, cancelable: false}), + // Events + // TODO: Load events can be UIEvent or Event depending on what generated them. + // This is where this abstraction breaks down. + // But the common targets are ,