dex/test/type-chart.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

66 lines
2.6 KiB
JavaScript

import { describe, it, expect } from 'vitest';
import { multiplier, defensiveMatchups } from '../src/data/type-chart.js';
describe('multiplier — modern (Gen 9)', () => {
it('single-type effectiveness', () => {
expect(multiplier('fire', ['grass'])).toBe(2);
expect(multiplier('fire', ['water'])).toBe(0.5);
expect(multiplier('normal', ['ghost'])).toBe(0);
expect(multiplier('ghost', ['normal'])).toBe(0);
expect(multiplier('dragon', ['fairy'])).toBe(0);
});
it('stacks across a dual type', () => {
expect(multiplier('water', ['ground', 'rock'])).toBe(4);
expect(multiplier('grass', ['water', 'ground'])).toBe(4);
expect(multiplier('fighting', ['normal', 'flying'])).toBe(1); // 2 * 0.5
expect(multiplier('ground', ['flying', 'steel'])).toBe(0); // immunity wins
});
it('ghost hits steel neutrally from Gen 6 on', () => {
expect(multiplier('ghost', ['steel'], 9)).toBe(1);
});
});
describe('multiplier — era rules', () => {
it('Fairy does not exist before Gen 6 (defender ignored)', () => {
expect(multiplier('dragon', ['fairy'], 5)).toBe(1);
expect(multiplier('poison', ['fairy'], 5)).toBe(1);
// still applies in Gen 6+
expect(multiplier('poison', ['fairy'], 6)).toBe(2);
});
it('Steel resisted Ghost and Dark before Gen 6', () => {
expect(multiplier('ghost', ['steel'], 5)).toBe(0.5);
expect(multiplier('dark', ['steel'], 5)).toBe(0.5);
expect(multiplier('ghost', ['steel'], 6)).toBe(1);
});
it('Gen 1 quirks', () => {
expect(multiplier('ghost', ['psychic'], 1)).toBe(0); // the famous bug
expect(multiplier('ghost', ['psychic'], 2)).toBe(2); // fixed in Gen 2
expect(multiplier('bug', ['poison'], 1)).toBe(2);
expect(multiplier('poison', ['bug'], 1)).toBe(2);
expect(multiplier('bug', ['poison'], 2)).toBe(0.5); // modern value
});
});
describe('defensiveMatchups', () => {
it('buckets a dual type by multiplier and omits absent attackers', () => {
// Sableye (Dark/Ghost) modern: immune to Normal/Fighting/Psychic, weak
// only to Fairy (added in Gen 6).
const m = defensiveMatchups(['dark', 'ghost'], 9);
expect(m['0'].sort()).toEqual(['fighting', 'normal', 'psychic'].sort());
expect(m['2']).toEqual(['fairy']);
expect(m['4']).toEqual([]);
expect(m['0.5']).toContain('poison');
// Pre-Gen-2 there is no Dark/Steel attacker, so Fairy is absent too
const g1 = defensiveMatchups(['ghost'], 1);
const flat = Object.values(g1).flat();
expect(flat).not.toContain('dark');
expect(flat).not.toContain('steel');
expect(flat).not.toContain('fairy');
});
});