tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 238TOML 102Markdown 83Shell 24178 Sutichaykuna

⎇ main llimphi-wasm-runner/tests/form.rs Rust · 166 siq'ikuna · 6.3 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · Sergio ·

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}