Repository navigation
Testing and CI
English · Español
What is tested, how, and which gate catches what: Vitest for the frontend and Node scripts, cargo test for Rust (including image-comparison golden tests for the card designer), the custom check scripts, Knip, the git hook, and the GitHub Actions workflows.
The local gates and CI run the same commands, so a green pnpm check plus the three Rust commands predicts a green CI. Setup is in Development Setup; conventions that the gates enforce are in Conventions.
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 --> I["idiomas: idiomas.mjs"]
C --> K["check:knip: knip"]
R1["check:rust-fmt: cargo fmt --check"]
R2["check:clippy: clippy -D warnings"]
R3["test:rust: cargo test --workspace"]
O["no OpenSSL: cargo tree -i openssl is empty"]
CI["CI job 'checks'"] --> C
CI --> R1
CI --> R2
CI --> R3
CI --> O
| Gate | Command | Fails when |
|---|---|---|
| Types |
pnpm check:tipos (tsc --noEmit) |
TypeScript errors, unused locals/parameters (strict, noUnusedLocals, noUnusedParameters in tsconfig.json) |
| Unit tests |
pnpm test (vitest run) |
A test fails |
| Lint + format |
pnpm check:biome (biome check .) |
Lint error or wrong formatting (tabs, quotes, line width 100; see biome.json) |
| Native tooltips | pnpm check:title |
A .tsx puts title= on an HTML element or a pass-through component |
| Translations | pnpm idiomas |
A language of src/lib/idiomas.ts has no dictionary, a string passed to t() is missing in one, one string is in two area files, placeholders or plural forms do not match (see below) |
| Dead code |
pnpm check:knip (knip) |
Unused files or dependencies |
| Rust format | pnpm check:rust-fmt |
cargo fmt --check reports differences |
| Rust lints | pnpm check:clippy |
Any clippy warning (-D warnings, all targets, whole workspace), including unwrap/expect outside tests |
| Rust tests | pnpm test:rust |
Any test fails (this also runs the ts-rs export tests, see Development Setup) |
| No OpenSSL | CI step only |
cargo tree -i openssl finds the crate |
pnpm check runs its parts in the order tipos, test, biome, title, idiomas, knip (package.json scripts.check); title and idiomas run with --silent so only their own messages show.
Config: vitest.config.ts uses the node environment (no DOM), alias @ to src, restoreMocks: true, and picks up src/**/*.test.ts, src/**/*.test.tsx and scripts/**/*.test.mjs. happy-dom is a dev dependency but the default environment is node.
Philosophy: React components are not rendered in tests. The tested code is the logic that was deliberately pulled out of components into plain modules and stores, which are given a mocked @/lib/ipc. Typical pattern (see src/stores/__tests__/actualizacion-store.test.ts): vi.mock("@/lib/ipc", ...) fakes call and on with a switch on the command name and throws on an unexpected command, then asserts the sequence of commands the store issued.
| Folder | Tests (files) |
|---|---|
src/stores/__tests__/ |
deck-store, deck-store-abrir, ui-store, plan-store, trabajo-store, actualizacion-store
|
src/lib/__tests__/ |
cartas, deck-ops, dialogos, errores, idiomas (the language table), image-url, imagen-viva, tema-inicial, temas-instalados, trabajos (the listen-before-launch job helper) |
src/i18n/__tests__/ |
traducir (fallback language, English, Spanish; Russian plural forms by n; non-number n uses other; numbers formatted in the UI language; cargarDiccionario) |
src/components/__tests__/ |
emoji (every language's flag has an image; rendered to a string with react-dom/server, no DOM) |
src/features/decklist/__tests__/ |
acciones |
src/features/export/__tests__/ |
acciones |
src/features/library/__tests__/ |
organizar (filter, sort, group, selection) |
src/features/preview/__tests__/ |
acciones-carta, arrastrar-ficheros
|
src/features/variants/__tests__/ |
vincular |
src/features/card-editor/__tests__/ |
propagar (apply to card / copies / all) |
src/features/disenar/__tests__/ |
estilos, encuadre, grupos, renumerar, textos
|
src/features/stats/__tests__/ |
textos |
scripts/__tests__/ |
release-notas.test.mjs (argument parsing, release notes template and cleaning, latest.json notes rewriting), idiomas.test.mjs (comprobar: missing keys, changed placeholders, missing or foreign plural categories, plural without {n}, warnings, an area not merged by index.ts) |
Gotcha: the scroll and animation behaviour of the real UI is not covered by any automated test; it has to be tried by hand in WebKitGTK (see Development Setup).
pnpm test:rust runs cargo test --workspace for two crates: the app (proxy-builder, library proxy_builder_lib) and the pure core proxy-core. As of this version there are several hundred #[test]/#[tokio::test] functions spread over roughly 90 modules (grep the tree for exact numbers). Dev-dependencies: tempfile, lopdf (inspects generated PDFs), and tokio with test-util.
Rules visible in the code:
-
No network in the default run. Anything that needs the live internet (Scryfall, GitHub, Google Fonts, MTGJSON, Cockatrice, mtg.io) is
#[ignore]and meant for manual runs. Parsers are tested against saved fixtures:src-tauri/src/fixtures/(archidekt-1.json,cockatrice-tokens.xml,mtgio-muro-de-algas.json,tweakcn-amethyst-haze.json) andsrc-tauri/src/scryfall/fixtures/. -
Temp state. Tests that need an
AppStatebuild one over atempfile::tempdir()(data and cache paths), never the real app folders. -
Tests may unwrap.
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]only applies outside tests; integration test files add#![allow(clippy::unwrap_used, clippy::expect_used)]. -
Where the volume is. Most tests sit in
proxy-core: the designer (diseno/render.rs,componer.rs,cuerpos.rs,texto.rs,pack.rs,pintar.rs,estructura.rs), layout (layout/plan.rs,grid.rs), decklist parsing and export (decklist/), imaging (imaging/frame.rs,corners.rs,adjust.rs) andstats. In the app crate:scryfall/resolve.rs,commands/cards.rs,commands/stats.rs,render.rs,store/decks.rs,package.rs,commands/export.rs,commands/identify.rs.
src-tauri/src/actualizar.rs has a test, la_configuracion_del_actualizador_es_valida, that reads tauri.conf.json and asserts createUpdaterArtifacts is true, requireSignedVersion is true, the endpoint is a GitHub releases/latest/download/latest.json URL, and the public key decodes to a minisign key with the expected key id. Reason: breaking any of these silently leaves every installed copy without updates. See Releases and Auto-Updates.
Location: src-tauri/crates/proxy-core/tests/.
| File | What |
|---|---|
dorados.rs |
Base cases per style, each rendered with the real engine and compared to a stored PNG in tests/dorados/; also dorados_en_otros_idiomas (idioma-fr.png, idioma-ru.png, idioma-ro.png: Latin with diacritics, Cyrillic and a language without printed cards, asserting no FaltanLetras and SinTextoOficial only for Romanian) |
layouts.rs |
One golden per special Magic layout in tests/dorados/layouts/ (planeswalker, saga, class, case, split, flip, transform, MDFC, battle, meld, dungeon, plane, vanguard...) using real card data saved in tests/dorados/layouts/datos/*.json
|
comun/mod.rs |
Shared helpers: synthetic art (arte(seed)), emblem, symbols drawn locally, and diferencia()
|
compatibilidad/diseno-2.5.9.json |
A design as saved by version 2.5.9, used by the compatibility tests below |
Design points:
-
Test assets are generated in code (gradient-and-circles art, a blue oval emblem, coloured-circle symbols) so no copyrighted art is committed. Fonts and the house frame pack come from the real
assets/(the engine constructed byMotor::de_fabrica()). -
Comparison:
diferencia(a, b)returns the percentage of pixels where any channel differs by more than 8/255; sizes that differ count as 100 %. A case fails at 0.5 % or more (dorados_de_los_casos_base). On failure the new render is written to the OS temp directory as<case>-nuevo.png(nuevo-<name>inlayouts.rs) for inspection, and the test lists all failing cases together. - First run creates, later runs compare. If a golden file does not exist it is written and the case passes.
-
Updating goldens:
PB_ACTUALIZAR_DORADOS=1 cargo test -p proxy-core --test dorados(or--test layouts) rewrites them. Only do this when the visual change is intended and approved; review the diff in the PNGs before committing. -
Determinism tests:
dos_renders_iguales_dan_los_mismos_bytesasserts two renders of one case are byte-identical (the PDF and preview rely on determinism);cambiar_un_color_del_pack_cambia_el_doradoproves a pack palette edit changes the output (guards against the pack colours being ignored). -
las_cartas_grandes_salen_a_su_medidachecks large cards (planes, schemes, Vanguard) render at their own size.
Adding languages must not change what Spanish and English designs already are, on disk or on screen. Three tests pin that down:
-
diseno::efectivo::tests::las_huellas_en_espanol_e_ingles_son_las_de_antes: fixed render hashes (huella) taken from 2.5.9 for Spanish and English. If they changed, every existing designed card would be re-rendered. -
diseno::modelo::tests::un_diseno_de_antes_se_reescribe_igual:tests/compatibilidad/diseno-2.5.9.jsonis read and written back byte-identical (so saving a deck in this version does not rewrite older designs). - In the same module,
otro_idioma_se_guarda_tras_es_y_en_y_lo_lee_la_version_de_antes: a design with another language keepsesandenfirst, an older two-field struct still reads it, and an unknown language key is ignored.
See Designer Engine for ByLang and the fingerprint.
Run with cargo test <path> -- --ignored --nocapture. They need local data or the network and are never part of CI.
| Variable | Used by | Meaning |
|---|---|---|
PB_ACTUALIZAR_DORADOS |
tests/dorados.rs, tests/layouts.rs
|
Rewrite goldens instead of comparing |
PB_CJK |
dorados::dorado_en_japones |
Path of a Noto Sans JP font file (the app downloads it apart, so it is not in the repo); compares with idioma-ja.png. Without the font the render carries FaltanLetras. |
PB_BALATRO |
tests/comun pack_balatro(), layouts::balatro_matriz, dorados::balatro_*
|
Folder of the Balatro frame pack (the separate packs repository) |
PB_SALIDA |
same | Output folder for contact sheets (defaults to a temp dir) |
PB_ORIGINAL |
dorados::original_con_sus_imagenes, original_colores
|
Folder with the downloaded "Original" frame images |
PB_GF |
dorados::letras_libres_a_ojo, diseno/letras_libres.rs
|
Folder of the free Google Fonts files |
PB_AUTOR |
dorados::balatro_contra_el_autor |
The pack author's reference renders to compare against |
PB_OCR_DATOS, PB_OCR_IMG
|
ocr.rs, originales.rs
|
OCR model folder and a test image |
PB_ID_DIR |
commands/identify.rs |
Folder of images for batch identification |
PB_IMG |
imaging/corners.rs en_vivo
|
An image to measure the corner fill |
PB_DB, PB_DECK
|
scryfall/fichas_auto.rs en_vivo
|
A copy of a library DB and a deck name for token detection |
PB_PDF_DE_PRUEBA |
commands/export.rs de_punta_a_punta
|
Where to leave the end-to-end PDF |
PB_PACKS_LOCAL |
diseno/descargables.rs |
Debug builds only: load downloadable packs from a local folder instead of GitHub (not just a test variable) |
Other #[ignore] examples: live downloads (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) are runnable viewers, not tests: cargo run --release -p proxy-core --example disenar -- <symbols> <arts> <out> [width].
Guards the i18n contract: UI text is written in Spanish inside t("..."), and every dictionary (src/i18n/<code>/<area>.ts, merged in that folder's index.ts) is keyed by the Spanish source string. Because t() silently falls back (language, then English, then the Spanish source), a missing translation compiles and renders fine and would only be visible with the app in that language; this script is the way to notice. It:
- Collects the requested strings: every double-quoted literal in
t("...")andparaTraducir("...")undersrc/**/*.ts(x)(skippingbindings,node_modules, dot folders,*.test.ts(x)andsrc/i18n/itself). - Reads the language table from
src/lib/idiomas.tsand loads each dictionary folder by transpiling its.tsfiles withtypescript(so values, including plural objects, are real data, not regex matches). Folders starting with_(such as__tests__) are skipped. - For every language except Spanish, fails when: its folder is missing; a requested string has no entry; a key is in two areas (the merge would let the last win silently); an area of the English dictionary is missing or not merged by
index.ts; the{x}placeholders differ from the Spanish key (also in each plural form); a plural object sits on a key without{n}; or a plural lacks, or adds, a category of that language'sIntl.PluralRules(locale). A folder that is not a language of the table is also an error. -
Warns (without failing) about entries nobody requests and values identical to English (ignoring brand names in the script's
IDENTICAS_PERMITIDASand the keys listed in an optionalsrc/i18n/<code>/identicas.json).--detalleprints each warning; by default only the count shows.
Its pure parts (leerPedidas, leerTabla, leerDiccionarios, comprobar) are exported and tested in scripts/__tests__/idiomas.test.mjs.
Gotchas: only double-quoted literals directly inside t(...) are seen; a t( with a template literal or a variable is not checked. paraTraducir("...") (identity function, src/components/language-provider.tsx) exists to mark texts that live in data tables and are translated elsewhere (for example TEXTOS in src/lib/errores.ts). See Internationalization.
Fails if a .tsx under src/ sets title= on a lowercase HTML tag (except iframe/frame, where it is the accessible name) or on a component that forwards its props to the DOM (PASAN_AL_DOM: Table*, Badge, Label, Button). Reason: on Linux the native title tooltip is drawn by GTK with system colours and looks like a foreign window; swept clean by hand it came back within a day. The right tool is Tooltip (src/components/ui/tooltip.tsx) or WithTooltip. A runtime backstop, TooltipsNativos (src/components/tooltips-nativos.tsx), converts any that slip through. Own components with a title or titulo prop (for example SettingCard) are not flagged because they do not render a native attribute.
Config knip.json: entries are scripts/** and src/**/*.test.{ts,tsx}; project files src/** and scripts/**. Unused exports, types and enum members are off (rules), so Knip is about unused files and dependencies. src/bindings/** is ignored; tw-animate-css and tailwindcss are whitelisted dependencies (consumed from CSS), and the rustfmt binary is whitelisted for the lint-staged config.
.husky/pre-commit runs lint-staged (auto-fix staged files), then, depending on what is staged, tsc --noEmit + idiomas + check:title for TypeScript and check:clippy for Rust. It is intentionally fast; vitest, knip, cargo test and check:biome over the whole repo are left to pnpm check and CI. Details in Development Setup.
Triggers: push to main and every pull_request. One job, checks, on ubuntu-22.04:
-
actions/checkout@v4. - Install Tauri's Linux libraries (
libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf). -
pnpm/action-setup@v4(version frompackageManager),actions/setup-node@v4(Node 22, pnpm cache). -
dtolnay/rust-toolchain@stablewithclippyandrustfmt, andswatinem/rust-cache@v2(workspacesrc-tauri -> target). -
pnpm install --frozen-lockfile. -
pnpm check,pnpm check:rust-fmt,pnpm check:clippy,pnpm test:rust. - Step "Sin OpenSSL en el arbol" (the workflow text is Spanish):
! cargo tree --manifest-path src-tauri/Cargo.toml -i openssl 2>/dev/nullpasses only when the crate is absent from the tree. Why: networking isreqwestwithrustls(default-features = false); OpenSSL would complicate cross-platform builds and installers.
Because the toolchain is latest stable (not pinned), a new Rust release can introduce new clippy warnings and fail CI without any code change; the maintainer runs the latest-stable clippy before sending changes.
Manual only (workflow_dispatch), deliberately not triggered by tags: publishing the draft creates the tag, and a tag trigger rebuilt everything again. Permissions contents: write. A matrix with fail-fast: false:
| Platform | args |
|---|---|
ubuntu-22.04 |
none |
windows-latest |
none |
macos-latest |
--target aarch64-apple-darwin |
macos-latest |
--target x86_64-apple-darwin |
Each leg: checkout, Linux libs (Linux only), pnpm, Node 22, Rust stable (with both Apple targets on macOS), cache, pnpm install --frozen-lockfile, then tauri-apps/tauri-action@v0 with tagName: v__VERSION__, releaseDraft: true, and the signing secrets. It does not run the test gates: scripts/release.mjs waits for the CI run of the release commit before launching it. Full detail in Releases and Auto-Updates.
| Changing | Add a test in |
|---|---|
| Pure logic (parsing, geometry, imaging, stats, designer) |
src-tauri/crates/proxy-core/src/<module>.rs (#[cfg(test)]), or a golden if it is visual |
| A visual change in card rendering | Update goldens deliberately (see above) |
| A Tauri command or store/DB behaviour | The module's #[cfg(test)] in src-tauri/src/..., using a temp AppState
|
| Network parsers | A saved fixture next to the module, plus an #[ignore] live test |
| Frontend store/lib logic |
__tests__/ next to the code, mocking @/lib/ipc
|
| A new UI string | An entry in the matching src/i18n/<code>/<area>.ts of every language (the gate will tell you) |
Use and build · Uso y desarrollo
- User-Guide · Guía de usuario
- Development-Setup · Preparación del entorno de desarrollo
- Testing-and-CI · Tests y CI
- Releases-and-Auto-Updates · Versiones y actualizaciones automáticas
- Conventions · Convenciones
Architecture · Arquitectura
- Architecture · Arquitectura
- IPC-and-Error-Handling · IPC y manejo de errores
- Rust-App-Layer · Capa de aplicación Rust
Data · Datos
- Deck-Model · Modelo del mazo
- Storage-and-Blobs · Almacenamiento y blobs
- Deck-Package-Format · Formato del paquete de mazo
- Deck-Library · Biblioteca de mazos
Getting cards · Conseguir cartas
- Decklist-Parsing · Parseo de la lista del mazo
- Scryfall-Integration · Integración con Scryfall
- External-Sources · Fuentes externas
- Card-Identification-and-OCR · Identificación de cartas y OCR
Layout, images, PDF · Maquetación, imagen y PDF
- Layout-Engine · Motor de maquetación
- Imaging-and-PDF · Imagen y PDF
- Export · Exportación
- Live-Preview · Vista previa en vivo
- Statistics · Estadísticas
Card designer · Diseñador de cartas
- Designer-Overview · Visión general del diseñador
- Designer-Engine · Motor del diseñador
- Designer-Composition · Composición del diseñador
- Frame-Packs · Packs de marcos
- Designer-App-Layer · Capa de aplicación del diseñador
- Designer-UI · Interfaz del diseñador
- Card-Editor-Dialog · Diálogo Editar carta
Game table · Mesa de juego
Frontend · Frontend
- Frontend-Architecture · Arquitectura del frontend
- State-Stores · Stores de estado
- Internationalization · Internacionalización
- Themes-and-Skins · Temas y skins
- Icons · Iconos
- Settings-and-Shell · Ajustes y shell
- Tutorials · Tutoriales
- Decklist-Panel · Panel de la lista del mazo
- Add-Card-and-Variants · Añadir carta y variantes
- Set-Browser · Explorador de sets
Reference · Referencia