Skip to main content

aprender_test_showcase/wasm/
dom.rs

1//! Mock DOM for WASM Testing
2//!
3//! This module provides DOM abstractions that enable 100% test coverage
4//! without requiring actual browser/web-sys dependencies.
5//!
6//! Probar: Visual feedback - Visual management through observable DOM state
7
8use std::collections::HashMap;
9
10/// Represents a DOM element for testing
11#[derive(Debug, Clone, PartialEq)]
12pub struct DomElement {
13    /// Element ID
14    pub id: String,
15    /// Element tag name
16    pub tag: String,
17    /// Text content
18    pub text_content: String,
19    /// Element attributes
20    pub attributes: HashMap<String, String>,
21    /// CSS classes
22    pub classes: Vec<String>,
23    /// Whether element is visible
24    pub visible: bool,
25    /// Child elements
26    pub children: Vec<DomElement>,
27}
28
29impl Default for DomElement {
30    fn default() -> Self {
31        Self::new("div")
32    }
33}
34
35impl DomElement {
36    /// Creates a new DOM element with the given tag
37    #[must_use]
38    pub fn new(tag: &str) -> Self {
39        Self {
40            id: String::new(),
41            tag: tag.to_string(),
42            text_content: String::new(),
43            attributes: HashMap::new(),
44            classes: Vec::new(),
45            visible: true,
46            children: Vec::new(),
47        }
48    }
49
50    /// Creates an element with an ID
51    #[must_use]
52    pub fn with_id(mut self, id: &str) -> Self {
53        self.id = id.to_string();
54        self
55    }
56
57    /// Sets the text content
58    #[must_use]
59    pub fn with_text(mut self, text: &str) -> Self {
60        self.text_content = text.to_string();
61        self
62    }
63
64    /// Adds a class
65    #[must_use]
66    pub fn with_class(mut self, class: &str) -> Self {
67        self.classes.push(class.to_string());
68        self
69    }
70
71    /// Sets an attribute
72    #[must_use]
73    pub fn with_attr(mut self, key: &str, value: &str) -> Self {
74        self.attributes.insert(key.to_string(), value.to_string());
75        self
76    }
77
78    /// Adds a child element
79    #[must_use]
80    pub fn with_child(mut self, child: DomElement) -> Self {
81        self.children.push(child);
82        self
83    }
84
85    /// Sets visibility
86    pub fn set_visible(&mut self, visible: bool) {
87        self.visible = visible;
88    }
89
90    /// Sets text content
91    pub fn set_text(&mut self, text: &str) {
92        self.text_content = text.to_string();
93    }
94
95    /// Adds a class
96    pub fn add_class(&mut self, class: &str) {
97        if !self.classes.contains(&class.to_string()) {
98            self.classes.push(class.to_string());
99        }
100    }
101
102    /// Removes a class
103    pub fn remove_class(&mut self, class: &str) {
104        self.classes.retain(|c| c != class);
105    }
106
107    /// Checks if element has a class
108    #[must_use]
109    pub fn has_class(&self, class: &str) -> bool {
110        self.classes.contains(&class.to_string())
111    }
112
113    /// Gets an attribute value
114    #[must_use]
115    pub fn get_attr(&self, key: &str) -> Option<&str> {
116        self.attributes.get(key).map(|s| s.as_str())
117    }
118}
119
120/// DOM events that can be dispatched
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub enum DomEvent {
123    /// Click event on an element
124    Click {
125        /// The ID of the clicked element
126        element_id: String,
127    },
128    /// Input event with new value
129    Input {
130        /// The ID of the input element
131        element_id: String,
132        /// The new value entered
133        value: String,
134    },
135    /// Key press event
136    KeyPress {
137        /// The key that was pressed
138        key: String,
139        /// Whether Ctrl was held
140        ctrl: bool,
141        /// Whether Shift was held
142        shift: bool,
143    },
144    /// Focus event on an element
145    Focus {
146        /// The ID of the focused element
147        element_id: String,
148    },
149    /// Blur event (element lost focus)
150    Blur {
151        /// The ID of the element that lost focus
152        element_id: String,
153    },
154    /// Submit event (for forms)
155    Submit {
156        /// The ID of the submitted form
157        element_id: String,
158    },
159}
160
161impl DomEvent {
162    /// Creates a click event
163    #[must_use]
164    pub fn click(element_id: &str) -> Self {
165        Self::Click {
166            element_id: element_id.to_string(),
167        }
168    }
169
170    /// Creates an input event
171    #[must_use]
172    pub fn input(element_id: &str, value: &str) -> Self {
173        Self::Input {
174            element_id: element_id.to_string(),
175            value: value.to_string(),
176        }
177    }
178
179    /// Creates a key press event
180    #[must_use]
181    pub fn key_press(key: &str) -> Self {
182        Self::KeyPress {
183            key: key.to_string(),
184            ctrl: false,
185            shift: false,
186        }
187    }
188
189    /// Creates a key press with modifiers
190    #[must_use]
191    pub fn key_press_with_modifiers(key: &str, ctrl: bool, shift: bool) -> Self {
192        Self::KeyPress {
193            key: key.to_string(),
194            ctrl,
195            shift,
196        }
197    }
198
199    /// Creates a focus event
200    #[must_use]
201    pub fn focus(element_id: &str) -> Self {
202        Self::Focus {
203            element_id: element_id.to_string(),
204        }
205    }
206
207    /// Creates a blur event
208    #[must_use]
209    pub fn blur(element_id: &str) -> Self {
210        Self::Blur {
211            element_id: element_id.to_string(),
212        }
213    }
214
215    /// Creates a submit event
216    #[must_use]
217    pub fn submit(element_id: &str) -> Self {
218        Self::Submit {
219            element_id: element_id.to_string(),
220        }
221    }
222}
223
224/// Mock DOM for testing WASM calculator without browser
225#[derive(Debug)]
226pub struct MockDom {
227    /// Root element
228    pub root: DomElement,
229    /// Elements by ID for quick lookup
230    elements: HashMap<String, DomElement>,
231    /// Event history for verification
232    event_history: Vec<DomEvent>,
233    /// Focused element ID
234    focused_element: Option<String>,
235}
236
237impl Default for MockDom {
238    fn default() -> Self {
239        Self::new()
240    }
241}
242
243impl MockDom {
244    /// Creates a new mock DOM
245    #[must_use]
246    pub fn new() -> Self {
247        Self {
248            root: DomElement::new("div").with_id("root"),
249            elements: HashMap::new(),
250            event_history: Vec::new(),
251            focused_element: None,
252        }
253    }
254
255    /// Creates a calculator DOM structure
256    #[must_use]
257    pub fn calculator() -> Self {
258        let mut dom = Self::new();
259
260        // Input field
261        let input = DomElement::new("input")
262            .with_id("calc-input")
263            .with_attr("type", "text")
264            .with_attr("placeholder", "Enter expression");
265
266        // Result display
267        let result = DomElement::new("div")
268            .with_id("calc-result")
269            .with_class("result-display");
270
271        // History list
272        let history = DomElement::new("ul")
273            .with_id("calc-history")
274            .with_class("history-list");
275
276        // Status display for Anomaly
277        let status = DomElement::new("div")
278            .with_id("calc-status")
279            .with_class("status-display");
280
281        // Buttons
282        let clear_btn = DomElement::new("button")
283            .with_id("btn-clear")
284            .with_text("C");
285
286        let equals_btn = DomElement::new("button")
287            .with_id("btn-equals")
288            .with_text("=");
289
290        // Build root
291        dom.root = DomElement::new("div")
292            .with_id("calculator")
293            .with_class("calculator-app")
294            .with_child(input.clone())
295            .with_child(result.clone())
296            .with_child(history.clone())
297            .with_child(status.clone())
298            .with_child(clear_btn.clone())
299            .with_child(equals_btn.clone());
300
301        // Register elements
302        dom.register_element(input);
303        dom.register_element(result);
304        dom.register_element(history);
305        dom.register_element(status);
306        dom.register_element(clear_btn);
307        dom.register_element(equals_btn);
308
309        dom
310    }
311
312    /// Registers an element for ID lookup
313    pub fn register_element(&mut self, element: DomElement) {
314        if !element.id.is_empty() {
315            self.elements.insert(element.id.clone(), element);
316        }
317    }
318
319    /// Gets an element by ID
320    #[must_use]
321    pub fn get_element(&self, id: &str) -> Option<&DomElement> {
322        self.elements.get(id)
323    }
324
325    /// Gets a mutable element by ID
326    pub fn get_element_mut(&mut self, id: &str) -> Option<&mut DomElement> {
327        self.elements.get_mut(id)
328    }
329
330    /// Dispatches an event
331    pub fn dispatch_event(&mut self, event: DomEvent) {
332        self.event_history.push(event.clone());
333
334        match &event {
335            DomEvent::Focus { element_id } => {
336                self.focused_element = Some(element_id.clone());
337            }
338            DomEvent::Blur { .. } => {
339                self.focused_element = None;
340            }
341            DomEvent::Input { element_id, value } => {
342                if let Some(elem) = self.elements.get_mut(element_id) {
343                    elem.set_text(value);
344                    elem.attributes.insert("value".to_string(), value.clone());
345                }
346            }
347            _ => {}
348        }
349    }
350
351    /// Gets the event history
352    #[must_use]
353    pub fn event_history(&self) -> &[DomEvent] {
354        &self.event_history
355    }
356
357    /// Clears event history
358    pub fn clear_event_history(&mut self) {
359        self.event_history.clear();
360    }
361
362    /// Gets the currently focused element ID
363    #[must_use]
364    pub fn focused_element(&self) -> Option<&str> {
365        self.focused_element.as_deref()
366    }
367
368    /// Updates element text by ID
369    pub fn set_element_text(&mut self, id: &str, text: &str) {
370        if let Some(elem) = self.elements.get_mut(id) {
371            elem.set_text(text);
372        }
373    }
374
375    /// Gets element text by ID
376    #[must_use]
377    pub fn get_element_text(&self, id: &str) -> Option<&str> {
378        self.elements.get(id).map(|e| e.text_content.as_str())
379    }
380
381    /// Adds a child element to a parent
382    pub fn append_child(&mut self, parent_id: &str, child: DomElement) {
383        let child_id = child.id.clone();
384        if let Some(parent) = self.elements.get_mut(parent_id) {
385            parent.children.push(child.clone());
386        }
387        if !child_id.is_empty() {
388            self.elements.insert(child_id, child);
389        }
390    }
391
392    /// Clears children of an element
393    pub fn clear_children(&mut self, id: &str) {
394        // First, collect child IDs to remove
395        let child_ids: Vec<String> = self
396            .elements
397            .get(id)
398            .map(|elem| {
399                elem.children
400                    .iter()
401                    .filter(|c| !c.id.is_empty())
402                    .map(|c| c.id.clone())
403                    .collect()
404            })
405            .unwrap_or_default();
406
407        // Remove child elements from registry
408        for child_id in child_ids {
409            self.elements.remove(&child_id);
410        }
411
412        // Clear children list
413        if let Some(elem) = self.elements.get_mut(id) {
414            elem.children.clear();
415        }
416    }
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422
423    // ===== DomElement tests =====
424
425    #[test]
426    fn test_dom_element_new() {
427        let elem = DomElement::new("span");
428        assert_eq!(elem.tag, "span");
429        assert!(elem.id.is_empty());
430        assert!(elem.text_content.is_empty());
431    }
432
433    #[test]
434    fn test_dom_element_default() {
435        let elem = DomElement::default();
436        assert_eq!(elem.tag, "div");
437    }
438
439    #[test]
440    fn test_dom_element_with_id() {
441        let elem = DomElement::new("div").with_id("test-id");
442        assert_eq!(elem.id, "test-id");
443    }
444
445    #[test]
446    fn test_dom_element_with_text() {
447        let elem = DomElement::new("p").with_text("Hello");
448        assert_eq!(elem.text_content, "Hello");
449    }
450
451    #[test]
452    fn test_dom_element_with_class() {
453        let elem = DomElement::new("div").with_class("active");
454        assert!(elem.classes.contains(&"active".to_string()));
455    }
456
457    #[test]
458    fn test_dom_element_with_attr() {
459        let elem = DomElement::new("input").with_attr("type", "text");
460        assert_eq!(elem.get_attr("type"), Some("text"));
461    }
462
463    #[test]
464    fn test_dom_element_with_child() {
465        let child = DomElement::new("span").with_text("child");
466        let parent = DomElement::new("div").with_child(child);
467        assert_eq!(parent.children.len(), 1);
468        assert_eq!(parent.children[0].text_content, "child");
469    }
470
471    #[test]
472    fn test_dom_element_set_visible() {
473        let mut elem = DomElement::new("div");
474        assert!(elem.visible);
475        elem.set_visible(false);
476        assert!(!elem.visible);
477    }
478
479    #[test]
480    fn test_dom_element_set_text() {
481        let mut elem = DomElement::new("div");
482        elem.set_text("Updated");
483        assert_eq!(elem.text_content, "Updated");
484    }
485
486    #[test]
487    fn test_dom_element_add_class() {
488        let mut elem = DomElement::new("div");
489        elem.add_class("foo");
490        elem.add_class("bar");
491        elem.add_class("foo"); // duplicate should not be added
492        assert_eq!(elem.classes.len(), 2);
493    }
494
495    #[test]
496    fn test_dom_element_remove_class() {
497        let mut elem = DomElement::new("div").with_class("foo").with_class("bar");
498        elem.remove_class("foo");
499        assert!(!elem.has_class("foo"));
500        assert!(elem.has_class("bar"));
501    }
502
503    #[test]
504    fn test_dom_element_has_class() {
505        let elem = DomElement::new("div").with_class("active");
506        assert!(elem.has_class("active"));
507        assert!(!elem.has_class("inactive"));
508    }
509
510    #[test]
511    fn test_dom_element_get_attr_none() {
512        let elem = DomElement::new("div");
513        assert_eq!(elem.get_attr("missing"), None);
514    }
515
516    #[test]
517    fn test_dom_element_clone() {
518        let elem = DomElement::new("div").with_id("test").with_text("content");
519        let cloned = elem.clone();
520        assert_eq!(elem, cloned);
521    }
522
523    #[test]
524    fn test_dom_element_debug() {
525        let elem = DomElement::new("div");
526        let debug = format!("{:?}", elem);
527        assert!(debug.contains("DomElement"));
528    }
529
530    // ===== DomEvent tests =====
531
532    #[test]
533    fn test_dom_event_click() {
534        let event = DomEvent::click("btn");
535        assert!(matches!(event, DomEvent::Click { element_id } if element_id == "btn"));
536    }
537
538    #[test]
539    fn test_dom_event_input() {
540        let event = DomEvent::input("field", "value");
541        assert!(
542            matches!(event, DomEvent::Input { element_id, value } if element_id == "field" && value == "value")
543        );
544    }
545
546    #[test]
547    fn test_dom_event_key_press() {
548        let event = DomEvent::key_press("Enter");
549        assert!(
550            matches!(event, DomEvent::KeyPress { key, ctrl, shift } if key == "Enter" && !ctrl && !shift)
551        );
552    }
553
554    #[test]
555    fn test_dom_event_key_press_with_modifiers() {
556        let event = DomEvent::key_press_with_modifiers("c", true, false);
557        assert!(
558            matches!(event, DomEvent::KeyPress { key, ctrl, shift } if key == "c" && ctrl && !shift)
559        );
560    }
561
562    #[test]
563    fn test_dom_event_focus() {
564        let event = DomEvent::focus("input");
565        assert!(matches!(event, DomEvent::Focus { element_id } if element_id == "input"));
566    }
567
568    #[test]
569    fn test_dom_event_blur() {
570        let event = DomEvent::blur("input");
571        assert!(matches!(event, DomEvent::Blur { element_id } if element_id == "input"));
572    }
573
574    #[test]
575    fn test_dom_event_submit() {
576        let event = DomEvent::submit("form");
577        assert!(matches!(event, DomEvent::Submit { element_id } if element_id == "form"));
578    }
579
580    #[test]
581    fn test_dom_event_clone() {
582        let event = DomEvent::click("btn");
583        let cloned = event.clone();
584        assert_eq!(event, cloned);
585    }
586
587    #[test]
588    fn test_dom_event_debug() {
589        let event = DomEvent::click("btn");
590        let debug = format!("{:?}", event);
591        assert!(debug.contains("Click"));
592    }
593
594    // ===== MockDom tests =====
595
596    #[test]
597    fn test_mock_dom_new() {
598        let dom = MockDom::new();
599        assert_eq!(dom.root.id, "root");
600        assert!(dom.event_history.is_empty());
601    }
602
603    #[test]
604    fn test_mock_dom_default() {
605        let dom = MockDom::default();
606        assert_eq!(dom.root.id, "root");
607    }
608
609    #[test]
610    fn test_mock_dom_calculator() {
611        let dom = MockDom::calculator();
612        assert!(dom.get_element("calc-input").is_some());
613        assert!(dom.get_element("calc-result").is_some());
614        assert!(dom.get_element("calc-history").is_some());
615        assert!(dom.get_element("calc-status").is_some());
616        assert!(dom.get_element("btn-clear").is_some());
617        assert!(dom.get_element("btn-equals").is_some());
618    }
619
620    #[test]
621    fn test_mock_dom_register_element() {
622        let mut dom = MockDom::new();
623        let elem = DomElement::new("span").with_id("test");
624        dom.register_element(elem);
625        assert!(dom.get_element("test").is_some());
626    }
627
628    #[test]
629    fn test_mock_dom_register_element_no_id() {
630        let mut dom = MockDom::new();
631        let elem = DomElement::new("span"); // no ID
632        let count_before = dom.elements.len();
633        dom.register_element(elem);
634        assert_eq!(dom.elements.len(), count_before); // should not be registered
635    }
636
637    #[test]
638    fn test_mock_dom_get_element_mut() {
639        let mut dom = MockDom::calculator();
640        if let Some(elem) = dom.get_element_mut("calc-result") {
641            elem.set_text("42");
642        }
643        assert_eq!(dom.get_element_text("calc-result"), Some("42"));
644    }
645
646    #[test]
647    fn test_mock_dom_dispatch_focus() {
648        let mut dom = MockDom::calculator();
649        dom.dispatch_event(DomEvent::focus("calc-input"));
650        assert_eq!(dom.focused_element(), Some("calc-input"));
651    }
652
653    #[test]
654    fn test_mock_dom_dispatch_blur() {
655        let mut dom = MockDom::calculator();
656        dom.dispatch_event(DomEvent::focus("calc-input"));
657        dom.dispatch_event(DomEvent::blur("calc-input"));
658        assert_eq!(dom.focused_element(), None);
659    }
660
661    #[test]
662    fn test_mock_dom_dispatch_input() {
663        let mut dom = MockDom::calculator();
664        dom.dispatch_event(DomEvent::input("calc-input", "2 + 2"));
665        let elem = dom.get_element("calc-input").unwrap();
666        assert_eq!(elem.text_content, "2 + 2");
667        assert_eq!(elem.get_attr("value"), Some("2 + 2"));
668    }
669
670    #[test]
671    fn test_mock_dom_dispatch_click() {
672        let mut dom = MockDom::calculator();
673        dom.dispatch_event(DomEvent::click("btn-equals"));
674        assert_eq!(dom.event_history().len(), 1);
675    }
676
677    #[test]
678    fn test_mock_dom_event_history() {
679        let mut dom = MockDom::calculator();
680        dom.dispatch_event(DomEvent::click("btn-clear"));
681        dom.dispatch_event(DomEvent::input("calc-input", "5"));
682        assert_eq!(dom.event_history().len(), 2);
683    }
684
685    #[test]
686    fn test_mock_dom_clear_event_history() {
687        let mut dom = MockDom::calculator();
688        dom.dispatch_event(DomEvent::click("btn-clear"));
689        dom.clear_event_history();
690        assert!(dom.event_history().is_empty());
691    }
692
693    #[test]
694    fn test_mock_dom_set_element_text() {
695        let mut dom = MockDom::calculator();
696        dom.set_element_text("calc-result", "100");
697        assert_eq!(dom.get_element_text("calc-result"), Some("100"));
698    }
699
700    #[test]
701    fn test_mock_dom_get_element_text_none() {
702        let dom = MockDom::new();
703        assert_eq!(dom.get_element_text("nonexistent"), None);
704    }
705
706    #[test]
707    fn test_mock_dom_append_child() {
708        let mut dom = MockDom::calculator();
709        let child = DomElement::new("li")
710            .with_id("history-item-1")
711            .with_text("2 + 2 = 4");
712        dom.append_child("calc-history", child);
713
714        assert!(dom.get_element("history-item-1").is_some());
715        let history = dom.get_element("calc-history").unwrap();
716        assert_eq!(history.children.len(), 1);
717    }
718
719    #[test]
720    fn test_mock_dom_append_child_no_id() {
721        let mut dom = MockDom::calculator();
722        let child = DomElement::new("li").with_text("item");
723        let elem_count_before = dom.elements.len();
724        dom.append_child("calc-history", child);
725        // Child without ID should not be added to elements map
726        assert_eq!(dom.elements.len(), elem_count_before);
727    }
728
729    #[test]
730    fn test_mock_dom_clear_children() {
731        let mut dom = MockDom::calculator();
732        let child1 = DomElement::new("li").with_id("item1");
733        let child2 = DomElement::new("li").with_id("item2");
734        dom.append_child("calc-history", child1);
735        dom.append_child("calc-history", child2);
736
737        dom.clear_children("calc-history");
738
739        assert!(dom.get_element("item1").is_none());
740        assert!(dom.get_element("item2").is_none());
741        let history = dom.get_element("calc-history").unwrap();
742        assert!(history.children.is_empty());
743    }
744
745    #[test]
746    fn test_mock_dom_debug() {
747        let dom = MockDom::new();
748        let debug = format!("{:?}", dom);
749        assert!(debug.contains("MockDom"));
750    }
751}