Newer
Older
rpg / packages / engine / src / scene / __tests__ / SceneManager.test.ts
import { describe, expect, it, vi } from 'vitest';
import { SceneManager, type Scene } from '../SceneManager';

/** Сцена-болванка с журналом вызовов (для проверки порядка enter/exit/update). */
function makeScene(name: string) {
    const events: string[] = [];
    const scene: Scene = {
        enter: () => {
            events.push(`${name}:enter`);
        },
        exit: () => {
            events.push(`${name}:exit`);
        },
        update: () => {
            events.push(`${name}:update`);
        },
        render: () => {
            events.push(`${name}:render`);
        }
    };
    return { scene, events };
}

describe('SceneManager', () => {
    it('push без перехода: enter сразу, сцена в стеке и тикает', () => {
        const sm = new SceneManager();
        const a = makeScene('a');
        void sm.push(a.scene);
        expect(sm.current).toBe(a.scene);
        sm.update(0.016);
        expect(a.events).toContain('a:enter');
        expect(a.events).toContain('a:update');
    });

    it('pop без перехода: exit верхней сцены, стек пуст', async () => {
        const sm = new SceneManager();
        const a = makeScene('a');
        void sm.push(a.scene);
        await sm.pop();
        expect(a.events).toContain('a:exit');
        expect(sm.current).toBeUndefined();
    });

    it('replace с переходом: out -> swap (exit/enter) -> in, затем transitioning=false', async () => {
        const sm = new SceneManager();
        const a = makeScene('a');
        const b = makeScene('b');
        await sm.replace(a.scene); // replace асинхронен даже без перехода
        a.events.length = 0;

        void sm.replace(b.scene, { duration: 0.2 });
        expect(sm.transitioning).toBe(true);

        sm.update(0.1); // половина out — сцены ещё не менялись
        expect(sm.current).toBe(a.scene);
        expect(sm.transitioning).toBe(true);

        sm.update(0.1); // out завершён — swap: exit(a), enter(b)
        expect(a.events).toContain('a:exit');
        expect(b.events).toContain('b:enter');
        expect(sm.current).toBe(b.scene);

        sm.update(0.1);
        sm.update(0.1); // in завершён
        expect(sm.transitioning).toBe(false);
    });

    it('push с переходом: новая сцена входит в середине (за чёрным экраном)', () => {
        const sm = new SceneManager();
        const a = makeScene('a');
        const b = makeScene('b');
        void sm.push(a.scene);

        void sm.push(b.scene, { duration: 0.2 });
        sm.update(0.2); // out завершён
        expect(sm.current).toBe(b.scene);
        expect(b.events).toContain('b:enter');
    });

    it('параллельный переход глотается: новая сцена не входит', () => {
        const sm = new SceneManager();
        const a = makeScene('a');
        const b = makeScene('b');
        const c = makeScene('c');
        void sm.push(a.scene);

        void sm.replace(b.scene, { duration: 0.2 });
        void sm.replace(c.scene, { duration: 0.2 }); // глотается
        sm.update(0.2);
        sm.update(0.2);
        sm.update(0.2);

        expect(sm.current).toBe(b.scene);
        expect(c.events).toEqual([]);
    });

    it('update во время перехода тикает текущую (старую) сцену', () => {
        const sm = new SceneManager();
        const a = makeScene('a');
        void sm.push(a.scene);
        a.events.length = 0;

        void sm.replace(makeScene('b').scene, { duration: 0.2 });
        sm.update(0.05);
        expect(a.events).toContain('a:update');
    });

    it('render рисует весь стек (нижние сцены видны под верхними)', () => {
        const sm = new SceneManager();
        const a = makeScene('a');
        const b = makeScene('b');
        void sm.push(a.scene);
        void sm.push(b.scene);

        sm.render();
        expect(a.events).toContain('a:render');
        expect(b.events).toContain('b:render');
    });

    it('enter может быть асинхронным — переход его не ждёт', async () => {
        const sm = new SceneManager();
        let resolved = false;
        const scene: Scene = {
            enter: () =>
                new Promise<void>((res) => {
                    setTimeout(() => {
                        resolved = true;
                        res();
                    }, 5);
                }),
            exit: () => {},
            update: () => {},
            render: () => {}
        };
        const spy = vi.spyOn(scene, 'enter');
        await sm.push(scene);
        expect(spy).toHaveBeenCalled();
        // Не ждём промис — переход уже вернулся.
        await new Promise((r) => setTimeout(r, 10));
        expect(resolved).toBe(true);
    });
});