dex/test/savedex.test.js
chris 3a54cdcc53 Add a Vitest unit suite for the pure-logic modules
31 tests in test/, run in a plain Node env with a tiny localStorage shim
(no jsdom — its Node-22.4 transitive-dep breakage isn't worth working
around for logic tests):

- type-chart: modern effectiveness, dual-type stacking, and the era rules
  (no Fairy pre-6, Steel resists Ghost/Dark pre-6, the Gen 1 Ghost→Psychic
  and Bug↔Poison quirks).
- damage-calc: nature multipliers, the Gen 3+ stat formula (Garchomp
  reference values), STAB/effectiveness/immunity, pre/post-Gen-6 crit.
- savedex: a synthetic Gen 1 SRAM (bitfields + checksum) round-trips;
  checksum-mismatch and size guards.
- selection: normalizeSelection v2→v3 migration, per-game isolation,
  global favourite, stats vs statsNational.
- pokedex-resolver: prettify, dex resolution against a mini snapshot.

`npm test` wired into CI after format:check.

Also: docker-compose host port 8080 → 4753 (README updated).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
2026-09-10 11:33:27 -04:00

62 lines
2.0 KiB
JavaScript

import { describe, it, expect } from 'vitest';
import { parseSaveDex } from '../src/lib/savedex.js';
// --- Synthetic Gen 1 (R/B/Y) SRAM -------------------------------------
// 32 KB, with the owned/seen bitfields and the byte checksum the parser
// verifies. Offsets mirror src/lib/savedex.js `G1`.
const G1 = {
owned: 0x25a3,
seen: 0x25b6,
species: 151,
ckByte: 0x3523,
ckFrom: 0x2598,
ckTo: 0x3522,
};
function setBits(bytes, start, ids) {
for (const n of ids) bytes[start + ((n - 1) >> 3)] |= 1 << ((n - 1) & 7);
}
function makeGen1Save({ caught, seen }) {
const bytes = new Uint8Array(0x8000);
setBits(bytes, G1.owned, caught);
setBits(bytes, G1.seen, seen);
let sum = 0;
for (let i = G1.ckFrom; i <= G1.ckTo; i++) sum = (sum + bytes[i]) >>> 0;
bytes[G1.ckByte] = ~sum & 0xff;
return bytes.buffer;
}
describe('parseSaveDex — Gen 1', () => {
it('reads owned/seen and verifies the checksum', () => {
const caught = [1, 25, 151];
const seen = [1, 4, 25, 151];
const dex = parseSaveDex(makeGen1Save({ caught, seen }));
expect(dex.gen).toBe(1);
expect(dex.game).toBe('Red / Blue / Yellow');
expect(dex.versionGroups).toEqual(['red-blue', 'yellow']);
expect(dex.caught).toEqual(caught);
expect(dex.seen).toEqual(seen);
expect(dex.checksumOk).toBe(true);
expect(dex.nationalUnlocked).toBe(true);
});
it('still parses (checksumOk false) when the checksum byte is wrong', () => {
const buf = makeGen1Save({ caught: [1], seen: [1, 2] });
new Uint8Array(buf)[G1.ckByte] ^= 0xff;
const dex = parseSaveDex(buf);
expect(dex.gen).toBe(1);
expect(dex.caught).toEqual([1]);
expect(dex.checksumOk).toBe(false);
});
it('rejects a file that is too small', () => {
expect(() => parseSaveDex(new Uint8Array(1024).buffer)).toThrow(/too small/i);
});
it('rejects a large file with no recognisable Pokédex', () => {
expect(() => parseSaveDex(new Uint8Array(0x20000).buffer)).toThrow();
});
});