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);
});
});