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//! `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}