Skip to content

Testing and CI

Pablo Martínez López edited this page Oct 21, 2026 · 12 revisions

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.

The gates at a glance

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
Loading
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.

Frontend tests (Vitest)

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).

Rust tests

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) and src-tauri/src/scryfall/fixtures/.
  • Temp state. Tests that need an AppState build one over a tempfile::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) and stats. 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.

Updater configuration guard

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.

Golden tests (designer rendering)

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 by Motor::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> in layouts.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_bytes asserts two renders of one case are byte-identical (the PDF and preview rely on determinism); cambiar_un_color_del_pack_cambia_el_dorado proves a pack palette edit changes the output (guards against the pack colours being ignored).
  • las_cartas_grandes_salen_a_su_medida checks large cards (planes, schemes, Vanguard) render at their own size.

Compatibility with older versions (design languages)

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.json is 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 keeps es and en first, an older two-field struct still reads it, and an unknown language key is ignored.

See Designer Engine for ByLang and the fingerprint.

Ignored (manual) tests and their environment variables

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].

The check scripts

scripts/idiomas.mjs (pnpm idiomas)

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:

  1. Collects the requested strings: every double-quoted literal in t("...") and paraTraducir("...") under src/**/*.ts(x) (skipping bindings, node_modules, dot folders, *.test.ts(x) and src/i18n/ itself).
  2. Reads the language table from src/lib/idiomas.ts and loads each dictionary folder by transpiling its .ts files with typescript (so values, including plural objects, are real data, not regex matches). Folders starting with _ (such as __tests__) are skipped.
  3. 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's Intl.PluralRules(locale). A folder that is not a language of the table is also an error.
  4. Warns (without failing) about entries nobody requests and values identical to English (ignoring brand names in the script's IDENTICAS_PERMITIDAS and the keys listed in an optional src/i18n/<code>/identicas.json). --detalle prints 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.

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

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.

Knip (pnpm check:knip)

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.

Git hook (local safety net)

.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.

GitHub Actions

ci.yml (name ci)

Triggers: push to main and every pull_request. One job, checks, on ubuntu-22.04:

  1. actions/checkout@v4.
  2. Install Tauri's Linux libraries (libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf).
  3. pnpm/action-setup@v4 (version from packageManager), actions/setup-node@v4 (Node 22, pnpm cache).
  4. dtolnay/rust-toolchain@stable with clippy and rustfmt, and 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. Step "Sin OpenSSL en el arbol" (the workflow text is Spanish): ! cargo tree --manifest-path src-tauri/Cargo.toml -i openssl 2>/dev/null passes only when the crate is absent from the tree. Why: networking is reqwest with rustls (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.

release.yml (name release)

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.

Where to add tests

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)

See also

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

Frontend · Frontend

Reference · Referencia

Clone this wiki locally