use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::atomic::*};
use tairitsu_vdom::{ElementHandle, EventData, EventHandle, Platform, VElement, VNode, VText};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct MockElement(pub u64);
impl ElementHandle for MockElement {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[derive(Clone, Debug)]
pub struct MockEvent;
impl EventHandle for MockEvent {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
pub struct MockPlatform {
next_element_id: AtomicU64,
next_raf_id: AtomicU32,
raf_callbacks: Rc<RefCell<HashMap<u32, Box<dyn FnOnce(f64)>>>>,
element_text_content: Rc<RefCell<HashMap<u64, String>>>,
element_children: Rc<RefCell<HashMap<u64, Vec<u64>>>>,
element_attributes: Rc<RefCell<HashMap<u64, HashMap<String, String>>>>,
element_styles: Rc<RefCell<HashMap<u64, HashMap<String, String>>>>,
}
impl MockPlatform {
pub fn new() -> Self {
Self {
next_element_id: AtomicU64::new(1),
next_raf_id: AtomicU32::new(1),
raf_callbacks: Rc::new(RefCell::new(HashMap::new())),
element_text_content: Rc::new(RefCell::new(HashMap::new())),
element_children: Rc::new(RefCell::new(HashMap::new())),
element_attributes: Rc::new(RefCell::new(HashMap::new())),
element_styles: Rc::new(RefCell::new(HashMap::new())),
}
}
pub fn trigger_raf(&self, timestamp: f64) -> usize {
let mut callbacks = self.raf_callbacks.borrow_mut();
let count = callbacks.len();
let all_callbacks: Vec<_> = callbacks.drain().collect();
drop(callbacks);
for (_, callback) in all_callbacks {
callback(timestamp);
}
count
}
pub fn get_text_content(&self, element: MockElement) -> Option<String> {
self.element_text_content.borrow().get(&element.0).cloned()
}
pub fn set_text_content(&self, element: MockElement, text: String) {
self.element_text_content
.borrow_mut()
.insert(element.0, text);
}
pub fn get_attribute(&self, element: MockElement, name: &str) -> Option<String> {
self.element_attributes
.borrow()
.get(&element.0)?
.get(name)
.cloned()
}
pub fn get_children(&self, element: MockElement) -> Vec<MockElement> {
self.element_children
.borrow()
.get(&element.0)
.cloned()
.unwrap_or_default()
.into_iter()
.map(MockElement)
.collect()
}
pub fn pending_raf_count(&self) -> usize {
self.raf_callbacks.borrow().len()
}
}
impl Default for MockPlatform {
fn default() -> Self {
Self::new()
}
}
impl Platform for MockPlatform {
type Element = MockElement;
type Event = MockEvent;
fn create_element(&self, _tag: &str) -> Self::Element {
MockElement(self.next_element_id.fetch_add(1, Ordering::SeqCst))
}
fn create_text_node(&self, text: &str) -> Self::Element {
let id = self.next_element_id.fetch_add(1, Ordering::SeqCst);
self.set_text_content(MockElement(id), text.to_string());
MockElement(id)
}
fn append_child(&self, parent: &Self::Element, child: &Self::Element) {
self.element_children
.borrow_mut()
.entry(parent.0)
.or_insert_with(Vec::new)
.push(child.0);
}
fn remove_child(&self, parent: &Self::Element, child: &Self::Element) {
if let Some(children) = self.element_children.borrow_mut().get_mut(&parent.0) {
children.retain(|&id| id != child.0);
}
}
fn set_attribute(&self, element: &Self::Element, name: &str, value: &str) {
self.element_attributes
.borrow_mut()
.entry(element.0)
.or_insert_with(HashMap::new)
.insert(name.to_string(), value.to_string());
}
fn remove_attribute(&self, element: &Self::Element, name: &str) {
if let Some(attrs) = self.element_attributes.borrow_mut().get_mut(&element.0) {
attrs.remove(name);
}
}
fn set_style(&self, element: &Self::Element, name: &str, value: &str) {
self.element_styles
.borrow_mut()
.entry(element.0)
.or_insert_with(HashMap::new)
.insert(name.to_string(), value.to_string());
}
fn set_class(&self, _element: &Self::Element, _class: &str) {
}
fn add_event_listener(
&self,
_element: &Self::Element,
_event: &str,
_handler: Box<dyn FnMut(Box<dyn EventData>)>,
) {
}
fn remove_event_listener(&self, _element: &Self::Element, _event: &str) {
}
fn get_bounding_client_rect(&self, _element: &Self::Element) -> tairitsu_vdom::DomRect {
tairitsu_vdom::DomRect {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
}
}
fn inner_width(&self) -> i32 {
1024
}
fn inner_height(&self) -> i32 {
768
}
fn set_timeout(&self, _callback: Box<dyn FnOnce()>, _ms: i32) -> i32 {
0
}
fn clear_timeout(&self, _id: i32) {}
fn request_animation_frame(&self, callback: Box<dyn FnOnce(f64)>) -> u32 {
let id = self.next_raf_id.fetch_add(1, Ordering::SeqCst);
self.raf_callbacks.borrow_mut().insert(id, callback);
id
}
fn cancel_animation_frame(&self, id: u32) {
self.raf_callbacks.borrow_mut().remove(&id);
}
fn get_canvas_context(
&self,
_element: &Self::Element,
_context_type: &str,
) -> Option<tairitsu_vdom::CanvasContext> {
None
}
fn canvas_set_fill_style(&self, _ctx: tairitsu_vdom::CanvasContext, _color: &str) {}
fn canvas_fill_rect(
&self,
_ctx: tairitsu_vdom::CanvasContext,
_x: f64,
_y: f64,
_w: f64,
_h: f64,
) {
}
fn canvas_clear_rect(
&self,
_ctx: tairitsu_vdom::CanvasContext,
_x: f64,
_y: f64,
_w: f64,
_h: f64,
) {
}
fn create_resize_observer(
&self,
_callback: Box<dyn FnMut(Vec<tairitsu_vdom::ResizeObserverEntry>)>,
) -> u64 {
1
}
fn observe_resize(&self, _observer: u64, _element: &Self::Element) {}
fn unobserve_resize(&self, _observer: u64, _element: &Self::Element) {}
fn disconnect_resize(&self, _observer: u64) {}
fn create_mutation_observer(
&self,
_callback: Box<dyn FnMut(Vec<tairitsu_vdom::MutationRecord>)>,
) -> u64 {
1
}
fn observe_mutations(
&self,
_observer: u64,
_element: &Self::Element,
_options: Option<tairitsu_vdom::MutationObserverInit>,
) {
}
fn disconnect_mutation(&self, _observer: u64) {}
fn get_element_by_id(&self, _id: &str) -> Option<Self::Element> {
None
}
fn query_selector(&self, _selector: &str) -> Option<Self::Element> {
None
}
fn query_selector_all(&self, _selector: &str) -> Vec<Self::Element> {
vec![]
}
fn element_from_point(&self, _x: i32, _y: i32) -> Option<Self::Element> {
None
}
fn element_closest(&self, _element: &Self::Element, _selector: &str) -> Option<Self::Element> {
None
}
fn get_scroll_y(&self) -> f64 {
0.0
}
fn scroll_to(&self, _top: f64, _behavior: &str) {}
fn on_scroll(&self, _callback: Box<dyn FnMut(f64, f64)>) {}
fn on_resize(&self, _callback: Box<dyn FnMut(i32, i32)>) {}
fn copy_to_clipboard(&self, _text: &str) -> bool {
false
}
fn read_clipboard(&self) -> Option<String> {
None
}
fn clipboard_write_text_async(
&self,
_text: &str,
on_complete: Box<dyn FnOnce(Result<(), String>)>,
) {
on_complete(Ok(()));
}
fn clipboard_read_text_async(&self, on_complete: Box<dyn FnOnce(Result<String, String>)>) {
on_complete(Err("clipboard not available in mock".to_string()));
}
fn prefers_dark_mode(&self) -> bool {
false
}
fn get_element_rect_by_id(&self, _id: &str) -> Option<tairitsu_vdom::DomRect> {
None
}
fn get_bounding_rect_by_class(
&self,
_class_name: &str,
_element: &Self::Element,
) -> Option<tairitsu_vdom::DomRect> {
None
}
fn request_fullscreen(&self, _element: &Self::Element) {}
fn match_media(&self, _query: &str) -> u64 {
0
}
fn media_query_list_get_media(&self, _list: u64) -> String {
String::new()
}
fn media_query_list_get_matches(&self, _list: u64) -> bool {
false
}
fn media_query_list_add_listener(&self, _list: u64, _callback: Box<dyn FnMut(bool)>) -> u64 {
0
}
fn media_query_list_remove_listener(&self, _list: u64, _listener_id: u64) {}
fn get_target_element_from_event(
&self,
_client_x: i32,
_client_y: i32,
) -> Option<Self::Element> {
None
}
fn get_current_position(
&self,
_on_success: Box<dyn FnOnce(tairitsu_vdom::GeoPosition)>,
on_error: Box<dyn FnOnce(tairitsu_vdom::GeoPositionError)>,
_enable_high_accuracy: bool,
_timeout: u32,
_maximum_age: u32,
) {
on_error(tairitsu_vdom::GeoPositionError {
code: 1,
message: "geolocation not available in mock".to_string(),
});
}
fn file_reader_sync_read_as_text(
&self,
_blob: u64,
_encoding: Option<&str>,
) -> Result<String, String> {
Err("file reader not available in mock".to_string())
}
fn file_reader_sync_read_as_array_buffer(&self, _blob: u64) -> Result<Vec<u8>, String> {
Err("file reader not available in mock".to_string())
}
fn file_reader_read_as_text(
&self,
_blob: u64,
_encoding: Option<&str>,
on_complete: Box<dyn FnOnce(Result<String, String>)>,
) {
on_complete(Err("file reader not available in mock".to_string()));
}
fn file_reader_read_as_array_buffer(
&self,
_blob: u64,
on_complete: Box<dyn FnOnce(Result<Vec<u8>, String>)>,
) {
on_complete(Err("file reader not available in mock".to_string()));
}
fn idb_open(
&self,
_name: &str,
_version: Option<u64>,
on_complete: Box<dyn FnOnce(Result<u64, String>)>,
) -> u64 {
on_complete(Err("indexeddb not available in mock".to_string()));
0
}
fn idb_put(
&self,
_db: u64,
_store_name: &str,
_value: &str,
_key: Option<&str>,
on_complete: Box<dyn FnOnce(Result<(), String>)>,
) {
on_complete(Err("indexeddb not available in mock".to_string()));
}
fn idb_get(
&self,
_db: u64,
_store_name: &str,
_key: &str,
on_complete: Box<dyn FnOnce(Result<Option<String>, String>)>,
) {
on_complete(Err("indexeddb not available in mock".to_string()));
}
fn idb_delete(
&self,
_db: u64,
_store_name: &str,
_key: &str,
on_complete: Box<dyn FnOnce(Result<(), String>)>,
) {
on_complete(Err("indexeddb not available in mock".to_string()));
}
fn idb_get_all(
&self,
_db: u64,
_store_name: &str,
on_complete: Box<dyn FnOnce(Result<Vec<String>, String>)>,
) {
on_complete(Err("indexeddb not available in mock".to_string()));
}
fn idb_clear(
&self,
_db: u64,
_store_name: &str,
on_complete: Box<dyn FnOnce(Result<(), String>)>,
) {
on_complete(Err("indexeddb not available in mock".to_string()));
}
}
fn mount_vnode_with_refs(platform: &MockPlatform, vnode: &VNode) -> MockElement {
match vnode {
VNode::Element(velement) => {
let element = platform.create_element(&velement.tag);
if let Some(ref element_ref) = velement.element_ref {
use std::any::Any;
let mut ref_mut = element_ref.borrow_mut();
*ref_mut = Some(Box::new(element) as Box<dyn Any>);
}
for (name, value) in &velement.attributes {
platform.set_attribute(&element, name, value);
}
for (name, value) in &velement.style.css_variables {
platform.set_style(&element, name, value);
}
for child in &velement.children {
let child_element = mount_vnode_with_refs(platform, child);
platform.append_child(&element, &child_element);
}
element
}
VNode::Text(vtext) => platform.create_text_node(&vtext.text),
VNode::Fragment(children) => {
let wrapper = platform.create_element("fragment");
for child in children {
let child_element = mount_vnode_with_refs(platform, child);
platform.append_child(&wrapper, &child_element);
}
wrapper
}
}
}
#[cfg(test)]
mod test_element_ref_mounting {
use super::*;
use tairitsu_hooks::use_element_ref;
use tairitsu_vdom::vnode::VNode;
#[test]
fn test_element_ref_populated_after_mount() {
let platform = MockPlatform::new();
let ref_handle = use_element_ref::<MockElement>();
let any_ref = ref_handle.as_any_ref();
let velement = VElement {
tag: "div".to_string(),
key: None,
attributes: HashMap::new(),
children: Vec::new(),
style: tairitsu_vdom::Style::default(),
class: tairitsu_vdom::Classes::default(),
event_handlers: HashMap::new(),
inner_html: None,
element_ref: Some(any_ref.clone()),
};
let vnode = VNode::Element(velement);
mount_vnode_with_refs(&platform, &vnode);
let ref_value = any_ref.borrow();
assert!(
ref_value.is_some(),
"element_ref should be populated after mount"
);
if let Some(any_box) = ref_value.as_ref() {
if let Some(_element) = any_box.downcast_ref::<MockElement>() {
} else {
panic!("Failed to downcast to MockElement");
}
}
}
#[test]
fn test_element_ref_with_nested_children() {
let platform = MockPlatform::new();
let parent_ref = use_element_ref::<MockElement>();
let child_ref = use_element_ref::<MockElement>();
let parent_any_ref = parent_ref.as_any_ref();
let child_any_ref = child_ref.as_any_ref();
let parent_element = VElement {
tag: "div".to_string(),
key: None,
attributes: HashMap::new(),
children: vec![VNode::Element(VElement {
tag: "span".to_string(),
key: None,
attributes: HashMap::new(),
children: vec![VNode::Text(VText {
text: "Hello".to_string(),
})],
style: tairitsu_vdom::Style::default(),
class: tairitsu_vdom::Classes::default(),
event_handlers: HashMap::new(),
inner_html: None,
element_ref: Some(child_any_ref.clone()),
})],
style: tairitsu_vdom::Style::default(),
class: tairitsu_vdom::Classes::default(),
event_handlers: HashMap::new(),
inner_html: None,
element_ref: Some(parent_any_ref.clone()),
};
let vnode = VNode::Element(parent_element);
mount_vnode_with_refs(&platform, &vnode);
let parent_ref_value = parent_any_ref.borrow();
let child_ref_value = child_any_ref.borrow();
assert!(
parent_ref_value.is_some(),
"parent_ref should be populated after mount"
);
assert!(
child_ref_value.is_some(),
"child_ref should be populated after mount"
);
let parent_el = parent_ref_value
.as_ref()
.unwrap()
.downcast_ref::<MockElement>()
.unwrap();
let child_el = child_ref_value
.as_ref()
.unwrap()
.downcast_ref::<MockElement>()
.unwrap();
assert_ne!(
parent_el.0, child_el.0,
"parent and child should be different elements"
);
}
#[test]
fn test_element_ref_without_ref_attribute() {
let platform = MockPlatform::new();
let velement = VElement {
tag: "div".to_string(),
key: None,
attributes: HashMap::new(),
children: Vec::new(),
style: tairitsu_vdom::Style::default(),
class: tairitsu_vdom::Classes::default(),
event_handlers: HashMap::new(),
inner_html: None,
element_ref: None,
};
let vnode = VNode::Element(velement);
let element = mount_vnode_with_refs(&platform, &vnode);
assert!(element.0 > 0, "element should have a valid ID");
}
}
#[cfg(test)]
mod test_raf_animation {
use super::*;
use std::{cell::RefCell, rc::Rc, time::Duration};
use tairitsu_hooks::{
use_simple_animation, AnimationConfig, AnimationDirection, AnimationState, EasingFunction,
};
#[test]
#[ignore]
fn test_animation_completes_after_duration() {
let platform = MockPlatform::new();
let anim = use_simple_animation(300);
let _handle = anim.start_with_platform(&platform);
assert_eq!(anim.state(), AnimationState::Running);
assert!(anim.is_running());
let mut frame_count = 0;
let timestamps = [0.0, 50.0, 100.0, 150.0, 200.0, 250.0, 300.0, 350.0, 400.0];
for timestamp in timestamps {
while platform.pending_raf_count() > 0 {
frame_count += platform.trigger_raf(timestamp);
}
if anim.state() == AnimationState::Finished {
break;
}
}
assert_eq!(
anim.state(),
AnimationState::Finished,
"animation should be finished after duration"
);
assert!(!anim.is_running(), "animation should not be running");
assert_eq!(anim.progress(), 1.0, "final progress should be 1.0");
assert!(
frame_count >= 2,
"on_frame callback should be called at least twice, got {}",
frame_count
);
}
#[test]
fn test_animation_with_easing() {
let platform = MockPlatform::new();
let config = AnimationConfig {
duration: Duration::from_millis(100),
easing: EasingFunction::EaseOut,
..Default::default()
};
let anim = tairitsu_hooks::use_animation(Some(config));
let frame_progress_values: Rc<RefCell<Vec<f32>>> = Rc::new(RefCell::new(Vec::new()));
let frame_progress_clone = Rc::clone(&frame_progress_values);
anim.on_update(move |t| {
frame_progress_clone.borrow_mut().push(t);
});
anim.start_with_platform(&platform);
for ts in [0.0, 50.0, 100.0] {
platform.trigger_raf(ts);
}
let values = frame_progress_values.borrow();
if values.len() >= 2 {
let last_progress = values.last().unwrap();
assert!(*last_progress <= 1.0, "progress should not exceed 1.0");
}
}
#[test]
fn test_animation_can_be_cancelled() {
let platform = MockPlatform::new();
let anim = use_simple_animation(1000);
let handle = anim.start_with_platform(&platform);
while platform.pending_raf_count() > 0 {
platform.trigger_raf(0.0);
}
while platform.pending_raf_count() > 0 {
platform.trigger_raf(50.0);
}
assert!(anim.is_running());
handle.cancel();
assert!(!anim.is_running());
while platform.pending_raf_count() > 0 {
platform.trigger_raf(100.0);
}
assert_eq!(anim.state(), AnimationState::Idle);
}
#[test]
#[ignore]
fn test_animation_with_delay() {
let platform = MockPlatform::new();
let config = AnimationConfig {
duration: Duration::from_millis(100),
delay: Duration::from_millis(50),
..Default::default()
};
let anim = tairitsu_hooks::use_animation(Some(config));
anim.start_with_platform(&platform);
while platform.pending_raf_count() > 0 {
platform.trigger_raf(25.0);
}
assert_eq!(anim.progress(), 0.0, "progress should be 0 during delay");
while platform.pending_raf_count() > 0 {
platform.trigger_raf(75.0); }
assert!(
anim.progress() > 0.0,
"progress should advance after delay, got {}",
anim.progress()
);
}
#[test]
fn test_animation_alternate_direction() {
let platform = MockPlatform::new();
let config = AnimationConfig {
duration: Duration::from_millis(100),
direction: AnimationDirection::Alternate,
iterations: 2,
..Default::default()
};
let anim = tairitsu_hooks::use_animation(Some(config));
anim.start_with_platform(&platform);
platform.trigger_raf(0.0);
platform.trigger_raf(50.0);
let _progress_1 = anim.progress();
platform.trigger_raf(150.0);
let _progress_2 = anim.progress();
}
}
#[cfg(test)]
mod test_signal_dom_patch {
use super::*;
use tairitsu_hooks::use_signal;
#[test]
fn test_signal_update_triggers_dom_change() {
let platform = MockPlatform::new();
let signal = use_signal(|| 0);
let create_vnode = |value: i32| -> VNode {
VNode::Element(VElement {
tag: "div".to_string(),
key: None,
attributes: {
let mut attrs = HashMap::new();
attrs.insert("data-value".to_string(), value.to_string());
attrs
},
children: vec![VNode::Text(VText {
text: value.to_string(),
})],
style: tairitsu_vdom::Style::default(),
class: tairitsu_vdom::Classes::default(),
event_handlers: HashMap::new(),
inner_html: None,
element_ref: None,
})
};
let vnode_0 = create_vnode(0);
let element = mount_vnode_with_refs(&platform, &vnode_0);
let children = platform.get_children(element);
assert_eq!(children.len(), 1, "should have one child text node");
let text_node = children[0];
let text_content = platform.get_text_content(text_node);
assert_eq!(text_content, Some("0".to_string()));
signal.set(42);
let vnode_42 = create_vnode(42);
let element_updated = mount_vnode_with_refs(&platform, &vnode_42);
let children_updated = platform.get_children(element_updated);
assert_eq!(children_updated.len(), 1, "should have one child text node");
let text_node_updated = children_updated[0];
let text_content_updated = platform.get_text_content(text_node_updated);
assert_eq!(
text_content_updated,
Some("42".to_string()),
"DOM text should reflect updated signal value"
);
assert_eq!(signal.get(), 42);
}
#[test]
fn test_signal_get_and_set() {
let signal = use_signal(|| "hello".to_string());
assert_eq!(signal.get(), "hello");
signal.set("world".to_string());
assert_eq!(signal.get(), "world");
}
#[test]
fn test_signal_clone_independence() {
let signal1 = use_signal(|| 100);
let signal2 = signal1.clone();
signal1.set(200);
assert_eq!(signal1.get(), 200);
assert_eq!(signal2.get(), 200);
}
}
#[cfg(test)]
mod test_button_state_machine {
use tairitsu_hooks::{ButtonStateMachine, InteractionEvent, InteractionState};
#[test]
fn test_all_valid_transitions_from_table() {
let test_cases = vec![
(
InteractionState::Idle,
InteractionEvent::MouseEnter,
InteractionState::Hover,
),
(
InteractionState::Hover,
InteractionEvent::MouseLeave,
InteractionState::Idle,
),
(
InteractionState::Hover,
InteractionEvent::MouseDown,
InteractionState::Active,
),
(
InteractionState::Hover,
InteractionEvent::Focus,
InteractionState::Focused,
),
(
InteractionState::Active,
InteractionEvent::MouseUp,
InteractionState::Hover,
),
(
InteractionState::Active,
InteractionEvent::MouseLeave,
InteractionState::Idle,
),
(
InteractionState::Focused,
InteractionEvent::MouseEnter,
InteractionState::Hover,
),
(
InteractionState::Focused,
InteractionEvent::Blur,
InteractionState::Idle,
),
(
InteractionState::Idle,
InteractionEvent::Disable,
InteractionState::Disabled,
),
(
InteractionState::Hover,
InteractionEvent::Disable,
InteractionState::Disabled,
),
(
InteractionState::Active,
InteractionEvent::Disable,
InteractionState::Disabled,
),
(
InteractionState::Focused,
InteractionEvent::Disable,
InteractionState::Disabled,
),
(
InteractionState::Disabled,
InteractionEvent::Enable,
InteractionState::Idle,
),
];
for (initial, event, expected) in test_cases {
let mut sm = ButtonStateMachine::new();
sm.set_state(initial);
let result = sm.transition(event);
assert_eq!(
result,
Some(expected),
"Failed: {:?} + {:?} should be {:?}, got {:?}",
initial,
event,
expected,
result
);
assert_eq!(
sm.state(),
expected,
"State mismatch after transition: {:?} + {:?}",
initial,
event
);
}
}
#[test]
fn test_invalid_transitions_return_none() {
let invalid_cases = vec![
(InteractionState::Idle, InteractionEvent::MouseDown),
(InteractionState::Idle, InteractionEvent::MouseUp),
(InteractionState::Idle, InteractionEvent::MouseLeave),
(InteractionState::Hover, InteractionEvent::MouseEnter),
(InteractionState::Idle, InteractionEvent::Blur),
(InteractionState::Idle, InteractionEvent::Enable),
(InteractionState::Disabled, InteractionEvent::MouseEnter),
(InteractionState::Disabled, InteractionEvent::Focus),
];
for (initial, event) in invalid_cases {
let mut sm = ButtonStateMachine::new();
sm.set_state(initial);
let original_state = sm.state();
let result = sm.transition(event);
assert!(
result.is_none(),
"Transition {:?} + {:?} should be invalid (returned Some)",
original_state,
event
);
assert_eq!(
sm.state(),
original_state,
"State should not change on invalid transition"
);
}
}
#[test]
fn test_interaction_flow_hover_active_hover_idle() {
let mut sm = ButtonStateMachine::new();
assert_eq!(sm.state(), InteractionState::Idle);
assert!(sm.is_interactive());
assert_eq!(
sm.transition(InteractionEvent::MouseEnter),
Some(InteractionState::Hover)
);
assert_eq!(sm.state(), InteractionState::Hover);
assert_eq!(
sm.transition(InteractionEvent::MouseDown),
Some(InteractionState::Active)
);
assert_eq!(sm.state(), InteractionState::Active);
assert_eq!(
sm.transition(InteractionEvent::MouseUp),
Some(InteractionState::Hover)
);
assert_eq!(sm.state(), InteractionState::Hover);
assert_eq!(
sm.transition(InteractionEvent::MouseLeave),
Some(InteractionState::Idle)
);
assert_eq!(sm.state(), InteractionState::Idle);
}
#[test]
fn test_focus_transitions() {
let mut sm = ButtonStateMachine::new();
assert_eq!(
sm.transition(InteractionEvent::Focus),
Some(InteractionState::Focused)
);
assert_eq!(sm.state(), InteractionState::Focused);
assert_eq!(
sm.transition(InteractionEvent::MouseEnter),
Some(InteractionState::Hover)
);
assert_eq!(sm.state(), InteractionState::Hover);
assert_eq!(
sm.transition(InteractionEvent::MouseDown),
Some(InteractionState::Active)
);
assert_eq!(sm.state(), InteractionState::Active);
assert_eq!(
sm.transition(InteractionEvent::MouseUp),
Some(InteractionState::Hover)
);
assert_eq!(sm.state(), InteractionState::Hover);
assert_eq!(
sm.transition(InteractionEvent::MouseLeave),
Some(InteractionState::Idle)
);
assert_eq!(sm.state(), InteractionState::Idle);
}
#[test]
fn test_disable_from_all_states() {
for initial_state in &[
InteractionState::Idle,
InteractionState::Hover,
InteractionState::Active,
InteractionState::Focused,
] {
let mut sm = ButtonStateMachine::new();
sm.set_state(*initial_state);
assert_eq!(
sm.transition(InteractionEvent::Disable),
Some(InteractionState::Disabled),
"Disable should work from {:?}",
initial_state
);
assert_eq!(sm.state(), InteractionState::Disabled);
assert!(!sm.is_interactive());
}
}
#[test]
fn test_disabled_state_blocks_all_interactions() {
let mut sm = ButtonStateMachine::new();
sm.transition(InteractionEvent::Disable);
assert_eq!(sm.state(), InteractionState::Disabled);
let events = vec![
InteractionEvent::MouseEnter,
InteractionEvent::MouseLeave,
InteractionEvent::MouseDown,
InteractionEvent::MouseUp,
InteractionEvent::Focus,
InteractionEvent::Blur,
];
for event in events {
assert!(
sm.transition(event).is_none(),
"Event {:?} should be ignored while disabled",
event
);
assert_eq!(
sm.state(),
InteractionState::Disabled,
"State should remain Disabled"
);
}
}
#[test]
fn test_is_interactive() {
let mut sm = ButtonStateMachine::new();
for state in &[
InteractionState::Idle,
InteractionState::Hover,
InteractionState::Active,
InteractionState::Focused,
] {
sm.set_state(*state);
assert!(sm.is_interactive(), "{:?} should be interactive", state);
}
sm.set_state(InteractionState::Disabled);
assert!(!sm.is_interactive());
}
#[test]
fn test_reset() {
let mut sm = ButtonStateMachine::new();
sm.transition(InteractionEvent::MouseEnter);
sm.transition(InteractionEvent::MouseDown);
assert_eq!(sm.state(), InteractionState::Active);
sm.reset();
assert_eq!(sm.state(), InteractionState::Idle);
assert!(sm.is_interactive());
}
#[test]
fn test_acceptance_criteria_from_plan() {
let mut sm = ButtonStateMachine::new();
assert_eq!(
sm.transition(InteractionEvent::MouseEnter),
Some(InteractionState::Hover)
);
assert_eq!(
sm.transition(InteractionEvent::MouseDown),
Some(InteractionState::Active)
);
assert_eq!(
sm.transition(InteractionEvent::MouseUp),
Some(InteractionState::Hover)
);
assert_eq!(
sm.transition(InteractionEvent::MouseLeave),
Some(InteractionState::Idle)
);
}
}
pub mod integration_tests_summary {}