Skip to content

Testing and CI

Pablo Martínez López edited this page Oct 20, 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 string passed to t() has no English entry, or one string is in two dictionary files
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, image-url, imagen-viva, tema-inicial, temas-instalados, trabajos (the listen-before-launch job helper)
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)

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/
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()

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.

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_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 the English dictionary (src/i18n/en/*.ts, merged in index.ts) is keyed by the Spanish source string. Because t() silently falls back to Spanish when a key is missing, a missing translation would compile and render fine and would only be visible with the app in English; this script is the way to notice. It:

  1. Reads every src/i18n/en/*.ts (except index.ts) and collects keys, reporting any key present in two files (the merge would silently let the last one win).
  2. Walks src/**/*.ts(x) (skipping bindings, node_modules, tests and the dictionary itself) and extracts every string literal in t("...") and paraTraducir("...").
  3. Fails listing each requested string with no English key and the file that asked for it.

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/en/<area>.ts (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