tawasuyu

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Definiciones

⎇ main widgets/form/src/lib.rs Rust · 654 líneas · 24.2 KB Historia Quién tocó qué crudo

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

1//! `llimphi-widget-form` — validación de formularios sobre
2//! [`llimphi_widget_field`].
3//!
4//! Un formulario valida en tres tiempos, y el que se hace mal es el tercero:
5//!
6//! 1. **qué se pide** — las reglas por campo ([`Regla`], [`CampoDef`]);
7//! 2. **qué está mal** — la agregación ([`Formulario::validar`] →
8//!    [`Errores`]), que junta **todos** los problemas y no sólo el primero;
9//! 3. **cuándo se dice** — [`EstadoForm`]. Un formulario que grita «obligatorio»
10//!    contra un campo vacío que todavía no tocaste es hostil, y uno que se
11//!    guarda el error hasta el submit te hace ir y volver. La regla que
12//!    implementa esto es la de siempre: se muestra al **salir** del campo
13//!    (`tocar`) o al **intentar enviar** (`enviar`), nunca mientras se escribe
14//!    por primera vez.
15//!
16//! Es lógica pura + dos vistas de pegamento. El estado —los valores— sigue
17//! viviendo en el `Model` del caller: acá no hay un `Form` con buffers adentro,
18//! porque eso obligaría a sincronizar dos fuentes de verdad.
19//!
20//! ```ignore
21//! // una vez, en el Model o como constante:
22//! let form = Formulario::new(vec![
23//!     CampoDef::new(Id::Nombre, "Nombre").regla(Regla::Requerido),
24//!     CampoDef::new(Id::Correo, "Correo").reglas(vec![Regla::Requerido, Regla::Correo]),
25//!     CampoDef::new(Id::Edad, "Edad").regla(Regla::Rango(18.0, 120.0)),
26//! ]);
27//!
28//! // en la vista:
29//! let errores = form.validar(&[(Id::Nombre, &m.nombre), (Id::Correo, &m.correo),
30//!                              (Id::Edad, &m.edad)]);
31//! let visibles = m.estado.visibles(&errores);
32//! campo_view(&form, Id::Correo, input_view, &visibles, None, &pal)
33//!
34//! // en el update:
35//! Msg::Blur(id)  => m.estado.tocar(id),
36//! Msg::Enviar    => { m.estado.enviar(); if errores.es_valido() { … } }
37//! ```
38
39#![forbid(unsafe_code)]
40// Sin `std`: sólo `alloc`, para que cruce a una jaula WASM o al kernel (W4.6).
41#![no_std]
42
43extern crate alloc;
44
45use alloc::format;
46use alloc::string::{String, ToString};
47use alloc::sync::Arc;
48use alloc::vec::Vec;
49
50use llimphi_compositor::llimphi_layout::taffy::{
51    prelude::{length, percent, FlexDirection, Size, Style},
52    AlignItems, Rect,
53};
54use llimphi_compositor::llimphi_text::Alignment;
55use llimphi_compositor::peniko::Color;
56use llimphi_compositor::View;
57use llimphi_theme::Theme;
58use llimphi_widget_field::{field_view, FieldPalette, FieldSpec};
59
60// ─────────────────────────────────────────────────────────────────────────
61// Reglas
62// ─────────────────────────────────────────────────────────────────────────
63
64/// Una condición que el valor de un campo tiene que cumplir.
65///
66/// El mensaje sale de la regla y lleva la **etiqueta del campo**, no su id:
67/// «Correo es obligatorio» y no «campo 2 inválido». Para un texto propio está
68/// [`Regla::Propia`].
69#[derive(Clone)]
70pub enum Regla {
71    /// No vacío (ignora espacios: tres espacios no son un nombre).
72    Requerido,
73    /// Al menos `n` caracteres.
74    LargoMin(usize),
75    /// A lo sumo `n` caracteres.
76    LargoMax(usize),
77    /// Tiene que parsear como número.
78    Numero,
79    /// Numérico dentro de `[min, max]`, inclusive. Implica [`Regla::Numero`].
80    Rango(f64, f64),
81    /// Forma mínima de un correo: algo, arroba, algo, punto, algo. **No** es una
82    /// validación de RFC —eso no se puede hacer con una regex y no sirve de
83    /// nada—: descarta el error de tipeo y deja pasar el resto, que es lo único
84    /// honesto que puede hacer un formulario sin mandar el mail.
85    Correo,
86    /// Predicado propio: devuelve el mensaje de error, o `None` si pasa.
87    Propia(Arc<dyn Fn(&str) -> Option<String> + Send + Sync>),
88}
89
90impl core::fmt::Debug for Regla {
91    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
92        match self {
93            Regla::Requerido => write!(f, "Requerido"),
94            Regla::LargoMin(n) => write!(f, "LargoMin({n})"),
95            Regla::LargoMax(n) => write!(f, "LargoMax({n})"),
96            Regla::Numero => write!(f, "Numero"),
97            Regla::Rango(a, b) => write!(f, "Rango({a}, {b})"),
98            Regla::Correo => write!(f, "Correo"),
99            Regla::Propia(_) => write!(f, "Propia(..)"),
100        }
101    }
102}
103
104/// Forma mínima de correo, sin pretender validar direcciones de verdad.
105fn parece_correo(v: &str) -> bool {
106    let Some((usuario, dominio)) = v.split_once('@') else {
107        return false;
108    };
109    if usuario.is_empty() || dominio.is_empty() || usuario.contains(' ') || dominio.contains(' ') {
110        return false;
111    }
112    match dominio.rsplit_once('.') {
113        Some((izq, tld)) => !izq.is_empty() && tld.len() >= 2,
114        None => false,
115    }
116}
117
118impl Regla {
119    /// Evalúa la regla. `None` = pasa; `Some(mensaje)` = el error a mostrar.
120    ///
121    /// Salvo [`Regla::Requerido`], **un valor vacío pasa todas**: si no, un
122    /// campo opcional vacío se quejaría de largo mínimo o de formato. Que un
123    /// campo sea obligatorio lo dice `Requerido`, y sólo eso.
124    pub fn evaluar(&self, valor: &str, etiqueta: &str) -> Option<String> {
125        let v = valor.trim();
126        if let Regla::Propia(f) = self {
127            return f(valor);
128        }
129        if v.is_empty() {
130            return match self {
131                Regla::Requerido => Some(format!("{etiqueta} es obligatorio")),
132                _ => None,
133            };
134        }
135        match self {
136            Regla::Requerido => None,
137            Regla::LargoMin(n) => (v.chars().count() < *n)
138                .then(|| format!("{etiqueta} necesita al menos {n} caracteres")),
139            Regla::LargoMax(n) => (v.chars().count() > *n)
140                .then(|| format!("{etiqueta} admite a lo sumo {n} caracteres")),
141            Regla::Numero => v.parse::<f64>().is_err().then(|| format!("{etiqueta} tiene que ser un número")),
142            Regla::Rango(min, max) => match v.parse::<f64>() {
143                Err(_) => Some(format!("{etiqueta} tiene que ser un número")),
144                Ok(n) if n < *min || n > *max => {
145                    Some(format!("{etiqueta} tiene que estar entre {min} y {max}"))
146                }
147                Ok(_) => None,
148            },
149            Regla::Correo => (!parece_correo(v)).then(|| format!("{etiqueta} no parece un correo")),
150            Regla::Propia(_) => None, // ya resuelto arriba
151        }
152    }
153}
154
155/// Un campo del formulario: su id (el enum del caller), cómo se llama y qué se
156/// le pide.
157pub struct CampoDef<Id> {
158    pub id: Id,
159    pub etiqueta: String,
160    pub reglas: Vec<Regla>,
161    /// Texto de ayuda debajo del control, cuando no hay error.
162    pub ayuda: Option<String>,
163}
164
165impl<Id> CampoDef<Id> {
166    pub fn new(id: Id, etiqueta: impl Into<String>) -> Self {
167        Self { id, etiqueta: etiqueta.into(), reglas: Vec::new(), ayuda: None }
168    }
169
170    pub fn regla(mut self, r: Regla) -> Self {
171        self.reglas.push(r);
172        self
173    }
174
175    pub fn reglas(mut self, rs: Vec<Regla>) -> Self {
176        self.reglas.extend(rs);
177        self
178    }
179
180    pub fn ayuda(mut self, texto: impl Into<String>) -> Self {
181        self.ayuda = Some(texto.into());
182        self
183    }
184
185    /// ¿Es obligatorio? Sale de las **reglas**, no de un flag aparte: un
186    /// asterisco que se pone a mano es un asterisco que algún día miente.
187    pub fn obligatorio(&self) -> bool {
188        self.reglas.iter().any(|r| matches!(r, Regla::Requerido))
189    }
190}
191
192// ─────────────────────────────────────────────────────────────────────────
193// Formulario y errores
194// ─────────────────────────────────────────────────────────────────────────
195
196/// La definición del formulario: qué campos hay y qué se le pide a cada uno.
197/// No guarda valores — esos viven en el `Model` del caller.
198pub struct Formulario<Id> {
199    campos: Vec<CampoDef<Id>>,
200}
201
202/// Los errores de una validación, en el **orden de los campos** del formulario
203/// (que es el orden en que se leen en pantalla, no el orden en que se
204/// encontraron).
205#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct Errores<Id> {
207    items: Vec<(Id, String)>,
208}
209
210impl<Id: Copy + PartialEq> Default for Errores<Id> {
211    fn default() -> Self {
212        Self { items: Vec::new() }
213    }
214}
215
216impl<Id: Copy + PartialEq> Errores<Id> {
217    /// Sin errores.
218    pub fn ninguno() -> Self {
219        Self::default()
220    }
221
222    pub fn es_valido(&self) -> bool {
223        self.items.is_empty()
224    }
225
226    pub fn cantidad(&self) -> usize {
227        self.items.len()
228    }
229
230    /// El error de un campo, si lo tiene.
231    pub fn de(&self, id: Id) -> Option<&str> {
232        self.items.iter().find(|(i, _)| *i == id).map(|(_, m)| m.as_str())
233    }
234
235    /// El primero en orden de pantalla — al que hay que llevar el foco tras un
236    /// submit fallido.
237    pub fn primero(&self) -> Option<(Id, &str)> {
238        self.items.first().map(|(i, m)| (*i, m.as_str()))
239    }
240
241    pub fn iter(&self) -> impl Iterator<Item = (Id, &str)> {
242        self.items.iter().map(|(i, m)| (*i, m.as_str()))
243    }
244}
245
246impl<Id: Copy + PartialEq> Formulario<Id> {
247    pub fn new(campos: Vec<CampoDef<Id>>) -> Self {
248        Self { campos }
249    }
250
251    pub fn campos(&self) -> &[CampoDef<Id>] {
252        &self.campos
253    }
254
255    pub fn campo(&self, id: Id) -> Option<&CampoDef<Id>> {
256        self.campos.iter().find(|c| c.id == id)
257    }
258
259    /// Valida un campo suelto contra su valor. `None` si pasa o si el id no está
260    /// en el formulario.
261    ///
262    /// Devuelve el **primer** error de ese campo: mostrar tres quejas juntas
263    /// abajo de un input no ayuda a arreglar ninguna.
264    pub fn validar_campo(&self, id: Id, valor: &str) -> Option<String> {
265        let c = self.campo(id)?;
266        c.reglas.iter().find_map(|r| r.evaluar(valor, &c.etiqueta))
267    }
268
269    /// Valida el formulario entero. `valores` aparea id con lo que hay escrito;
270    /// un campo que no aparezca se valida como **vacío**, que es lo correcto
271    /// (un requerido que el caller se olvidó de mandar tiene que fallar, no
272    /// pasar en silencio).
273    pub fn validar(&self, valores: &[(Id, &str)]) -> Errores<Id> {
274        let mut items = Vec::new();
275        for c in &self.campos {
276            let valor = valores
277                .iter()
278                .find(|(i, _)| *i == c.id)
279                .map(|(_, v)| *v)
280                .unwrap_or("");
281            if let Some(msg) = c.reglas.iter().find_map(|r| r.evaluar(valor, &c.etiqueta)) {
282                items.push((c.id, msg));
283            }
284        }
285        Errores { items }
286    }
287}
288
289// ─────────────────────────────────────────────────────────────────────────
290// Cuándo mostrar — la parte que se hace mal
291// ─────────────────────────────────────────────────────────────────────────
292
293/// Qué campos ya «se tocaron» y si se intentó enviar. Vive en el `Model`.
294#[derive(Debug, Clone, PartialEq, Eq)]
295pub struct EstadoForm<Id> {
296    enviado: bool,
297    tocados: Vec<Id>,
298}
299
300impl<Id: Copy + PartialEq> Default for EstadoForm<Id> {
301    fn default() -> Self {
302        Self { enviado: false, tocados: Vec::new() }
303    }
304}
305
306impl<Id: Copy + PartialEq> EstadoForm<Id> {
307    pub fn nuevo() -> Self {
308        Self::default()
309    }
310
311    /// El campo perdió el foco (blur). Desde acá su error se muestra.
312    pub fn tocar(&mut self, id: Id) {
313        if !self.tocados.contains(&id) {
314            self.tocados.push(id);
315        }
316    }
317
318    /// Se intentó enviar: desde acá se muestran **todos** los errores, tocados
319    /// o no. Es el momento en que esconder algo sería mentir sobre por qué no
320    /// se envió.
321    pub fn enviar(&mut self) {
322        self.enviado = true;
323    }
324
325    pub fn se_intento_enviar(&self) -> bool {
326        self.enviado
327    }
328
329    /// Vuelve al estado inicial (tras un envío exitoso, o al abrir el
330    /// formulario de nuevo).
331    pub fn reiniciar(&mut self) {
332        self.enviado = false;
333        self.tocados.clear();
334    }
335
336    /// ¿Corresponde mostrar el error de este campo?
337    pub fn mostrar(&self, id: Id) -> bool {
338        self.enviado || self.tocados.contains(&id)
339    }
340
341    /// Los errores que corresponde mostrar ahora. Es lo que se le pasa a las
342    /// vistas: así el «cuándo» se decide **una vez** y no en cada campo.
343    pub fn visibles(&self, errores: &Errores<Id>) -> Errores<Id> {
344        Errores {
345            items: errores
346                .items
347                .iter()
348                .filter(|(id, _)| self.mostrar(*id))
349                .cloned()
350                .collect(),
351        }
352    }
353}
354
355// ─────────────────────────────────────────────────────────────────────────
356// Vistas
357// ─────────────────────────────────────────────────────────────────────────
358
359/// Paleta del resumen de errores. El campo usa la del `field`.
360#[derive(Debug, Clone, Copy)]
361pub struct FormPalette {
362    pub campo: FieldPalette,
363    /// Fondo del resumen de errores.
364    pub bg_resumen: Color,
365    pub fg_resumen: Color,
366    pub borde_resumen: Color,
367}
368
369impl FormPalette {
370    pub fn from_theme(t: &Theme) -> Self {
371        let mut fondo = t.fg_destructive;
372        // El rojo pleno de fondo no deja leer: se usa translúcido sobre el panel.
373        fondo.components[3] *= 0.12;
374        Self {
375            campo: FieldPalette::from_theme(t),
376            bg_resumen: fondo,
377            fg_resumen: t.fg_destructive,
378            borde_resumen: t.fg_destructive,
379        }
380    }
381}
382
383impl Default for FormPalette {
384    fn default() -> Self {
385        Self::from_theme(&Theme::dark())
386    }
387}
388
389/// Envuelve un control en su `field`, sacando del formulario **la etiqueta, el
390/// asterisco de obligatorio y el error**. Es el punto de todo esto: esos tres
391/// datos dejan de escribirse dos veces (una en las reglas y otra en la vista),
392/// que es como terminan diciendo cosas distintas.
393///
394/// `errores` son los **visibles** ([`EstadoForm::visibles`]), no todos.
395pub fn campo_view<Msg: Clone + 'static, Id: Copy + PartialEq>(
396    form: &Formulario<Id>,
397    id: Id,
398    control: View<Msg>,
399    errores: &Errores<Id>,
400    palette: &FormPalette,
401) -> View<Msg> {
402    let def = form.campo(id);
403    field_view(FieldSpec {
404        label: def.map(|c| c.etiqueta.clone()).unwrap_or_default(),
405        control,
406        required: def.map(|c| c.obligatorio()).unwrap_or(false),
407        helper: def.and_then(|c| c.ayuda.clone()),
408        error: errores.de(id).map(|s| s.to_string()),
409        palette: palette.campo,
410    })
411}
412
413/// Resumen de errores para la cabecera del formulario: «Hay 3 errores» + la
414/// lista, cada uno **clickeable** para ir al campo.
415///
416/// No es decoración: en un formulario largo, el error puede estar fuera de
417/// pantalla y lo único que se ve es que el botón no hizo nada. Devuelve `None`
418/// si no hay errores visibles — así el caller lo mete sin un `if` propio.
419pub fn resumen_errores_view<Msg: Clone + 'static, Id: Copy + PartialEq>(
420    errores: &Errores<Id>,
421    on_ir_a: impl Fn(Id) -> Msg,
422    palette: &FormPalette,
423) -> Option<View<Msg>> {
424    if errores.es_valido() {
425        return None;
426    }
427    let n = errores.cantidad();
428    let titulo = if n == 1 {
429        "Hay 1 error que corregir".to_string()
430    } else {
431        format!("Hay {n} errores que corregir")
432    };
433    let mut hijos: Vec<View<Msg>> = Vec::with_capacity(n + 1);
434    hijos.push(
435        View::<Msg>::new(Style {
436            size: Size { width: percent(1.0_f32), height: length(18.0_f32) },
437            align_items: Some(AlignItems::CENTER),
438            ..Default::default()
439        })
440        .text_aligned(titulo.clone(), 12.0, palette.fg_resumen, Alignment::Start)
441        // El resumen es el punto de entrada del formulario tras un submit
442        // fallido: se anuncia como alerta, no como un párrafo más.
443        .role(llimphi_compositor::Role::Alert)
444        .aria_label(titulo),
445    );
446    for (id, mensaje) in errores.iter() {
447        let msg = on_ir_a(id);
448        hijos.push(
449            View::<Msg>::new(Style {
450                size: Size { width: percent(1.0_f32), height: length(17.0_f32) },
451                align_items: Some(AlignItems::CENTER),
452                ..Default::default()
453            })
454            .text_aligned(
455                format!("· {mensaje}"),
456                11.5,
457                palette.fg_resumen,
458                Alignment::Start,
459            )
460            .cursor(llimphi_compositor::Cursor::Pointer)
461            .role(llimphi_compositor::Role::Button)
462            .aria_label(mensaje.to_string())
463            .on_click(msg),
464        );
465    }
466    let alto = 18.0 + 17.0 * n as f32 + 12.0;
467    Some(
468        View::<Msg>::new(Style {
469            flex_direction: FlexDirection::Column,
470            size: Size { width: percent(1.0_f32), height: length(alto) },
471            padding: Rect {
472                left: length(10.0_f32),
473                right: length(10.0_f32),
474                top: length(6.0_f32),
475                bottom: length(6.0_f32),
476            },
477            ..Default::default()
478        })
479        .fill(palette.bg_resumen)
480        .radius(6.0)
481        .border(1.0, palette.borde_resumen)
482        .children(hijos),
483    )
484}
485
486#[cfg(test)]
487mod tests {
488    use super::*;
489    // El `vec!` lo usan SÓLO los tests: va acá adentro y no arriba, porque en un
490    // crate no_std un import que sólo usan los tests se ve como muerto (y
491    // `cargo fix` lo borra, rompiendo el build de tests sin avisar).
492    use alloc::vec;
493
494    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
495    enum Id {
496        Nombre,
497        Correo,
498        Edad,
499        Nota,
500    }
501
502    fn form() -> Formulario<Id> {
503        Formulario::new(vec![
504            CampoDef::new(Id::Nombre, "Nombre").regla(Regla::Requerido),
505            CampoDef::new(Id::Correo, "Correo")
506                .reglas(vec![Regla::Requerido, Regla::Correo]),
507            CampoDef::new(Id::Edad, "Edad").regla(Regla::Rango(18.0, 120.0)),
508            CampoDef::new(Id::Nota, "Nota").regla(Regla::LargoMax(10)),
509        ])
510    }
511
512    #[test]
513    fn un_campo_vacio_solo_se_queja_si_es_obligatorio() {
514        let f = form();
515        // Vacío y obligatorio: falla.
516        assert!(f.validar_campo(Id::Nombre, "").is_some());
517        // Vacío y opcional: pasa, aunque tenga reglas de formato. Si no, un
518        // campo opcional en blanco se quejaría de no ser un número.
519        assert_eq!(f.validar_campo(Id::Edad, ""), None);
520        assert_eq!(f.validar_campo(Id::Nota, ""), None);
521        // Y "obligatorio" no se cumple con espacios.
522        assert!(f.validar_campo(Id::Nombre, "   ").is_some());
523    }
524
525    #[test]
526    fn el_mensaje_lleva_la_etiqueta_del_campo() {
527        let f = form();
528        // «Correo es obligatorio», no «campo 2 inválido».
529        assert_eq!(
530            f.validar_campo(Id::Correo, "").as_deref(),
531            Some("Correo es obligatorio")
532        );
533        assert_eq!(
534            f.validar_campo(Id::Edad, "5").as_deref(),
535            Some("Edad tiene que estar entre 18 y 120")
536        );
537    }
538
539    #[test]
540    fn se_muestra_el_primer_error_del_campo_no_los_tres() {
541        // Correo vacío falla por Requerido; el mensaje de formato no se apila.
542        let f = form();
543        let msg = f.validar_campo(Id::Correo, "").unwrap();
544        assert!(!msg.contains("correo"), "no debería apilar el de formato: {msg}");
545    }
546
547    #[test]
548    fn la_agregacion_junta_todos_los_campos_en_orden_de_pantalla() {
549        let f = form();
550        let e = f.validar(&[
551            (Id::Nombre, ""),
552            (Id::Correo, "sin-arroba"),
553            (Id::Edad, "5"),
554            (Id::Nota, "ok"),
555        ]);
556        assert_eq!(e.cantidad(), 3, "tres campos mal, tres errores");
557        // En el orden del formulario, que es el de la pantalla — no el de
558        // detección ni el de un mapa.
559        let ids: Vec<Id> = e.iter().map(|(i, _)| i).collect();
560        assert_eq!(ids, vec![Id::Nombre, Id::Correo, Id::Edad]);
561        // El primero es al que hay que llevar el foco.
562        assert_eq!(e.primero().map(|(i, _)| i), Some(Id::Nombre));
563        assert!(e.de(Id::Nota).is_none());
564        assert!(!e.es_valido());
565    }
566
567    #[test]
568    fn un_campo_que_el_caller_se_olvido_de_mandar_falla_si_es_obligatorio() {
569        // Se valida como vacío, no se saltea: si no, olvidarse de pasarlo
570        // dejaría pasar un formulario incompleto en silencio.
571        let f = form();
572        let e = f.validar(&[(Id::Correo, "a@b.co")]);
573        assert_eq!(e.de(Id::Nombre).is_some(), true);
574        assert!(e.de(Id::Correo).is_none());
575    }
576
577    #[test]
578    fn el_correo_descarta_el_tipeo_y_no_pretende_validar_direcciones() {
579        let f = form();
580        for malo in ["sin-arroba", "@dominio.com", "usuario@", "u@dominio", "u @b.com"] {
581            assert!(f.validar_campo(Id::Correo, malo).is_some(), "debería fallar: {malo}");
582        }
583        for bueno in ["a@b.co", "sergio@gioser.net", "x.y+z@sub.dominio.com"] {
584            assert_eq!(f.validar_campo(Id::Correo, bueno), None, "debería pasar: {bueno}");
585        }
586    }
587
588    #[test]
589    fn una_regla_propia_manda_su_propio_mensaje_y_ve_el_valor_crudo() {
590        let f = Formulario::new(vec![CampoDef::new(Id::Nota, "Nota").regla(Regla::Propia(
591            Arc::new(|v: &str| (!v.starts_with("OK")).then(|| "Tiene que empezar con OK".into())),
592        ))]);
593        assert_eq!(
594            f.validar_campo(Id::Nota, "hola").as_deref(),
595            Some("Tiene que empezar con OK")
596        );
597        assert_eq!(f.validar_campo(Id::Nota, "OK dale"), None);
598        // La propia SÍ ve el vacío (a diferencia de las de formato): es la
599        // única forma de escribir una regla condicional de verdad.
600        assert!(f.validar_campo(Id::Nota, "").is_some());
601    }
602
603    #[test]
604    fn obligatorio_sale_de_las_reglas_y_no_de_un_flag_aparte() {
605        let f = form();
606        assert!(f.campo(Id::Nombre).unwrap().obligatorio());
607        assert!(!f.campo(Id::Edad).unwrap().obligatorio());
608    }
609
610    #[test]
611    fn no_se_grita_mientras_se_tipea_por_primera_vez() {
612        let f = form();
613        let errores = f.validar(&[(Id::Nombre, ""), (Id::Correo, ""), (Id::Edad, ""), (Id::Nota, "")]);
614        assert_eq!(errores.cantidad(), 2, "nombre y correo son obligatorios");
615
616        let mut estado: EstadoForm<Id> = EstadoForm::nuevo();
617        // Recién abierto: hay errores, no se muestra ninguno.
618        assert!(estado.visibles(&errores).es_valido());
619
620        // Salir del campo muestra el suyo, y sólo el suyo.
621        estado.tocar(Id::Nombre);
622        let v = estado.visibles(&errores);
623        assert_eq!(v.cantidad(), 1);
624        assert!(v.de(Id::Nombre).is_some() && v.de(Id::Correo).is_none());
625
626        // Intentar enviar los muestra todos: esconder alguno sería mentir sobre
627        // por qué no se envió.
628        estado.enviar();
629        assert_eq!(estado.visibles(&errores).cantidad(), 2);
630
631        // Y reiniciar vuelve al silencio inicial.
632        estado.reiniciar();
633        assert!(estado.visibles(&errores).es_valido());
634    }
635
636    #[test]
637    fn tocar_dos_veces_el_mismo_campo_no_lo_duplica() {
638        let mut estado: EstadoForm<Id> = EstadoForm::nuevo();
639        estado.tocar(Id::Nombre);
640        estado.tocar(Id::Nombre);
641        assert!(estado.mostrar(Id::Nombre));
642        assert!(!estado.mostrar(Id::Correo));
643    }
644
645    #[test]
646    fn el_largo_se_cuenta_en_caracteres_no_en_bytes() {
647        let f = Formulario::new(vec![CampoDef::new(Id::Nota, "Nota")
648            .reglas(vec![Regla::LargoMin(3), Regla::LargoMax(5)])]);
649        // «añón» son 4 caracteres y 6 bytes: contar bytes lo rechazaría.
650        assert_eq!(f.validar_campo(Id::Nota, "añón"), None);
651        assert!(f.validar_campo(Id::Nota, "añ").is_some());
652        assert!(f.validar_campo(Id::Nota, "añónico").is_some());
653    }
654}