tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
1//! Certifica los eventos con payload (Text/Toggle/Value/Select) headless, con el
2//! guest del formulario, más la edición value-driven y el render de los widgets.
3
4use llimphi_ui::{Key, NamedKey};
5use llimphi_wasm_runner::{edit_value, EventPayload, RunnerMsg, WasmGuest};
6
7const FORM_WASM: &[u8] = include_bytes!("../assets/form.wasm");
8
9// Orden de controles en Form::view (= orden de registro = EventId):
10// text_input(0), checkbox(1), slider(2), select(3), multiline(4), radio(5), button(6).
11const EV_NOMBRE: u32 = 0;
12const EV_MAYUS: u32 = 1;
13const EV_EDAD: u32 = 2;
14const EV_COLOR: u32 = 3;
15const EV_NOTAS: u32 = 4;
16const EV_PRIORIDAD: u32 = 5;
17const EV_LIMPIAR: u32 = 6;
18
19// col = [titulo, campo, check, edad_row, color, saludo, detalle, notas, prioridad, limpiar].
20fn campo(g: &WasmGuest) -> String {
21 g.view().children[1].input.as_ref().unwrap().value.clone()
22}
23fn saludo(g: &WasmGuest) -> String {
24 g.view().children[5].text.as_ref().unwrap().content.clone()
25}
26fn detalle(g: &WasmGuest) -> String {
27 g.view().children[6].text.as_ref().unwrap().content.clone()
28}
29
30#[test]
31fn form_text_toggle_y_click() {
32 let mut g = WasmGuest::load(FORM_WASM, 0).expect("carga el form");
33 assert_eq!(saludo(&g), "Hola, …");
34 assert_eq!(campo(&g), "");
35
36 g.dispatch(EV_NOMBRE, EventPayload::Text("ana".into())).unwrap();
37 assert_eq!(campo(&g), "ana");
38 assert_eq!(saludo(&g), "Hola, ana!");
39
40 g.dispatch(EV_MAYUS, EventPayload::Toggle(true)).unwrap();
41 assert_eq!(saludo(&g), "HOLA, ANA!");
42
43 g.dispatch(EV_LIMPIAR, EventPayload::Click).unwrap();
44 assert_eq!(campo(&g), "");
45 assert_eq!(saludo(&g), "Hola, …");
46}
47
48#[test]
49fn form_slider_y_dropdown() {
50 let mut g = WasmGuest::load(FORM_WASM, 0).expect("carga el form");
51 // Defaults: edad 30, color 1 (verde).
52 assert!(detalle(&g).contains("Edad: 30"), "{}", detalle(&g));
53 assert!(detalle(&g).contains("Color: verde"), "{}", detalle(&g));
54
55 // Evento Value: el guest reconstruye Msg::SetEdad(75.0).
56 g.dispatch(EV_EDAD, EventPayload::Value(75.0)).unwrap();
57 assert!(detalle(&g).contains("Edad: 75"), "{}", detalle(&g));
58
59 // Evento Select: Msg::SetColor(0) ⇒ "rojo".
60 g.dispatch(EV_COLOR, EventPayload::Select(0)).unwrap();
61 assert!(detalle(&g).contains("Color: rojo"), "{}", detalle(&g));
62}
63
64#[test]
65fn dropdown_abre_y_lista_opciones() {
66 let mut g = WasmGuest::load(FORM_WASM, 0).expect("carga el form");
67 // Cerrado: el select (children[4]) materializa sólo su header.
68 assert_eq!(g.render().children[4].children.len(), 1, "cerrado: sólo header");
69 // Abrir: header + las 3 opciones.
70 g.apply(&RunnerMsg::ToggleSelect(EV_COLOR)).unwrap();
71 assert_eq!(
72 g.render().children[4].children.len(),
73 1 + 3,
74 "abierto: header + 3 opciones"
75 );
76 // Cualquier evento cierra el dropdown.
77 g.apply(&RunnerMsg::Event(EV_COLOR, EventPayload::Select(2))).unwrap();
78 assert_eq!(g.render().children[4].children.len(), 1, "elegir cierra");
79}
80
81#[test]
82fn form_multiline_y_radio_cruzan_la_frontera() {
83 let mut g = WasmGuest::load(FORM_WASM, 0).expect("carga el form");
84 // Defaults: prioridad baja, notas vacías (1 línea por count de "").
85 assert!(detalle(&g).contains("Prioridad: baja"), "{}", detalle(&g));
86
87 // Notas multilínea: el host ya computó el texto con \n (edit_value) y el
88 // guest lo almacena; detalle refleja el conteo de líneas.
89 g.dispatch(EV_NOTAS, EventPayload::Text("uno\ndos\ntres".into()))
90 .unwrap();
91 assert!(detalle(&g).contains("Notas: 3 líneas"), "{}", detalle(&g));
92 // El input multilínea está marcado como tal en la vista.
93 assert!(
94 g.view().children[7].input.as_ref().unwrap().multiline,
95 "notas es multiline"
96 );
97
98 // Radio: elegir prioridad alta (índice 2) por Select.
99 g.dispatch(EV_PRIORIDAD, EventPayload::Select(2)).unwrap();
100 assert!(detalle(&g).contains("Prioridad: alta"), "{}", detalle(&g));
101 // El nodo radio (children[8]) materializa una fila por opción.
102 assert_eq!(g.render().children[8].children.len(), 3, "3 opciones de radio");
103}
104
105#[test]
106fn radio_se_materializa_con_una_marcada() {
107 use llimphi_wasm_runner::wire_to_view;
108 use llimphi_wire_view::{WireNode, WireRadio};
109
110 // Grupo de 3 opciones, la 1 (índice) marcada, handler on_radio = 9.
111 let node = WireNode::new()
112 .with_radio(WireRadio {
113 options: vec!["rojo".into(), "verde".into(), "azul".into()],
114 selected: 1,
115 })
116 .on_radio(9);
117 let v = wire_to_view(&node, None, None);
118
119 assert_eq!(v.children.len(), 3, "una fila por opción");
120 // Sólo la marcada lleva ◉; las otras ○.
121 let marcadas: Vec<String> = v
122 .children
123 .iter()
124 .map(|c| c.text.as_ref().unwrap().content.clone())
125 .collect();
126 assert!(marcadas[0].starts_with('\u{25cb}'), "rojo: ○ ({})", marcadas[0]);
127 assert!(marcadas[1].starts_with('\u{25c9}'), "verde: ◉ ({})", marcadas[1]);
128 assert!(marcadas[2].starts_with('\u{25cb}'), "azul: ○ ({})", marcadas[2]);
129 assert!(marcadas[1].contains("verde"));
130 // Cada fila es clickable (emite el Select de su índice).
131 assert!(v.children[2].on_click.is_some(), "la opción azul es clickable");
132}
133
134#[test]
135fn slider_se_materializa_con_barra() {
136 let g = WasmGuest::load(FORM_WASM, 0).expect("carga el form");
137 let v = g.render();
138 // edad_row = children[3]; el slider es su 2º hijo.
139 let slider = &v.children[3].children[1];
140 assert!(llimphi_ui::raro!(slider, on_click_at).is_some(), "el slider responde a click posicional");
141 assert_eq!(slider.children.len(), 1, "track con barra de relleno");
142}
143
144#[test]
145fn input_se_materializa_focusable() {
146 let g = WasmGuest::load(FORM_WASM, 0).expect("carga el form");
147 assert!(llimphi_ui::raro!(g.render().children[1], focusable).is_some(), "el input es focusable");
148}
149
150#[test]
151fn edit_value_anexa_y_borra() {
152 assert_eq!(
153 edit_value("an", &Key::Character("a".into()), Some("a"), false),
154 Some("ana".into())
155 );
156 assert_eq!(
157 edit_value("ana", &Key::Named(NamedKey::Backspace), None, false),
158 Some("an".into())
159 );
160 // Enter en un campo de una línea no edita; en uno multilínea inserta \n.
161 assert_eq!(edit_value("x", &Key::Named(NamedKey::Enter), None, false), None);
162 assert_eq!(
163 edit_value("x", &Key::Named(NamedKey::Enter), None, true),
164 Some("x\n".into())
165 );
166}