Skip to content

Tests y CI

Pablo Martínez López edited this page Nov 10, 2026 · 14 revisions

Tests y CI

English · Español

Qué se prueba, cómo, y qué puerta atrapa cada cosa: Vitest para el frontend y los scripts de Node, cargo test para Rust (incluidos los tests dorados por comparación de imágenes del diseñador de cartas y el crate del motor de la mesa, mesa), los vectores de prueba compartidos con el servidor de amigos de Tapete, los scripts de comprobación propios, Knip, el git hook y los workflows de GitHub Actions.

Las puertas locales y CI ejecutan los mismos comandos, así que un pnpm check en verde más los tres comandos de Rust predicen un CI en verde. La preparación está en Preparación del entorno de desarrollo; las convenciones que hacen cumplir las puertas están en Convenciones.

Las puertas de un vistazo

flowchart TD
    C["pnpm check"] --> T["check:tipos: tsc --noEmit"]
    C --> V["test: vitest run"]
    C --> B["check:biome: biome check ."]
    C --> TI["check:title: sin-title-nativo.mjs"]
    C --> TA["check:tactil: sin-hover-solo.mjs"]
    C --> I["idiomas: idiomas.mjs"]
    C --> IC["check:iconos: iconos.mjs --check"]
    C --> K["check:knip: knip"]
    R1["check:rust-fmt: cargo fmt --check"]
    R2["check:clippy: clippy -D warnings"]
    R3["test:rust: cargo test --workspace"]
    RA["check:clippy-android: clippy para aarch64-linux-android (solo en local)"]
    O["sin OpenSSL: cargo tree -i openssl está vacío"]
    CI["job de CI 'checks'"] --> C
    CI --> R1
    CI --> R2
    CI --> R3
    CI --> O
Loading
Puerta Comando Falla cuando
Tipos pnpm check:tipos (tsc --noEmit) Errores de TypeScript, variables locales o parámetros sin usar (strict, noUnusedLocals, noUnusedParameters en tsconfig.json)
Tests unitarios pnpm test (vitest run) Falla un test
Lint + formato pnpm check:biome (biome check .) Error de lint o formato incorrecto (tabuladores, comillas, ancho de línea 100; véase biome.json)
Tooltips nativos pnpm check:title Un .tsx pone title= en un elemento HTML o en un componente que lo pasa al DOM
Táctil pnpm check:tactil (node scripts/sin-hover-solo.mjs) Un .ts/.tsx escucha algo que solo existe con ratón (onPointerEnter, onMouseEnter, onPointerLeave, onMouseLeave, onMouseOver, onPointerOver, onContextMenu, o addEventListener de mouseenter, pointerenter, mouseover, pointerover, contextmenu) sin un comentario // táctil: ... en su línea o en las tres anteriores que diga cómo se hace con el dedo (véase abajo)
Traducciones pnpm idiomas Un idioma de src/lib/idiomas.ts no tiene diccionario, a uno le falta una cadena pasada a t(), una cadena está en dos ficheros de área, o no cuadran los marcadores o las formas de plural (véase abajo)
Iconos incluidos pnpm check:iconos (node scripts/iconos.mjs --check) src/lib/iconos/incluidos.ts no cuadra con los ids de icono escritos en src/ (pnpm iconos), o un id nombra un icono que no existe en su colección de @iconify/json (véase Iconos)
Código muerto pnpm check:knip (knip) Ficheros o dependencias sin usar
Formato de Rust pnpm check:rust-fmt cargo fmt --check informa de diferencias
Lints de Rust pnpm check:clippy Cualquier aviso de clippy (-D warnings, todos los targets, todo el workspace), incluidos unwrap/expect fuera de tests
Lints de Rust en Android pnpm check:clippy-android Cualquier aviso de clippy en las ramas cfg(mobile) / cfg(target_os = "android"), que el clippy normal nunca compila (cargo clippy --lib --target aarch64-linux-android -- -D warnings tras . scripts/android-env.sh). No está en el CI: el workflow no tiene la cadena de herramientas de Android salvo en la release; ejecútalo antes de entregar un cambio que toque android.rs, fichero.rs, ciclo.rs, tls.rs o plugins/android-extra/
Tests de Rust pnpm test:rust Falla cualquier test (esto también ejecuta los tests de exportación de ts-rs, véase Preparación del entorno de desarrollo)
Sin OpenSSL Solo un paso de CI cargo tree -i openssl encuentra el crate

pnpm check ejecuta sus partes en el orden tipos, test, biome, title, tactil, idiomas, iconos, knip (scripts.check de package.json); title, tactil, idiomas e iconos se ejecutan con --silent para que solo se vean sus propios mensajes.

Tests del frontend (Vitest)

Configuración: vitest.config.ts usa el entorno node (sin DOM), el alias @ a src, restoreMocks: true, y recoge src/**/*.test.ts, src/**/*.test.tsx y scripts/**/*.test.mjs. happy-dom es una dependencia de desarrollo, pero el entorno por defecto es node.

Filosofía: los componentes de React no se renderizan en los tests. El código probado es la lógica que se sacó a propósito de los componentes a módulos y stores planos, a los que se da un @/lib/ipc simulado. Patrón típico (véase src/stores/__tests__/actualizacion-store.test.ts): vi.mock("@/lib/ipc", ...) falsea call y on con un switch sobre el nombre del comando y lanza una excepción ante un comando inesperado, y luego comprueba la secuencia de comandos que emitió el store.

Carpeta Tests (ficheros)
src/stores/__tests__/ deck-store, deck-store-abrir, ui-store, plan-store, trabajo-store, actualizacion-store, originales-store (conOriginales), vivo-store (cursores y fantasmas de los demás jugadores), mesa-store (aplica los parches que escribe crates/mesa, véase abajo)
src/lib/__tests__/ cartas, deck-ops, dialogos, errores, idiomas (la tabla de idiomas), image-url, imagen-viva, tema-inicial, temas-instalados, trabajos (el ayudante de trabajos que escucha antes de lanzar), cierre (la guarda de cierre: pendientes / hayPendientes con cada razón, altas y bajas de diálogos en el registro, varios diálogos a la vez, un mazo que no se pudo guardar)
src/i18n/__tests__/ traducir (respaldo idioma, inglés, español; formas del plural ruso por n; un n que no es número usa other; números con el formato del idioma de la interfaz; cargarDiccionario)
src/components/__tests__/ emoji (la bandera de cada idioma tiene imagen; pintado a texto con react-dom/server, sin DOM)
src/components/emoji/__tests__/ insertar (insertarEnCursor: en el cursor, sustituyendo lo seleccionado, al final sin selección, posiciones fuera de rango recortadas, nada si pasaría del máximo; idiomaDelSelector: el chino y el rumano)
src/features/decklist/__tests__/ acciones
src/features/export/__tests__/ acciones
src/features/library/__tests__/ organizar (filtrar, ordenar, agrupar, selección), partidas-deck (porcentaje de victorias)
src/features/mesa/__tests__/, mesa/dados/__tests__/, mesa/listas/__tests__/ La lógica de la pantalla de la mesa de juego (arrastre, atajos, búsqueda, chat, cuentas, daño de comandante, equipos, disposición, sonidos, física y geometría de los dados, elección de lista…); véase Mesa de juego
src/features/mesa/red/__tests__/, src/features/amigos/__tests__/ codigo (códigos de partida y enlaces pegados: cualquiera de los dos esquemas o el enlace /join/ de la web), visto (la última conexión como tiempo relativo)
src/features/preview/__tests__/ acciones-carta, arrastrar-ficheros
src/features/variants/__tests__/ vincular
src/features/card-editor/__tests__/ propagar (aplicar a la carta / copias / todas)
src/features/disenar/__tests__/ estilos, encuadre, grupos, por-cara (estilo y ajustes por cara), renumerar, textos
src/features/stats/__tests__/ textos
scripts/__tests__/ release-notas.test.mjs (análisis de argumentos, plantilla y limpieza de las notas de la versión, reescritura de las notas de latest.json), idiomas.test.mjs (comprobar: claves que faltan, marcadores cambiados, categorías de plural que faltan o son de otro idioma, plural sin {n}, avisos, una clave repetida en un fichero, un fichero de área que falta)

Trampa conocida: el comportamiento del scroll y de las animaciones de la interfaz real no lo cubre ningún test automático; hay que probarlo a mano en WebKitGTK (véase Preparación del entorno de desarrollo).

Tests de Rust

pnpm test:rust ejecuta cargo test --workspace para tres crates (members en src-tauri/Cargo.toml): la app (proxy-builder, biblioteca proxy_builder_lib), el núcleo puro proxy-core y el motor puro de la mesa, mesa. En esta versión hay unas 750 funciones #[test]/#[tokio::test] repartidas en unos 140 ficheros (busca en el árbol para cifras exactas). Dev-dependencies de la app: tempfile, lopdf (inspecciona los PDF generados) y tokio con test-util; proxy-core tiene lopdf.

Reglas visibles en el código:

  • Sin red en la ejecución por defecto. Todo lo que necesite internet real (Scryfall, GitHub, Google Fonts, MTGJSON, Cockatrice, mtg.io) está en #[ignore] y pensado para ejecuciones manuales. Los parsers se prueban contra fixtures guardados: src-tauri/src/fixtures/ (archidekt-1.json, cockatrice-tokens.xml, mtgio-muro-de-algas.json, tweakcn-amethyst-haze.json) y src-tauri/src/scryfall/fixtures/.
  • Estado temporal. Los tests que necesitan un AppState lo construyen sobre un tempfile::tempdir() (rutas de datos y de caché), nunca sobre las carpetas reales de la app.
  • Los tests pueden usar unwrap. #![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))] solo se aplica fuera de los tests; los ficheros de tests de integración añaden #![allow(clippy::unwrap_used, clippy::expect_used)].
  • Dónde está el volumen. La mayoría de los tests están en proxy-core: el diseñador (diseno/render.rs, componer.rs, cuerpos.rs, texto.rs, pack.rs, pintar.rs, estructura.rs), layout (layout/plan.rs, grid.rs), análisis y exportación de la lista del mazo (decklist/), tratamiento de imagen (imaging/frame.rs, corners.rs, adjust.rs) y stats. En el crate de la app: scryfall/resolve.rs, commands/cards.rs, commands/stats.rs, render.rs, store/decks.rs, package.rs, commands/export.rs, commands/identify.rs.

Guardas específicas de Android

  • src-tauri/src/tls.rs: nadie_crea_un_cliente_ni_un_verificador_fuera_de_tls_rs recorre todos los .rs de src-tauri/src y falla con cualquier línea fuera de tls.rs que contenga Client::builder() sin envolver en tls::reqwest(, with_platform_verifier o CaTlsConfig::. Motivo: un cliente construido a mano usaría el verificador del sistema, que entra en pánico en Android y en escritorio nadie lo notaría (Capa de aplicación Rust).
  • src-tauri/src/fichero.rs: la lógica de rutas y content:// contra un Acceso simulado (leer copia a la caché, escribir pasa por un temporal, nombres de Android o de la URI, ZIP, compartir) para que nada de eso necesite un móvil.
  • src-tauri/src/abrir.rs: solo cuentan las URI content:// de largo razonable; un fichero pendiente se entrega una vez. commands/sistema.rs: la pantalla completa sin Android no es un fallo. commands/export.rs: la estimación de memoria y el ZIP de los PDF partidos.
  • src-tauri/src/actualizar.rs: la clave del objetivo de Android (android-aarch64), dónde va el APK bajado y la_feature_de_pruebas_no_se_activa_nunca_sola (abajo).
  • scripts/__tests__/release-notas.test.mjs cubre también conAndroid y nombreApk.
  • Pantalla: src/lib/__tests__/ para formato, atras, pulsacion-larga, teclado, seccion-movil y los filtros móviles de dialogos; features/shell/__tests__/atras-shell.test.ts; la lógica de los gestos táctiles de la mesa y de Diseñar vive en módulos simples (features/mesa/gestos.ts, features/disenar/gestos.ts) para probarla sin DOM.

Lo que no tiene test automático: el plugin de Kotlin, el instalador del sistema, la hoja de compartir y el WebView de Android. Eso se probó a mano en un emulador de Android (Preparación del entorno de desarrollo) y figura como no probado en un móvil real en Móvil.

Guarda de la configuración del actualizador

src-tauri/src/actualizar.rs tiene un test, la_configuracion_del_actualizador_es_valida, que lee tauri.conf.json y comprueba que createUpdaterArtifacts es true, que requireSignedVersion es true, que los endpoints son exactamente el latest.json del repositorio tapete y después el de proxy-builder, en ese orden (el segundo es el respaldo mientras el repositorio no se renombre), y que la clave pública se decodifica como una clave minisign con el id de clave esperado. Motivo: romper cualquiera de estas cosas deja en silencio a todas las copias instaladas sin actualizaciones. Véase Versiones y actualizaciones automáticas.

Una segunda guarda, la_feature_de_pruebas_no_se_activa_nunca_sola, lee Cargo.toml, ci.yml, release.yml, package.json y tauri.conf.json (include_str!) y falla si la feature actualizar-local (un actualizador apuntado a un latest.json local, para emuladores) está activa por defecto o aparece mencionada en cualquiera de ellos. Motivo: una release compilada con ella no buscaría actualizaciones en GitHub.

Tests dorados (renderizado del diseñador)

Ubicación: src-tauri/crates/proxy-core/tests/.

Fichero Qué es
dorados.rs Casos base por estilo, cada uno renderizado con el motor real y comparado con un PNG guardado en tests/dorados/; además dorados_en_otros_idiomas (idioma-fr.png, idioma-ru.png, idioma-ro.png: latín con diacríticos, cirílico y un idioma sin cartas impresas; comprueba que no sale FaltanLetras y que SinTextoOficial solo sale en rumano)
layouts.rs Un dorado por cada maquetación especial de Magic en tests/dorados/layouts/ (planeswalker, saga, clase, case, split, flip, transform, MDFC, batalla, meld, mazmorra, plano, vanguard...) usando datos reales de cartas guardados en tests/dorados/layouts/datos/*.json
comun/mod.rs Ayudantes compartidos: ilustración sintética (arte(seed)), emblema, símbolos dibujados en local y diferencia()
compatibilidad/diseno-2.5.9.json Un diseño tal como lo guardaba la versión 2.5.9, para los tests de compatibilidad de abajo

Puntos de diseño:

  • Los recursos de los tests se generan en código (ilustración de degradado y círculos, un emblema ovalado azul, símbolos de círculos de colores) para no commitear arte con derechos de autor. Las letras y el pack de marcos de la casa salen de los assets/ reales (el motor se construye con Motor::de_fabrica()).
  • Comparación: diferencia(a, b) devuelve el porcentaje de píxeles en los que algún canal difiere en más de 8/255; los tamaños distintos cuentan como 100 %. Un caso falla con 0,5 % o más (dorados_de_los_casos_base). Si falla, el nuevo renderizado se escribe en el directorio temporal del sistema como <caso>-nuevo.png (nuevo-<nombre> en layouts.rs) para inspeccionarlo, y el test lista juntos todos los casos fallidos.
  • La primera ejecución crea, las siguientes comparan. Si no existe un fichero dorado, se escribe y el caso pasa.
  • Actualizar los dorados: PB_ACTUALIZAR_DORADOS=1 cargo test -p proxy-core --test dorados (o --test layouts) los reescribe. Hazlo solo cuando el cambio visual sea intencionado y esté aprobado; revisa la diferencia en los PNG antes de commitear.
  • Tests de determinismo: dos_renders_iguales_dan_los_mismos_bytes comprueba que dos renderizados de un mismo caso son idénticos byte a byte (el PDF y la vista previa dependen del determinismo); cambiar_un_color_del_pack_cambia_el_dorado demuestra que editar la paleta de un pack cambia el resultado (protege contra que se ignoren los colores del pack).
  • las_cartas_grandes_salen_a_su_medida comprueba que las cartas grandes (planos, planes, Vanguard) se renderizan a su propio tamaño.

Compatibilidad con versiones anteriores (idiomas de diseño)

Añadir idiomas no puede cambiar lo que ya son los diseños en español e inglés, ni en disco ni en pantalla. Lo fijan tres tests:

  • diseno::efectivo::tests::las_huellas_en_espanol_e_ingles_son_las_de_antes: huellas de render (huella) fijas, sacadas de la 2.5.9, en español e inglés. Si cambiaran, se volverían a pintar todas las cartas diseñadas.
  • diseno::modelo::tests::un_diseno_de_antes_se_reescribe_igual: tests/compatibilidad/diseno-2.5.9.json se lee y se vuelve a escribir idéntico byte a byte (guardar un deck en esta versión no reescribe los diseños de antes).
  • En el mismo módulo, otro_idioma_se_guarda_tras_es_y_en_y_lo_lee_la_version_de_antes: un diseño con otro idioma deja es y en primero, una estructura antigua de dos campos lo sigue leyendo y una clave de un idioma desconocido se ignora.

Véase Motor del diseñador para ByLang y la huella.

Tests ignorados (manuales) y sus variables de entorno

Se ejecutan con cargo test <ruta> -- --ignored --nocapture. Necesitan datos locales o la red y nunca forman parte de CI.

Variable Usada por Significado
PB_ACTUALIZAR_DORADOS tests/dorados.rs, tests/layouts.rs Reescribir los dorados en vez de compararlos
PB_CJK dorados::dorado_en_japones Ruta de un fichero de Noto Sans JP (la app la baja aparte, así que no está en el repo); compara con idioma-ja.png. Sin la letra, el render lleva FaltanLetras.
PB_BALATRO tests/comun pack_balatro(), layouts::balatro_matriz, dorados::balatro_* Carpeta del pack de marcos Balatro (el repositorio de packs aparte)
PB_SALIDA igual Carpeta de salida de las hojas de contacto (por defecto, un directorio temporal)
PB_ORIGINAL dorados::original_con_sus_imagenes, original_colores Carpeta con las imágenes de marcos «Original» descargadas
PB_GF dorados::letras_libres_a_ojo, diseno/letras_libres.rs Carpeta de los ficheros de Google Fonts libres
PB_AUTOR dorados::balatro_contra_el_autor Los renderizados de referencia del autor del pack con los que comparar
PB_OCR_DATOS, PB_OCR_IMG ocr.rs, originales.rs Carpeta de modelos de OCR y una imagen de prueba
PB_ID_DIR commands/identify.rs Carpeta de imágenes para la identificación en lote
PB_IMG imaging/corners.rs en_vivo Una imagen con la que medir el relleno de esquinas
PB_DB, PB_DECK scryfall/fichas_auto.rs en_vivo Una copia de una base de datos de la biblioteca y un nombre de mazo para la detección de fichas
PB_PDF_DE_PRUEBA commands/export.rs de_punta_a_punta Dónde dejar el PDF de punta a punta
PB_PACKS_LOCAL diseno/descargables.rs Solo compilaciones de depuración: carga los packs descargables desde una carpeta local en vez de GitHub (no es solo una variable de test)
PB_DATOS, PB_CACHE mesa/ejemplo.rs deck_de_ejemplo, deck_grande_de_ejemplo Las carpetas de datos y de caché de la app: escribe en esa biblioteca un deck de ejemplo con cartas diseñadas (sin Scryfall), para probar la mesa de juego
PB_TAPETE amigos/pruebas_servidor.rs Un servidor de amigos de Tapete en marcha (p. ej. http://127.0.0.1:1530) para el test de dos identidades (véase «Amigos: vectores compartidos y el test con el servidor», abajo)

Otras dos variables no son de tests ignorados, sino que reescriben datos guardados en una ejecución normal: PB_FIXTURES=1 (los fixtures del protocolo de src-tauri/src/red/ y los parches de crates/mesa) y PB_CATALOGO_ESCRIBIR=1 (la instantánea del catálogo de crates/mesa). Las dos se explican más abajo.

Otros ejemplos de #[ignore]: descargas en vivo (diseno/original.rs en_vivo, descargables.rs, fuentes/mtgio.rs, fuentes/cockatrice.rs, fuentes/mtgjson.rs, scryfall/client.rs, scryfall/nombres.rs, temas.rs, diseno/traduccion.rs, diseno/borrador.rs).

src-tauri/crates/proxy-core/examples/ (disenar.rs, prototipo.rs) son visores ejecutables, no tests: cargo run --release -p proxy-core --example disenar -- <symbols> <arts> <out> [width].

Tests del motor de la mesa (crates/mesa)

Ubicación: src-tauri/crates/mesa/tests/, con cargo test -p mesa (también entra en pnpm test:rust). El crate se compila con opt-level = 2 en desarrollo para que los tests de propiedad tarden segundos.

Fichero Qué es
acciones.rs Cada acción del catálogo (antes → evento → después). Un match exhaustivo (nombre) y la lista TODAS fallan si una variante nueva de Accion no tiene test
propiedad.rs 1.000 intenciones al azar (misma semilla → misma huella, las rechazadas no cambian nada), la visibilidad para cada mirador tras cada acción con los parches iguales a la vista entera, 2.000 eventos arbitrarios que nunca hacen panic
registro.rs Deshacer, rehacer, deshacer hasta una línea, permisos de deshacer en línea, invitados que entran tarde, repetición en cualquier seq, mulligan, huella; además busca en el código HashMap<, SystemTime, Instant::, rand::
preparar.rs Secciones del deck para la mesa y el reparto
turnos.rs El orden de los turnos por asientos desde el primer jugador, saltando a los eliminados; la ronda sube al pasar por el primero
equipos.rs Equipos: quién cuenta como oponente, la vida compartida
formatos.rs Formatos multijugador: los valores de fábrica, Archenemy, Planechase, el daño de comandante
muerte.rs Quién está fuera de juego (vida, veneno, daño de comandante, con la vida compartida de los equipos) y que la vista lo cuente
encuentro.rs Encuentros: el marcador y el cambio de cartas con el banquillo
resumen.rs El resumen de la partida: puestos por orden de eliminación, empates, concesiones, equipos
sparring.rs El sparring con partidas simuladas (un guion de jugadas: una tierra por ronda, los hechizos que dé el maná, pasar) con cada intensidad: cada una cae en su banda, apagado nunca reacciona, la misma semilla da las mismas reacciones, resolver y deshacer lo devuelve todo, nunca quita tierras básicas
catalogo.rs Las instantáneas del catálogo (véase abajo)
comun/mod.rs Decks y partidas de ejemplo, un generador de intenciones al azar y sin_fugas (las reglas de visibilidad escritas otra vez a mano)
fixtures.rs Juega una partida de ejemplo, comprueba en Rust que los parches sobre la primera vista dan la vista final y, con PB_FIXTURES=1 cargo test -p mesa --test fixtures, escribe src/stores/__tests__/fixtures/mesa-parches.json (una primera vista, 60 parches y la vista final). src/stores/__tests__/mesa-store.test.ts los aplica en TypeScript y tiene que llegar a la vista final, así Rust y el store de la pantalla coinciden en los parches. Nada compara el fichero guardado con lo que sale ahora de Rust, así que hay que reescribirlo después de cambiar la vista o los parches

Algunos módulos del motor tienen además tests unitarios en src/ (azar.rs, lista.rs, formato.rs, ids.rs, sparring/amenaza.rs, sparring/cartas.rs).

Las instantáneas del catálogo (VERSION_CATALOGO)

Los eventos (Evento, con su Cambio) son lo que guarda el registro de la partida, lo que se manda a los demás jugadores y lo que se repite, así que una repetición o una partida guardada tiene que seguir leyéndose en versiones posteriores. La regla, escrita en crates/mesa/src/accion.rs: las variantes de Accion y Cambio nunca se cambian ni se quitan, y una nueva o cambiada sube VERSION_CATALOGO (ahora 4), porque un enum de serde rechaza una etiqueta que no conoce y la red y las repeticiones guardadas comparan la versión.

tests/catalogo.rs (los_eventos_de_cada_version_se_siguen_leyendo) lee cada tests/catalogo/v1.json … v<VERSION_CATALOGO>.json y falla si falta uno o si ya no se puede leer. Así que, tras subir la versión, se escribe la instantánea nueva una sola vez con PB_CATALOGO_ESCRIBIR=1 cargo test -p mesa --test catalogo (unos 120 eventos al azar de una partida de dos jugadores más una línea de chat, sin las restauraciones de la partida entera) y no se vuelve a tocar. Sin ese fichero el test falla, y ese es el recordatorio.

tests/catalogo/ queda fuera de Biome (biome.json), como proxy-core/tests/compatibilidad, y también src/stores/__tests__/fixtures.

La capa de la app y la pantalla de la mesa de juego tienen sus propios tests (src-tauri/src/mesa/, protocol.rs::las_rutas_de_la_mesa, src/features/mesa/__tests__/): véase Mesa de juego.

Tests del juego en línea

  • src-tauri/src/red/pruebas.rs: partidas en el mismo proceso sobre iroh sin relés (direcciones locales), cada jugador con su sesión en memoria: tres jugadores y 200 jugadas al azar acaban con la misma huella; un invitado que se corta vuelve con la misma partida; un código malo, otra versión o (como jugador) una partida empezada se rechazan; si el host se cae, el siguiente jugador por asiento sigue solo y los demás lo encuentran; con dos jugadores, el que queda espera sin jugar; un host que pierde la red se para y luego se une al nuevo como invitado; y un espectador sigue mirando con el host nuevo (traspaso del anfitrión sin servidor, red/sucesion.rs, con la unidad de tiempo a 40 ms); los espectadores nunca ven la identidad de lo oculto, y solo les llega el movimiento de lo que ven; las imágenes propias llegan a un invitado por el host y una huella cambiada se tira; cada jugador sabe su latencia y si va directo; el reloj del host se calcula aunque el de aquí vaya atrasado; el fantasma de un invitado llega al host y a los demás con su nombre; quien se va aparece como ausente y deja de estarlo al volver; el host se queda la mano de un ausente que no decide; un expulsado queda eliminado, se le corta y no puede volver.
  • red/protocolo.rs: topes de las tramas, el Hola primero, cada mensaje de ida y vuelta, JSON y decks rotos, datagramas grandes o rotos, y los ejemplos para otros clientes en src-tauri/src/red/fixtures/ (al-host.json, del-host.json, datagramas.json), que reescribe PB_FIXTURES=1 cargo test --lib escribir_fixtures.

Amigos: vectores compartidos y el test con el servidor

El servidor de amigos de Tapete es un proyecto aparte (Amigos y mensajes); no interviene en la partida. Lo que mantiene alineados los dos lados:

Test Comprueba
amigos/firma.rs los_vectores_del_servidor_casan vectores/firma-v1.json (formato tapete-firma-v1): en cada caso válido la app monta la misma cadena canónica y la misma firma ed25519; los no válidos (bad_signature, bad_key) tampoco se verifican con verify_strict
amigos/cifrado.rs los_vectores_de_la_clave_de_cifrado_casan vectores/clave-cifrado-v1.json: la derivación HKDF de la clave X25519 y su firma, byte a byte
amigos/cifrado.rs (los demás) Cerrar y abrir un mensaje, uno alterado o ajeno no se abre, los límites de tamaño, el número de seguridad es el mismo en los dos lados
amigos/avisar.rs el_aviso_lleva_el_apodo_y_nunca_el_texto El aviso lleva el apodo y nunca el mensaje
enlace.rs Códigos y enlaces de amigo (codigo_amigo_pegado), los enlaces de la web, los esquemas aceptados son los registrados
amigos/pruebas_servidor.rs, #[ignore] Contra un servidor en marcha (PB_TAPETE=http://127.0.0.1:1530 cargo test --lib amigos::pruebas_servidor -- --ignored): dos identidades nuevas se dan de alta, se hacen amigas, se mandan un mensaje y una invitación cifrados, los confirman y ven el acuse de entrega

🛑 Los JSON de src-tauri/src/amigos/vectores/ son copia exacta de los tests/vectores/ del servidor. Si un lado los cambia, hay que cambiarlos en los dos en el mismo trabajo, o la app y el servidor dejan de entenderse. El CI solo ve la copia de la app.

Los scripts de comprobación

scripts/idiomas.mjs (pnpm idiomas)

Vigila el contrato de i18n: el texto de la interfaz se escribe en español dentro de t("..."), y cada diccionario (src/i18n/<código>/<área>.json, fundido por cargar.ts) se indexa por la cadena fuente en español. Como t() cae en silencio a un respaldo (el idioma, luego el inglés, luego la fuente en español), una traducción que falta compila y se pinta bien y solo se vería con la app en ese idioma; este script es la forma de enterarse. Hace esto:

  1. Reúne las cadenas pedidas: cada literal entre comillas dobles de t("...") y paraTraducir("...") en src/**/*.ts(x) (salta bindings, node_modules, las carpetas que empiezan por punto, los *.test.ts(x) y el propio src/i18n/).
  2. Lee la tabla de idiomas de src/lib/idiomas.ts, que es código y por eso se transpila con typescript. Luego carga cada carpeta de diccionario parseando sus .json con JSON.parse (los plurales llegan como datos reales) y revisa su texto en bruto por si una clave se repite en el mismo fichero, cosa que JSON.parse esconde. Las carpetas que empiezan por _ (como __tests__) se saltan.
  3. En cada idioma salvo el español, falla si: falta su carpeta; una cadena pedida no tiene entrada; una clave está en dos áreas (al fundir ganaría la última en silencio) o repetida en un mismo fichero; falta un fichero de área del diccionario inglés; los marcadores {x} no son los de la clave española (también en cada forma de un plural); hay un plural en una clave sin {n}; o a un plural le falta, o le sobra, una categoría de Intl.PluralRules(locale) de su idioma. Una carpeta que no es de ningún idioma de la tabla también es un error. El español (la fuente, es/) se comprueba con su propia regla: tiene las mismas áreas y claves que el inglés, y cada valor debe ser igual a su clave.
  4. Avisa (sin fallar) de las entradas que nadie pide y de los valores idénticos al inglés (sin contar los nombres de marca de IDENTICAS_PERMITIDAS del script ni las claves de un src/i18n/<código>/identicas.json opcional). --detalle lista cada aviso; si no, solo sale cuántos hay.

Sus partes puras (leerPedidas, leerTabla, leerDiccionarios, comprobar) se exportan y se prueban en scripts/__tests__/idiomas.test.mjs.

Trampas: solo se ven los literales entre comillas dobles directamente dentro de t(...); un t( con una plantilla literal o una variable no se comprueba. paraTraducir("...") (función identidad, src/components/language-provider.tsx) existe para marcar textos que viven en tablas de datos y se traducen en otro sitio (por ejemplo TEXTOS en src/lib/errores.ts). Véase Internacionalización.

scripts/sin-hover-solo.mjs (pnpm check:tactil)

Falla si un .ts/.tsx de src/ escucha algo que solo existe con ratón (las props y los eventos de addEventListener de la tabla de puertas) sin decir cómo se hace con el dedo. Cada uso lleva un comentario // táctil: ... (o {/* táctil: ... */}) en su línea o en las tres anteriores, p. ej. pulsación larga, un toque, un botón «...». Se saltan components/ui/ y components/tooltips-nativos.tsx (de solo lectura, heredados) y __tests__/bindings. Motivo: en una pantalla táctil no hay «ratón encima» ni clic derecho, y lo que solo se alcanzaba así se volvía inalcanzable. El comentario obliga a plantearlo al escribir el manejador. Los sitios que aún esperan su camino táctil lo dicen en el comentario («pendiente de ...») hasta que alguien lo resuelve. Véase Móvil.

scripts/i18n-anadir.py

No es una puerta sino la forma de añadir cadenas: python3 scripts/i18n-anadir.py <área> lee JSON por stdin ({ "Cadena en español": { "en": ..., "fr": ..., ... los otros 14 idiomas } }, un plural como { one, few, many, other } por idioma) y añade cada cadena a src/i18n/<código>/<área>.json en los 15 idiomas (el español repite la clave). Toma un flock sobre src/i18n/.bloqueo, de modo que varias personas o sesiones pueden añadir cadenas a la misma área a la vez sin pisarse; una clave que ya existe se deja como está, y rechaza una entrada a la que le falte un idioma. Después pnpm idiomas comprueba el resultado (Internacionalización).

scripts/sin-title-nativo.mjs (pnpm check:title)

Falla si un .tsx bajo src/ pone title= en una etiqueta HTML en minúsculas (salvo iframe/frame, donde es el nombre accesible) o en un componente que reenvía sus props al DOM (PASAN_AL_DOM: Table*, Badge, Label, Button). Motivo: en Linux el tooltip nativo de title lo dibuja GTK con los colores del sistema y parece una ventana ajena; barridos a mano, volvieron en un día. La herramienta correcta es Tooltip (src/components/ui/tooltip.tsx) o WithTooltip. Una red de seguridad en ejecución, TooltipsNativos (src/components/tooltips-nativos.tsx), convierte los que se cuelen. Los componentes propios con una prop title o titulo (por ejemplo SettingCard) no se señalan porque no renderizan un atributo nativo.

Knip (pnpm check:knip)

Configuración en knip.json: las entradas son scripts/**/*.{ts,mjs,js} y src/**/*.test.{ts,tsx}; los ficheros del proyecto, src/**/*.{ts,tsx} y scripts/**/*.{ts,mjs,js}. Los exports, tipos, miembros de enum y duplicados sin usar están desactivados (rules), así que Knip va de ficheros y dependencias sin usar. src/bindings/** se ignora; tw-animate-css y tailwindcss son dependencias en la lista blanca (se consumen desde CSS), igual que @iconify/json (solo la lee scripts/iconos.mjs, desde node_modules), y los binarios rustfmt (configuración de lint-staged) e inkscape (scripts/emoji-noto.mjs) están en la lista blanca.

Git hook (red de seguridad local)

.husky/pre-commit ejecuta lint-staged (arregla automáticamente los ficheros en staged) y luego, según lo que haya en staged, tsc --noEmit + idiomas + check:title para TypeScript y check:clippy para Rust. Es rápido a propósito; vitest, knip, cargo test y check:biome sobre todo el repo se dejan para pnpm check y CI. Detalles en Preparación del entorno de desarrollo.

GitHub Actions

ci.yml (nombre ci)

Disparadores: push a main y cada pull_request. Un solo job, checks, en ubuntu-22.04:

  1. actions/checkout@v4.
  2. Instalar las bibliotecas de Linux de Tauri (libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf) con awalsh128/cache-apt-pkgs-action@v1, que guarda en caché los .deb: el mirror de Ubuntu a veces baja a unos 50 kB/s y esos 55 MB tardaban hasta 20 minutos, cuando el resto del CI tarda unos 3. (release.yml sigue usando un apt-get install normal.)
  3. pnpm/action-setup@v4 (versión tomada de packageManager), actions/setup-node@v4 (Node 22, caché de pnpm).
  4. dtolnay/rust-toolchain@stable con clippy y rustfmt, y swatinem/rust-cache@v2 (workspace src-tauri -> target).
  5. pnpm install --frozen-lockfile.
  6. pnpm check, pnpm check:rust-fmt, pnpm check:clippy, pnpm test:rust.
  7. Paso «Sin OpenSSL en el árbol» (el texto del workflow está en español): ! cargo tree --manifest-path src-tauri/Cargo.toml -i openssl 2>/dev/null solo pasa cuando el crate no está en el árbol. Por qué: la red es reqwest con rustls (default-features = false); OpenSSL complicaría las compilaciones multiplataforma y los instaladores.

Como el toolchain es la última stable (no fijada), una nueva versión de Rust puede introducir avisos nuevos de clippy y hacer fallar CI sin ningún cambio de código; el mantenedor ejecuta el clippy de la última stable antes de enviar cambios.

release.yml (nombre release)

Solo manual (workflow_dispatch), a propósito sin disparar por etiquetas: publicar el borrador crea la etiqueta, y un disparador por etiqueta lo reconstruía todo otra vez. Permisos contents: write. Una matriz con fail-fast: false:

Plataforma args
ubuntu-22.04 ninguno
windows-latest ninguno
macos-latest --target aarch64-apple-darwin
macos-latest --target x86_64-apple-darwin

Cada entrada de la matriz: checkout, bibliotecas de Linux (solo en Linux), pnpm, Node 22, Rust stable (con los dos targets de Apple en macOS), caché, pnpm install --frozen-lockfile, y después tauri-apps/tauri-action@v0 con tagName: v__VERSION__, releaseDraft: true y los secretos de firma. No ejecuta las puertas de tests: scripts/release.mjs espera a la ejecución de CI del commit de la versión antes de lanzarlo. Todo el detalle en Versiones y actualizaciones automáticas.

Tras la matriz hay un quinto trabajo, android (ubuntu-24.04, needs: build): Java 21, plataforma 37 del SDK de Android, build-tools 36.0.0 y NDK 28.2 con sdkmanager, Rust con aarch64-linux-android, una clave de firma escrita desde los secretos ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD y ANDROID_KEY_BASE64 (el trabajo falla antes si falta alguno y borra la clave al final), pnpm tauri android build --apk --target aarch64 --ci (con los símbolos recortados para bajar la librería de unos 100 MB a 72 MB), comprueba que el APK está firmado (apksigner verify) y lleva el versionCode esperado, lo firma para el actualizador con --app-version y sube el APK y su .sig al borrador. No toca latest.json. ci.yml nunca compila Android: solo lo hace este trabajo, y solo en una release, así que su primera ejecución en GitHub es la primera prueba de su YAML.

Auditoría de dependencias

.github/workflows/auditoria.yml pasa cargo audit y pnpm audit --prod los lunes y a petición, aparte del CI: un aviso nuevo de una dependencia no pone en rojo un push que no ha tocado nada ni frena una release. Las excepciones están en src-tauri/.cargo/audit.toml, cada una con su motivo: hoy, las cuatro de zip 4, que solo trae tauri-plugin-updater y que solo abre paquetes tras comprobar su firma minisign. Se quitan cuando el plugin cambie de versión.

Todas las acciones van fijadas por SHA con la versión en un comentario (una etiqueta se puede mover, y un workflow con las claves de firma no debería seguirla). dtolnay/rust-toolchain necesita entonces toolchain: stable escrito, porque si no saca la versión de la referencia. .github/dependabot.yml propone las acciones cada mes y, de cargo y npm, solo arreglos de seguridad (que además hay que activar en los ajustes del repositorio).

Dónde añadir tests

Si cambias Añade un test en
Lógica pura (análisis, geometría, imagen, estadísticas, diseñador) src-tauri/crates/proxy-core/src/<módulo>.rs (#[cfg(test)]), o un dorado si es visual
Un cambio visual en el renderizado de cartas Actualiza los dorados de forma deliberada (véase arriba)
Un comando de Tauri o el comportamiento del store/BD El #[cfg(test)] del módulo en src-tauri/src/..., usando un AppState temporal
Parsers de red Un fixture guardado junto al módulo, más un test en vivo con #[ignore]
Una acción de la mesa de juego (variante de Accion/Cambio) Su caso en crates/mesa/tests/acciones.rs (sin él el match exhaustivo no compila), más subir VERSION_CATALOGO y su tests/catalogo/v<N>.json
Un cambio en cómo se firman las peticiones de amigos o cómo se deriva la clave de cifrado Los vectores compartidos de src-tauri/src/amigos/vectores/ y el mismo cambio en los tests/vectores/ del servidor (tienen que ser iguales)
Lógica de stores/lib del frontend __tests__/ junto al código, simulando @/lib/ipc
Algo que solo hace un móvil (una rama cfg(mobile), un fichero como content://) Un test de Rust contra un simulado (Acceso en fichero.rs) y pnpm check:clippy-android para la rama en sí; la parte de Kotlin se prueba en el emulador
Un manejador solo de ratón Un comentario // táctil: ... (la puerta) y una función pura para el gesto, probada con Vitest
Una cadena nueva de la interfaz Una entrada en el src/i18n/<código>/<área>.json correspondiente de cada idioma (la puerta te lo dirá)

Véase también

Documentation · Documentación

Language: English · Español

Use and build · Uso y desarrollo

Architecture · Arquitectura

Data · Datos

Getting cards · Conseguir cartas

Layout, images, PDF · Maquetación, imagen y PDF

Card designer · Diseñador de cartas

Game table · Mesa de juego

Mobile · Móvil

Frontend · Frontend

Reference · Referencia

Clone this wiki locally