showcase_calculator/wasm/
dom.rs1use std::collections::HashMap;
9
10#[derive(Debug, Clone, PartialEq)]
12pub struct DomElement {
13 pub id: String,
15 pub tag: String,
17 pub text_content: String,
19 pub attributes: HashMap<String, String>,
21 pub classes: Vec<String>,
23 pub visible: bool,
25 pub children: Vec<DomElement>,
27}
28
29impl Default for DomElement {
30 fn default() -> Self {
31 Self::new("div")
32 }
33}
34
35impl DomElement {
36 #[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 #[must_use]
52 pub fn with_id(mut self, id: &str) -> Self {
53 self.id = id.to_string();
54 self
55 }
56
57 #[must_use]
59 pub fn with_text(mut self, text: &str) -> Self {
60 self.text_content = text.to_string();
61 self
62 }
63
64 #[must_use]
66 pub fn with_class(mut self, class: &str) -> Self {
67 self.classes.push(class.to_string());
68 self
69 }
70
71 #[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 #[must_use]
80 pub fn with_child(mut self, child: DomElement) -> Self {
81 self.children.push(child);
82 self
83 }
84
85 pub fn set_visible(&mut self, visible: bool) {
87 self.visible = visible;
88 }
89
90 pub fn set_text(&mut self, text: &str) {
92 self.text_content = text.to_string();
93 }
94
95 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 pub fn remove_class(&mut self, class: &str) {
104 self.classes.retain(|c| c != class);
105 }
106
107 #[must_use]
109 pub fn has_class(&self, class: &str) -> bool {
110 self.classes.contains(&class.to_string())
111 }
112
113 #[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#[derive(Debug, Clone, PartialEq, Eq)]
122pub enum DomEvent {
123 Click {
125 element_id: String,
127 },
128 Input {
130 element_id: String,
132 value: String,
134 },
135 KeyPress {
137 key: String,
139 ctrl: bool,
141 shift: bool,
143 },
144 Focus {
146 element_id: String,
148 },
149 Blur {
151 element_id: String,
153 },
154 Submit {
156 element_id: String,
158 },
159}
160
161impl DomEvent {
162 #[must_use]
164 pub fn click(element_id: &str) -> Self {
165 Self::Click {
166 element_id: element_id.to_string(),
167 }
168 }
169
170 #[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 #[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 #[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 #[must_use]
201 pub fn focus(element_id: &str) -> Self {
202 Self::Focus {
203 element_id: element_id.to_string(),
204 }
205 }
206
207 #[must_use]
209 pub fn blur(element_id: &str) -> Self {
210 Self::Blur {
211 element_id: element_id.to_string(),
212 }
213 }
214
215 #[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#[derive(Debug)]
226pub struct MockDom {
227 pub root: DomElement,
229 elements: HashMap<String, DomElement>,
231 event_history: Vec<DomEvent>,
233 focused_element: Option<String>,
235}
236
237impl Default for MockDom {
238 fn default() -> Self {
239 Self::new()
240 }
241}
242
243impl MockDom {
244 #[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 #[must_use]
257 pub fn calculator() -> Self {
258 let mut dom = Self::new();
259
260 let input = DomElement::new("input")
262 .with_id("calc-input")
263 .with_attr("type", "text")
264 .with_attr("placeholder", "Enter expression");
265
266 let result = DomElement::new("div")
268 .with_id("calc-result")
269 .with_class("result-display");
270
271 let history = DomElement::new("ul")
273 .with_id("calc-history")
274 .with_class("history-list");
275
276 let status = DomElement::new("div")
278 .with_id("calc-status")
279 .with_class("status-display");
280
281 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 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 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 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 #[must_use]
321 pub fn get_element(&self, id: &str) -> Option<&DomElement> {
322 self.elements.get(id)
323 }
324
325 pub fn get_element_mut(&mut self, id: &str) -> Option<&mut DomElement> {
327 self.elements.get_mut(id)
328 }
329
330 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 #[must_use]
353 pub fn event_history(&self) -> &[DomEvent] {
354 &self.event_history
355 }
356
357 pub fn clear_event_history(&mut self) {
359 self.event_history.clear();
360 }
361
362 #[must_use]
364 pub fn focused_element(&self) -> Option<&str> {
365 self.focused_element.as_deref()
366 }
367
368 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 #[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 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 pub fn clear_children(&mut self, id: &str) {
394 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 for child_id in child_ids {
409 self.elements.remove(&child_id);
410 }
411
412 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 #[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"); 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 #[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 #[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"); let count_before = dom.elements.len();
633 dom.register_element(elem);
634 assert_eq!(dom.elements.len(), count_before); }
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 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}