Skip to main content

aprender_test_showcase/wasm/
keypad.rs

1//! WASM Keypad for Calculator
2//!
3//! Probar: Visual feedback - Visual buttons for browser-based interaction
4//!
5//! This module provides keypad functionality for the WASM calculator,
6//! mirroring the TUI keypad for cross-platform consistency.
7
8use super::dom::{DomElement, MockDom};
9
10/// Actions that keypad buttons can perform (shared with TUI)
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum KeypadAction {
13    /// Insert a digit (0-9)
14    Digit(u8),
15    /// Insert a decimal point
16    Decimal,
17    /// Insert an operator
18    Operator(char),
19    /// Evaluate the expression
20    Equals,
21    /// Clear the input
22    Clear,
23    /// Open parenthesis
24    OpenParen,
25    /// Close parenthesis
26    CloseParen,
27}
28
29impl KeypadAction {
30    /// Returns the character to insert for this action
31    #[must_use]
32    pub fn to_char(&self) -> Option<char> {
33        match self {
34            KeypadAction::Digit(d) => char::from_digit(*d as u32, 10),
35            KeypadAction::Decimal => Some('.'),
36            KeypadAction::Operator(op) => Some(*op),
37            KeypadAction::OpenParen => Some('('),
38            KeypadAction::CloseParen => Some(')'),
39            KeypadAction::Equals | KeypadAction::Clear => None,
40        }
41    }
42
43    /// Returns the button label for this action
44    #[must_use]
45    pub fn label(&self) -> String {
46        match self {
47            KeypadAction::Digit(d) => d.to_string(),
48            KeypadAction::Decimal => ".".to_string(),
49            KeypadAction::Operator(op) => op.to_string(),
50            KeypadAction::Equals => "=".to_string(),
51            KeypadAction::Clear => "C".to_string(),
52            KeypadAction::OpenParen => "(".to_string(),
53            KeypadAction::CloseParen => ")".to_string(),
54        }
55    }
56}
57
58/// A single keypad button definition
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct KeypadButtonDef {
61    /// The action this button performs
62    pub action: KeypadAction,
63    /// The DOM element ID for this button
64    pub id: String,
65    /// Grid row (0-indexed)
66    pub row: usize,
67    /// Grid column (0-indexed)
68    pub col: usize,
69}
70
71impl KeypadButtonDef {
72    /// Creates a new button definition
73    #[must_use]
74    pub fn new(action: KeypadAction, row: usize, col: usize) -> Self {
75        let id = match action {
76            KeypadAction::Digit(d) => format!("btn-{}", d),
77            KeypadAction::Decimal => "btn-decimal".to_string(),
78            KeypadAction::Operator(op) => format!("btn-{}", op_name(op)),
79            KeypadAction::Equals => "btn-equals".to_string(),
80            KeypadAction::Clear => "btn-clear".to_string(),
81            KeypadAction::OpenParen => "btn-open-paren".to_string(),
82            KeypadAction::CloseParen => "btn-close-paren".to_string(),
83        };
84        Self {
85            action,
86            id,
87            row,
88            col,
89        }
90    }
91}
92
93/// Returns a name for an operator (for element IDs)
94fn op_name(op: char) -> &'static str {
95    match op {
96        '+' => "plus",
97        '-' => "minus",
98        '*' => "times",
99        '/' => "divide",
100        '^' => "power",
101        '%' => "mod",
102        _ => "op",
103    }
104}
105
106/// WASM Keypad layout definition
107/// Layout:
108/// ```text
109/// [ 7 ] [ 8 ] [ 9 ] [ / ]
110/// [ 4 ] [ 5 ] [ 6 ] [ * ]
111/// [ 1 ] [ 2 ] [ 3 ] [ - ]
112/// [ 0 ] [ . ] [ = ] [ + ]
113/// [ C ] [ ( ] [ ) ] [ ^ ]
114/// ```
115#[derive(Debug, Clone)]
116pub struct WasmKeypad {
117    /// Button definitions
118    buttons: Vec<KeypadButtonDef>,
119    /// Number of columns
120    cols: usize,
121    /// Number of rows
122    rows: usize,
123}
124
125impl Default for WasmKeypad {
126    fn default() -> Self {
127        Self::new()
128    }
129}
130
131impl WasmKeypad {
132    /// Creates a new standard calculator keypad
133    #[must_use]
134    pub fn new() -> Self {
135        let buttons = vec![
136            // Row 0: 7 8 9 /
137            KeypadButtonDef::new(KeypadAction::Digit(7), 0, 0),
138            KeypadButtonDef::new(KeypadAction::Digit(8), 0, 1),
139            KeypadButtonDef::new(KeypadAction::Digit(9), 0, 2),
140            KeypadButtonDef::new(KeypadAction::Operator('/'), 0, 3),
141            // Row 1: 4 5 6 *
142            KeypadButtonDef::new(KeypadAction::Digit(4), 1, 0),
143            KeypadButtonDef::new(KeypadAction::Digit(5), 1, 1),
144            KeypadButtonDef::new(KeypadAction::Digit(6), 1, 2),
145            KeypadButtonDef::new(KeypadAction::Operator('*'), 1, 3),
146            // Row 2: 1 2 3 -
147            KeypadButtonDef::new(KeypadAction::Digit(1), 2, 0),
148            KeypadButtonDef::new(KeypadAction::Digit(2), 2, 1),
149            KeypadButtonDef::new(KeypadAction::Digit(3), 2, 2),
150            KeypadButtonDef::new(KeypadAction::Operator('-'), 2, 3),
151            // Row 3: 0 . = +
152            KeypadButtonDef::new(KeypadAction::Digit(0), 3, 0),
153            KeypadButtonDef::new(KeypadAction::Decimal, 3, 1),
154            KeypadButtonDef::new(KeypadAction::Equals, 3, 2),
155            KeypadButtonDef::new(KeypadAction::Operator('+'), 3, 3),
156            // Row 4: C ( ) ^
157            KeypadButtonDef::new(KeypadAction::Clear, 4, 0),
158            KeypadButtonDef::new(KeypadAction::OpenParen, 4, 1),
159            KeypadButtonDef::new(KeypadAction::CloseParen, 4, 2),
160            KeypadButtonDef::new(KeypadAction::Operator('^'), 4, 3),
161        ];
162
163        Self {
164            buttons,
165            cols: 4,
166            rows: 5,
167        }
168    }
169
170    /// Returns the number of buttons
171    #[must_use]
172    pub fn button_count(&self) -> usize {
173        self.buttons.len()
174    }
175
176    /// Returns the grid dimensions (rows, cols)
177    #[must_use]
178    pub fn dimensions(&self) -> (usize, usize) {
179        (self.rows, self.cols)
180    }
181
182    /// Gets all button definitions
183    #[must_use]
184    pub fn buttons(&self) -> &[KeypadButtonDef] {
185        &self.buttons
186    }
187
188    /// Gets a button by index
189    #[must_use]
190    pub fn get_button(&self, index: usize) -> Option<&KeypadButtonDef> {
191        self.buttons.get(index)
192    }
193
194    /// Gets a button by row and column
195    #[must_use]
196    pub fn get_button_at(&self, row: usize, col: usize) -> Option<&KeypadButtonDef> {
197        if row < self.rows && col < self.cols {
198            self.buttons.get(row * self.cols + col)
199        } else {
200            None
201        }
202    }
203
204    /// Finds a button by element ID
205    #[must_use]
206    pub fn find_button_by_id(&self, id: &str) -> Option<&KeypadButtonDef> {
207        self.buttons.iter().find(|b| b.id == id)
208    }
209
210    /// Finds a button by the character it inserts
211    #[must_use]
212    pub fn find_button_by_char(&self, ch: char) -> Option<&KeypadButtonDef> {
213        self.buttons.iter().find(|b| b.action.to_char() == Some(ch))
214    }
215
216    /// Creates DOM elements for all keypad buttons
217    pub fn create_dom_elements(&self) -> Vec<DomElement> {
218        self.buttons
219            .iter()
220            .map(|btn| {
221                DomElement::new("button")
222                    .with_id(&btn.id)
223                    .with_text(&btn.action.label())
224                    .with_class("keypad-btn")
225                    .with_class(&format!("keypad-row-{}", btn.row))
226                    .with_class(&format!("keypad-col-{}", btn.col))
227                    .with_attr("data-action", &format!("{:?}", btn.action))
228            })
229            .collect()
230    }
231
232    /// Creates a keypad container element with all buttons
233    #[must_use]
234    pub fn create_keypad_element(&self) -> DomElement {
235        let mut keypad = DomElement::new("div")
236            .with_id("calc-keypad")
237            .with_class("keypad");
238
239        for btn_elem in self.create_dom_elements() {
240            keypad = keypad.with_child(btn_elem);
241        }
242
243        keypad
244    }
245
246    /// Processes a button click event and returns the action
247    #[must_use]
248    pub fn handle_click(&self, element_id: &str) -> Option<KeypadAction> {
249        self.find_button_by_id(element_id).map(|btn| btn.action)
250    }
251
252    /// Maps a keyboard key to a keypad action
253    #[must_use]
254    pub fn key_to_action(key: &str) -> Option<KeypadAction> {
255        match key {
256            "0" => Some(KeypadAction::Digit(0)),
257            "1" => Some(KeypadAction::Digit(1)),
258            "2" => Some(KeypadAction::Digit(2)),
259            "3" => Some(KeypadAction::Digit(3)),
260            "4" => Some(KeypadAction::Digit(4)),
261            "5" => Some(KeypadAction::Digit(5)),
262            "6" => Some(KeypadAction::Digit(6)),
263            "7" => Some(KeypadAction::Digit(7)),
264            "8" => Some(KeypadAction::Digit(8)),
265            "9" => Some(KeypadAction::Digit(9)),
266            "." => Some(KeypadAction::Decimal),
267            "+" => Some(KeypadAction::Operator('+')),
268            "-" => Some(KeypadAction::Operator('-')),
269            "*" => Some(KeypadAction::Operator('*')),
270            "/" => Some(KeypadAction::Operator('/')),
271            "^" => Some(KeypadAction::Operator('^')),
272            "%" => Some(KeypadAction::Operator('%')),
273            "(" => Some(KeypadAction::OpenParen),
274            ")" => Some(KeypadAction::CloseParen),
275            "Enter" | "=" => Some(KeypadAction::Equals),
276            "Escape" | "c" | "C" => Some(KeypadAction::Clear),
277            _ => None,
278        }
279    }
280}
281
282/// Extension trait for MockDom to add keypad
283pub trait MockDomKeypadExt {
284    /// Adds keypad to an existing calculator DOM
285    fn add_keypad(&mut self, keypad: &WasmKeypad);
286}
287
288impl MockDomKeypadExt for MockDom {
289    fn add_keypad(&mut self, keypad: &WasmKeypad) {
290        let keypad_elem = keypad.create_keypad_element();
291
292        // Register keypad container
293        self.register_element(keypad_elem);
294
295        // Register individual buttons
296        for btn_def in keypad.buttons() {
297            let btn_elem = DomElement::new("button")
298                .with_id(&btn_def.id)
299                .with_text(&btn_def.action.label())
300                .with_class("keypad-btn");
301            self.register_element(btn_elem);
302        }
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    // ===== KeypadAction tests =====
311
312    #[test]
313    fn test_keypad_action_digit_to_char() {
314        for d in 0..=9 {
315            let action = KeypadAction::Digit(d);
316            assert_eq!(action.to_char(), char::from_digit(d as u32, 10));
317        }
318    }
319
320    #[test]
321    fn test_keypad_action_operator_to_char() {
322        for op in ['+', '-', '*', '/', '^'] {
323            let action = KeypadAction::Operator(op);
324            assert_eq!(action.to_char(), Some(op));
325        }
326    }
327
328    #[test]
329    fn test_keypad_action_decimal_to_char() {
330        assert_eq!(KeypadAction::Decimal.to_char(), Some('.'));
331    }
332
333    #[test]
334    fn test_keypad_action_parens_to_char() {
335        assert_eq!(KeypadAction::OpenParen.to_char(), Some('('));
336        assert_eq!(KeypadAction::CloseParen.to_char(), Some(')'));
337    }
338
339    #[test]
340    fn test_keypad_action_equals_clear_no_char() {
341        assert_eq!(KeypadAction::Equals.to_char(), None);
342        assert_eq!(KeypadAction::Clear.to_char(), None);
343    }
344
345    #[test]
346    fn test_keypad_action_label() {
347        assert_eq!(KeypadAction::Digit(5).label(), "5");
348        assert_eq!(KeypadAction::Decimal.label(), ".");
349        assert_eq!(KeypadAction::Operator('+').label(), "+");
350        assert_eq!(KeypadAction::Equals.label(), "=");
351        assert_eq!(KeypadAction::Clear.label(), "C");
352        assert_eq!(KeypadAction::OpenParen.label(), "(");
353        assert_eq!(KeypadAction::CloseParen.label(), ")");
354    }
355
356    #[test]
357    fn test_keypad_action_copy() {
358        let action = KeypadAction::Digit(5);
359        let copied = action;
360        assert_eq!(action, copied);
361    }
362
363    #[test]
364    fn test_keypad_action_debug() {
365        let action = KeypadAction::Operator('+');
366        let debug = format!("{:?}", action);
367        assert!(debug.contains("Operator"));
368    }
369
370    // ===== KeypadButtonDef tests =====
371
372    #[test]
373    fn test_button_def_new_digit() {
374        let btn = KeypadButtonDef::new(KeypadAction::Digit(5), 1, 1);
375        assert_eq!(btn.id, "btn-5");
376        assert_eq!(btn.row, 1);
377        assert_eq!(btn.col, 1);
378    }
379
380    #[test]
381    fn test_button_def_new_operators() {
382        let btn_plus = KeypadButtonDef::new(KeypadAction::Operator('+'), 3, 3);
383        assert_eq!(btn_plus.id, "btn-plus");
384
385        let btn_minus = KeypadButtonDef::new(KeypadAction::Operator('-'), 2, 3);
386        assert_eq!(btn_minus.id, "btn-minus");
387
388        let btn_times = KeypadButtonDef::new(KeypadAction::Operator('*'), 1, 3);
389        assert_eq!(btn_times.id, "btn-times");
390
391        let btn_divide = KeypadButtonDef::new(KeypadAction::Operator('/'), 0, 3);
392        assert_eq!(btn_divide.id, "btn-divide");
393
394        let btn_power = KeypadButtonDef::new(KeypadAction::Operator('^'), 4, 3);
395        assert_eq!(btn_power.id, "btn-power");
396    }
397
398    #[test]
399    fn test_button_def_new_special() {
400        let decimal = KeypadButtonDef::new(KeypadAction::Decimal, 3, 1);
401        assert_eq!(decimal.id, "btn-decimal");
402
403        let equals = KeypadButtonDef::new(KeypadAction::Equals, 3, 2);
404        assert_eq!(equals.id, "btn-equals");
405
406        let clear = KeypadButtonDef::new(KeypadAction::Clear, 4, 0);
407        assert_eq!(clear.id, "btn-clear");
408
409        let open = KeypadButtonDef::new(KeypadAction::OpenParen, 4, 1);
410        assert_eq!(open.id, "btn-open-paren");
411
412        let close = KeypadButtonDef::new(KeypadAction::CloseParen, 4, 2);
413        assert_eq!(close.id, "btn-close-paren");
414    }
415
416    #[test]
417    fn test_button_def_clone() {
418        let btn = KeypadButtonDef::new(KeypadAction::Digit(7), 0, 0);
419        let cloned = btn.clone();
420        assert_eq!(btn, cloned);
421    }
422
423    #[test]
424    fn test_button_def_debug() {
425        let btn = KeypadButtonDef::new(KeypadAction::Digit(9), 0, 2);
426        let debug = format!("{:?}", btn);
427        assert!(debug.contains("KeypadButtonDef"));
428    }
429
430    // ===== op_name tests =====
431
432    #[test]
433    fn test_op_name_all() {
434        assert_eq!(op_name('+'), "plus");
435        assert_eq!(op_name('-'), "minus");
436        assert_eq!(op_name('*'), "times");
437        assert_eq!(op_name('/'), "divide");
438        assert_eq!(op_name('^'), "power");
439        assert_eq!(op_name('%'), "mod");
440        assert_eq!(op_name('?'), "op"); // unknown
441    }
442
443    // ===== WasmKeypad tests =====
444
445    #[test]
446    fn test_wasm_keypad_new() {
447        let keypad = WasmKeypad::new();
448        assert_eq!(keypad.button_count(), 20);
449    }
450
451    #[test]
452    fn test_wasm_keypad_default() {
453        let keypad = WasmKeypad::default();
454        assert_eq!(keypad.button_count(), 20);
455    }
456
457    #[test]
458    fn test_wasm_keypad_dimensions() {
459        let keypad = WasmKeypad::new();
460        assert_eq!(keypad.dimensions(), (5, 4));
461    }
462
463    #[test]
464    fn test_wasm_keypad_get_button() {
465        let keypad = WasmKeypad::new();
466        let btn = keypad.get_button(0).unwrap();
467        assert_eq!(btn.action, KeypadAction::Digit(7));
468    }
469
470    #[test]
471    fn test_wasm_keypad_get_button_out_of_bounds() {
472        let keypad = WasmKeypad::new();
473        assert!(keypad.get_button(100).is_none());
474    }
475
476    #[test]
477    fn test_wasm_keypad_get_button_at() {
478        let keypad = WasmKeypad::new();
479        // Row 0, Col 0 = 7
480        assert_eq!(
481            keypad.get_button_at(0, 0).unwrap().action,
482            KeypadAction::Digit(7)
483        );
484        // Row 0, Col 3 = /
485        assert_eq!(
486            keypad.get_button_at(0, 3).unwrap().action,
487            KeypadAction::Operator('/')
488        );
489        // Row 4, Col 0 = C
490        assert_eq!(
491            keypad.get_button_at(4, 0).unwrap().action,
492            KeypadAction::Clear
493        );
494    }
495
496    #[test]
497    fn test_wasm_keypad_get_button_at_out_of_bounds() {
498        let keypad = WasmKeypad::new();
499        assert!(keypad.get_button_at(10, 10).is_none());
500    }
501
502    #[test]
503    fn test_wasm_keypad_get_button_at_boundary_row() {
504        let keypad = WasmKeypad::new();
505        // Row 4 is valid (last row), row 5 is not
506        assert!(keypad.get_button_at(4, 0).is_some());
507        assert!(keypad.get_button_at(5, 0).is_none());
508    }
509
510    #[test]
511    fn test_wasm_keypad_get_button_at_boundary_col() {
512        let keypad = WasmKeypad::new();
513        // Col 3 is valid (last col), col 4 is not
514        assert!(keypad.get_button_at(0, 3).is_some());
515        assert!(keypad.get_button_at(0, 4).is_none());
516    }
517
518    #[test]
519    fn test_wasm_keypad_get_button_at_exact_boundary() {
520        let keypad = WasmKeypad::new();
521        // Test exact boundary: row == rows should fail (5 == 5)
522        // but col is valid (< 4)
523        assert!(keypad.get_button_at(5, 0).is_none());
524        assert!(keypad.get_button_at(5, 1).is_none());
525        assert!(keypad.get_button_at(5, 2).is_none());
526        assert!(keypad.get_button_at(5, 3).is_none());
527    }
528
529    #[test]
530    fn test_wasm_keypad_find_by_id() {
531        let keypad = WasmKeypad::new();
532        let btn = keypad.find_button_by_id("btn-5").unwrap();
533        assert_eq!(btn.action, KeypadAction::Digit(5));
534    }
535
536    #[test]
537    fn test_wasm_keypad_find_by_id_not_found() {
538        let keypad = WasmKeypad::new();
539        assert!(keypad.find_button_by_id("nonexistent").is_none());
540    }
541
542    #[test]
543    fn test_wasm_keypad_find_by_char() {
544        let keypad = WasmKeypad::new();
545        let btn = keypad.find_button_by_char('+').unwrap();
546        assert_eq!(btn.action, KeypadAction::Operator('+'));
547    }
548
549    #[test]
550    fn test_wasm_keypad_find_by_char_not_found() {
551        let keypad = WasmKeypad::new();
552        assert!(keypad.find_button_by_char('?').is_none());
553    }
554
555    // ===== Keypad layout verification =====
556
557    #[test]
558    fn test_keypad_row_0() {
559        let keypad = WasmKeypad::new();
560        assert_eq!(
561            keypad.get_button_at(0, 0).unwrap().action,
562            KeypadAction::Digit(7)
563        );
564        assert_eq!(
565            keypad.get_button_at(0, 1).unwrap().action,
566            KeypadAction::Digit(8)
567        );
568        assert_eq!(
569            keypad.get_button_at(0, 2).unwrap().action,
570            KeypadAction::Digit(9)
571        );
572        assert_eq!(
573            keypad.get_button_at(0, 3).unwrap().action,
574            KeypadAction::Operator('/')
575        );
576    }
577
578    #[test]
579    fn test_keypad_row_1() {
580        let keypad = WasmKeypad::new();
581        assert_eq!(
582            keypad.get_button_at(1, 0).unwrap().action,
583            KeypadAction::Digit(4)
584        );
585        assert_eq!(
586            keypad.get_button_at(1, 1).unwrap().action,
587            KeypadAction::Digit(5)
588        );
589        assert_eq!(
590            keypad.get_button_at(1, 2).unwrap().action,
591            KeypadAction::Digit(6)
592        );
593        assert_eq!(
594            keypad.get_button_at(1, 3).unwrap().action,
595            KeypadAction::Operator('*')
596        );
597    }
598
599    #[test]
600    fn test_keypad_row_2() {
601        let keypad = WasmKeypad::new();
602        assert_eq!(
603            keypad.get_button_at(2, 0).unwrap().action,
604            KeypadAction::Digit(1)
605        );
606        assert_eq!(
607            keypad.get_button_at(2, 1).unwrap().action,
608            KeypadAction::Digit(2)
609        );
610        assert_eq!(
611            keypad.get_button_at(2, 2).unwrap().action,
612            KeypadAction::Digit(3)
613        );
614        assert_eq!(
615            keypad.get_button_at(2, 3).unwrap().action,
616            KeypadAction::Operator('-')
617        );
618    }
619
620    #[test]
621    fn test_keypad_row_3() {
622        let keypad = WasmKeypad::new();
623        assert_eq!(
624            keypad.get_button_at(3, 0).unwrap().action,
625            KeypadAction::Digit(0)
626        );
627        assert_eq!(
628            keypad.get_button_at(3, 1).unwrap().action,
629            KeypadAction::Decimal
630        );
631        assert_eq!(
632            keypad.get_button_at(3, 2).unwrap().action,
633            KeypadAction::Equals
634        );
635        assert_eq!(
636            keypad.get_button_at(3, 3).unwrap().action,
637            KeypadAction::Operator('+')
638        );
639    }
640
641    #[test]
642    fn test_keypad_row_4() {
643        let keypad = WasmKeypad::new();
644        assert_eq!(
645            keypad.get_button_at(4, 0).unwrap().action,
646            KeypadAction::Clear
647        );
648        assert_eq!(
649            keypad.get_button_at(4, 1).unwrap().action,
650            KeypadAction::OpenParen
651        );
652        assert_eq!(
653            keypad.get_button_at(4, 2).unwrap().action,
654            KeypadAction::CloseParen
655        );
656        assert_eq!(
657            keypad.get_button_at(4, 3).unwrap().action,
658            KeypadAction::Operator('^')
659        );
660    }
661
662    // ===== DOM integration tests =====
663
664    #[test]
665    fn test_create_dom_elements() {
666        let keypad = WasmKeypad::new();
667        let elements = keypad.create_dom_elements();
668        assert_eq!(elements.len(), 20);
669
670        // Check first element
671        assert_eq!(elements[0].id, "btn-7");
672        assert_eq!(elements[0].tag, "button");
673        assert!(elements[0].has_class("keypad-btn"));
674    }
675
676    #[test]
677    fn test_create_keypad_element() {
678        let keypad = WasmKeypad::new();
679        let elem = keypad.create_keypad_element();
680        assert_eq!(elem.id, "calc-keypad");
681        assert!(elem.has_class("keypad"));
682        assert_eq!(elem.children.len(), 20);
683    }
684
685    #[test]
686    fn test_handle_click_digit() {
687        let keypad = WasmKeypad::new();
688        let action = keypad.handle_click("btn-5");
689        assert_eq!(action, Some(KeypadAction::Digit(5)));
690    }
691
692    #[test]
693    fn test_handle_click_operator() {
694        let keypad = WasmKeypad::new();
695        let action = keypad.handle_click("btn-plus");
696        assert_eq!(action, Some(KeypadAction::Operator('+')));
697    }
698
699    #[test]
700    fn test_handle_click_equals() {
701        let keypad = WasmKeypad::new();
702        let action = keypad.handle_click("btn-equals");
703        assert_eq!(action, Some(KeypadAction::Equals));
704    }
705
706    #[test]
707    fn test_handle_click_clear() {
708        let keypad = WasmKeypad::new();
709        let action = keypad.handle_click("btn-clear");
710        assert_eq!(action, Some(KeypadAction::Clear));
711    }
712
713    #[test]
714    fn test_handle_click_unknown() {
715        let keypad = WasmKeypad::new();
716        let action = keypad.handle_click("nonexistent");
717        assert_eq!(action, None);
718    }
719
720    // ===== Keyboard mapping tests =====
721
722    #[test]
723    fn test_key_to_action_digits() {
724        for d in '0'..='9' {
725            let action = WasmKeypad::key_to_action(&d.to_string());
726            let expected = d.to_digit(10).unwrap() as u8;
727            assert_eq!(action, Some(KeypadAction::Digit(expected)));
728        }
729    }
730
731    #[test]
732    fn test_key_to_action_operators() {
733        assert_eq!(
734            WasmKeypad::key_to_action("+"),
735            Some(KeypadAction::Operator('+'))
736        );
737        assert_eq!(
738            WasmKeypad::key_to_action("-"),
739            Some(KeypadAction::Operator('-'))
740        );
741        assert_eq!(
742            WasmKeypad::key_to_action("*"),
743            Some(KeypadAction::Operator('*'))
744        );
745        assert_eq!(
746            WasmKeypad::key_to_action("/"),
747            Some(KeypadAction::Operator('/'))
748        );
749        assert_eq!(
750            WasmKeypad::key_to_action("^"),
751            Some(KeypadAction::Operator('^'))
752        );
753        assert_eq!(
754            WasmKeypad::key_to_action("%"),
755            Some(KeypadAction::Operator('%'))
756        );
757    }
758
759    #[test]
760    fn test_key_to_action_special() {
761        assert_eq!(WasmKeypad::key_to_action("."), Some(KeypadAction::Decimal));
762        assert_eq!(
763            WasmKeypad::key_to_action("("),
764            Some(KeypadAction::OpenParen)
765        );
766        assert_eq!(
767            WasmKeypad::key_to_action(")"),
768            Some(KeypadAction::CloseParen)
769        );
770    }
771
772    #[test]
773    fn test_key_to_action_equals() {
774        assert_eq!(
775            WasmKeypad::key_to_action("Enter"),
776            Some(KeypadAction::Equals)
777        );
778        assert_eq!(WasmKeypad::key_to_action("="), Some(KeypadAction::Equals));
779    }
780
781    #[test]
782    fn test_key_to_action_clear() {
783        assert_eq!(
784            WasmKeypad::key_to_action("Escape"),
785            Some(KeypadAction::Clear)
786        );
787        assert_eq!(WasmKeypad::key_to_action("c"), Some(KeypadAction::Clear));
788        assert_eq!(WasmKeypad::key_to_action("C"), Some(KeypadAction::Clear));
789    }
790
791    #[test]
792    fn test_key_to_action_unknown() {
793        assert_eq!(WasmKeypad::key_to_action("x"), None);
794        assert_eq!(WasmKeypad::key_to_action("Shift"), None);
795    }
796
797    // ===== MockDom extension tests =====
798
799    #[test]
800    fn test_mock_dom_add_keypad() {
801        let mut dom = MockDom::calculator();
802        let keypad = WasmKeypad::new();
803        dom.add_keypad(&keypad);
804
805        // Verify keypad container exists
806        assert!(dom.get_element("calc-keypad").is_some());
807
808        // Verify individual buttons exist
809        assert!(dom.get_element("btn-5").is_some());
810        assert!(dom.get_element("btn-plus").is_some());
811        assert!(dom.get_element("btn-equals").is_some());
812        assert!(dom.get_element("btn-clear").is_some());
813    }
814
815    #[test]
816    fn test_keypad_clone() {
817        let keypad = WasmKeypad::new();
818        let cloned = keypad.clone();
819        assert_eq!(keypad.button_count(), cloned.button_count());
820    }
821
822    #[test]
823    fn test_keypad_debug() {
824        let keypad = WasmKeypad::new();
825        let debug = format!("{:?}", keypad);
826        assert!(debug.contains("WasmKeypad"));
827    }
828
829    // ===== Property-based tests =====
830
831    #[test]
832    fn prop_all_digits_have_buttons() {
833        let keypad = WasmKeypad::new();
834        for d in 0..=9 {
835            let ch = char::from_digit(d, 10).unwrap();
836            assert!(
837                keypad.find_button_by_char(ch).is_some(),
838                "Missing button for digit {d}"
839            );
840        }
841    }
842
843    #[test]
844    fn prop_all_operators_have_buttons() {
845        let keypad = WasmKeypad::new();
846        for op in ['+', '-', '*', '/', '^'] {
847            assert!(
848                keypad.find_button_by_char(op).is_some(),
849                "Missing button for operator {op}"
850            );
851        }
852    }
853
854    #[test]
855    fn prop_button_positions_unique() {
856        let keypad = WasmKeypad::new();
857        let mut positions = std::collections::HashSet::new();
858        for btn in keypad.buttons() {
859            let pos = (btn.row, btn.col);
860            assert!(positions.insert(pos), "Duplicate position {:?}", pos);
861        }
862    }
863
864    #[test]
865    fn prop_button_ids_unique() {
866        let keypad = WasmKeypad::new();
867        let mut ids = std::collections::HashSet::new();
868        for btn in keypad.buttons() {
869            assert!(ids.insert(btn.id.clone()), "Duplicate ID {}", btn.id);
870        }
871    }
872}