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(); }); }); // --- Synthetic Gen 2 (Crystal) SRAM --------------------------------- // Offsets mirror src/lib/savedex.js `G2.cr`. const G2 = { owned: 0x2a27, seen: 0x2a47, unown: 0x2a67, ck: 0x2d0d, ckFrom: 0x2009, ckTo: 0x2b82 }; function makeGen2Save({ caught, seen, unown = [] }) { const bytes = new Uint8Array(0x8000); setBits(bytes, G2.owned, caught); setBits(bytes, G2.seen, seen); unown.forEach((letter, i) => (bytes[G2.unown + i] = letter)); // 1 = A … 26 = Z let sum = 0; for (let i = G2.ckFrom; i <= G2.ckTo; i++) sum = (sum + bytes[i]) & 0xffffffff; bytes[G2.ck] = sum & 0xff; bytes[G2.ck + 1] = (sum >> 8) & 0xff; return bytes.buffer; } describe('parseSaveDex — Gen 2 Unown Report', () => { it('turns the caught-letter list into Form Dex slugs', () => { const dex = parseSaveDex( makeGen2Save({ caught: [1, 25], seen: [1, 25, 201], unown: [3, 1, 20] }), // C, A, T ); expect(dex.gen).toBe(2); expect(dex.game).toBe('Crystal'); expect(dex.checksumOk).toBe(true); expect([...dex.formCaught].sort()).toEqual(['unown', 'unown-c', 'unown-t']); }); it('no Unown caught → empty list', () => { const dex = parseSaveDex(makeGen2Save({ caught: [1], seen: [1], unown: [] })); expect(dex.formCaught).toEqual([]); }); it('ignores a garbage list rather than ticking wrong boxes', () => { const dex = parseSaveDex(makeGen2Save({ caught: [1], seen: [1], unown: [3, 99, 5] })); expect(dex.formCaught).toEqual([]); }); });