Newer
Older
rpg / packages / engine / src / inventory / __tests__ / Inventory.test.ts
import { describe, expect, it, vi } from 'vitest';
import { Inventory, inventoryFromData } from '../Inventory';

describe('Inventory', () => {
    it('добавляет и считает предметы', () => {
        const inv = new Inventory();
        expect(inv.empty).toBe(true);
        inv.add('cloth');
        inv.add('bellflower', 2);
        expect(inv.count('cloth')).toBe(1);
        expect(inv.count('bellflower')).toBe(2);
        expect(inv.has('cloth')).toBe(true);
        expect(inv.has('salt')).toBe(false);
        expect(inv.empty).toBe(false);
    });

    it('remove вычитает и чистит пустые слоты', () => {
        const inv = new Inventory();
        inv.add('bellflower', 3);
        expect(inv.remove('bellflower', 2)).toBe(2);
        expect(inv.count('bellflower')).toBe(1);
        expect(inv.remove('bellflower', 5)).toBe(1); // нельзя убрать больше, чем есть
        expect(inv.count('bellflower')).toBe(0);
        expect(inv.has('bellflower')).toBe(false);
        expect(inv.remove('cloth')).toBe(0); // нет предмета — ничего
    });

    it('add с неположительным n — no-op', () => {
        const inv = new Inventory();
        inv.add('salt', 0);
        inv.add('salt', -1);
        expect(inv.empty).toBe(true);
    });

    it('onChange оповещает о каждом изменении, отписка работает', () => {
        const inv = new Inventory();
        const fn = vi.fn();
        const off = inv.onChange(fn);
        inv.add('cloth');
        inv.add('cloth');
        inv.remove('cloth');
        off();
        inv.add('cloth');
        expect(fn).toHaveBeenCalledTimes(3);
    });

    it('serialize/load round-trip; load игнорирует мусор', () => {
        const inv = new Inventory();
        inv.add('cloth');
        inv.add('bellflower', 3);
        const data = inv.serialize();
        const back = inventoryFromData(JSON.parse(JSON.stringify(data)));
        expect(back.all).toEqual([
            { id: 'cloth', count: 1 },
            { id: 'bellflower', count: 3 }
        ]);
        back.load({ items: { salt: 0, bad: -2, cloth: 2 } });
        expect(back.count('salt')).toBe(0);
        expect(back.count('bad')).toBe(0);
        expect(back.count('cloth')).toBe(2);
    });

    it('clear сбрасывает содержимое', () => {
        const inv = new Inventory();
        inv.add('cloth');
        inv.clear();
        expect(inv.empty).toBe(true);
    });

    it('maxPerStack обрезает добавление и возвращает факт', () => {
        const inv = new Inventory({ maxPerStack: 3 });
        expect(inv.add('bellflower', 5)).toBe(3);
        expect(inv.count('bellflower')).toBe(3);
        expect(inv.add('bellflower', 1)).toBe(0); // стак полон
        expect(inv.spaceFor('bellflower')).toBe(0);
        expect(inv.spaceFor('salt')).toBe(3);
    });

    it('maxSlots не даёт заводить новый слот, но стек в существующий идёт', () => {
        const inv = new Inventory({ maxSlots: 2 });
        inv.add('cloth');
        inv.add('salt');
        expect(inv.add('bellflower')).toBe(0); // слотов нет
        expect(inv.has('bellflower')).toBe(false);
        expect(inv.add('salt', 2)).toBe(2); // свой слот не лимитируем
    });

    it('load клампит под лимиты', () => {
        const inv = new Inventory({ maxSlots: 2, maxPerStack: 5 });
        inv.load({ items: { a: 9, b: 2, c: 1 } });
        expect(inv.all).toEqual([
            { id: 'a', count: 5 },
            { id: 'b', count: 2 }
        ]);
        expect(inv.size).toBe(2);
    });

    it('событие несёт деталь изменения для всех kind', () => {
        const inv = new Inventory();
        const fn = vi.fn();
        inv.onChange(fn);
        inv.add('cloth', 2);
        expect(fn).toHaveBeenLastCalledWith({ kind: 'add', id: 'cloth', count: 2, after: 2 });
        inv.remove('cloth');
        expect(fn).toHaveBeenLastCalledWith({ kind: 'remove', id: 'cloth', count: -1, after: 1 });
        inv.remove('cloth', 1);
        expect(fn).toHaveBeenLastCalledWith({ kind: 'remove', id: 'cloth', count: -1, after: 0 });
        inv.clear(); // пусто — clear не эмитится
        inv.add('salt');
        inv.load({ items: {} });
        expect(fn).toHaveBeenLastCalledWith({ kind: 'load', id: '', count: 0, after: 0 });
        inv.add('salt');
        inv.clear();
        expect(fn).toHaveBeenLastCalledWith({ kind: 'clear', id: '', count: 0, after: 0 });
    });

    it('генерик типизирует id (компиляция)', () => {
        type Item = 'a' | 'b';
        const inv = new Inventory<Item>();
        inv.add('a', 2);
        expect(inv.count('a')).toBe(2);
    });
});