tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
1//! Certificación headless del bucle Tier 3 — sin GPU ni ventana.
2//!
3//! Carga el `.wasm` del contador (compilado a wasm32, versionado en assets/),
4//! lo corre vía [`WasmGuest`] y verifica el viaje redondo completo: el guest
5//! pinta un `WireNode`, el host lo lee, un evento cruza a `wasm_dispatch` con su
6//! `EventId` + `EventPayload`, el `Model` del guest muta y la nueva vista lo
7//! refleja. También materializa el `WireNode` a un `View<RunnerMsg>` real.
8
9use llimphi_wasm_runner::{wire_to_view, EventPayload, RunnerMsg, WasmGuest};
10
11const COUNTER_WASM: &[u8] = include_bytes!("../assets/counter.wasm");
12
13// El +1 es el primer control que registra el guest ⇒ EventId 0; reset ⇒ 1.
14const EV_INCREMENT: u32 = 0;
15const EV_RESET: u32 = 1;
16
17/// El texto del número es el del primer hijo del árbol raíz (col → [número, botones]).
18fn number(guest: &WasmGuest) -> String {
19 guest.view().children[0]
20 .text
21 .as_ref()
22 .expect("el nodo número tiene texto")
23 .content
24 .clone()
25}
26
27#[test]
28fn counter_round_trip() {
29 let mut guest = WasmGuest::load(COUNTER_WASM, 0).expect("carga el guest counter");
30
31 // Estado inicial.
32 assert_eq!(number(&guest), "0");
33
34 // Los botones cargan los EventId que el guest asignó.
35 let buttons = &guest.view().children[1].children;
36 assert_eq!(buttons.len(), 2, "fila con +1 y reset");
37 assert_eq!(buttons[0].on_click, Some(EV_INCREMENT));
38 assert_eq!(buttons[1].on_click, Some(EV_RESET));
39
40 // Un click en +1 cruza a wasm_dispatch y muta el Model del guest.
41 guest
42 .dispatch(EV_INCREMENT, EventPayload::Click)
43 .expect("dispatch increment");
44 assert_eq!(number(&guest), "1");
45
46 guest
47 .dispatch(EV_INCREMENT, EventPayload::Click)
48 .expect("dispatch increment");
49 assert_eq!(number(&guest), "2");
50
51 // Reset vuelve a 0.
52 guest
53 .dispatch(EV_RESET, EventPayload::Click)
54 .expect("dispatch reset");
55 assert_eq!(number(&guest), "0");
56}
57
58#[test]
59fn materializa_view_real() {
60 let guest = WasmGuest::load(COUNTER_WASM, 0).expect("carga el guest counter");
61
62 // El WireNode se convierte en un View<RunnerMsg> Llimphi de verdad: si el
63 // mapeo explota (estilos, colores, handlers), esto trap-ea.
64 let view: llimphi_ui::View<RunnerMsg> = wire_to_view(guest.view(), None, None);
65
66 // Raíz: columna con relleno y dos hijos (número + fila de botones).
67 assert_eq!(view.children.len(), 2);
68
69 // El botón +1 quedó con on_click = RunnerMsg::Event(0, Click).
70 let row = &view.children[1];
71 let inc = &row.children[0];
72 match inc.on_click.as_ref().expect("el botón +1 tiene on_click") {
73 RunnerMsg::Event(id, EventPayload::Click) => assert_eq!(*id, EV_INCREMENT),
74 other => panic!("on_click inesperado: {other:?}"),
75 }
76 assert!(inc.fill.is_some(), "el botón conserva su relleno");
77}