import { StorySceneCtrl, BASE_TYPING_CPS } from '@ui/StorySceneCtrl'; import { StorageMgr } from '@common/StorageMgr'; import { IStorySceneConfig } from '@data/Interfaces'; import { STORAGE_KEY } from '@common/Constants'; const SCENE: IStorySceneConfig = { id: 'chapter_1_intro', bgm: 'bgm_story', maxDurationSec: 30, pages: [ { index: 1, illustration: 'p1', text: 'A' }, { index: 2, illustration: 'p2', text: 'BCDE' }, { index: 3, illustration: 'p3', text: 'FGH' }, ], }; function mem() { const m = new Map(); return { getItem: (k: string) => (m.has(k) ? (m.get(k) as string) : null), setItem: (k: string, v: string) => { m.set(k, v); }, removeItem: (k: string) => { m.delete(k); }, }; } describe('StorySceneCtrl — first-time gate (req 19.5)', () => { it('start() reports "already_seen" when storage flag is set', () => { const storage = new StorageMgr(mem()); storage.set(STORAGE_KEY.StoryIntroSeen, true); const ctrl = new StorySceneCtrl(SCENE, storage); expect(ctrl.start()).toBe('already_seen'); }); it('start() reports "playing" on first run', () => { const ctrl = new StorySceneCtrl(SCENE, new StorageMgr(mem())); expect(ctrl.start()).toBe('playing'); expect(ctrl.status).toBe('typing'); }); }); describe('StorySceneCtrl — typewriter (req 19.2-19.3)', () => { it('reveals characters at BASE_TYPING_CPS', () => { const ctrl = new StorySceneCtrl(SCENE, new StorageMgr(mem())); ctrl.start(); // Advance enough time to type "A". ctrl.tick(1 / BASE_TYPING_CPS + 0.001); expect(ctrl.visibleText).toBe('A'); // Page "A" complete → waiting_next. expect(ctrl.status).toBe('waiting_next'); }); it('tap during typing fully reveals the current page', () => { const ctrl = new StorySceneCtrl(SCENE, new StorageMgr(mem())); ctrl.start(); ctrl.onTap(); // accelerate page 1 ("A") expect(ctrl.visibleText).toBe('A'); expect(ctrl.status).toBe('waiting_next'); // Next tap → advance to page 2. ctrl.onTap(); expect(ctrl.currentPageNumber).toBe(2); expect(ctrl.status).toBe('typing'); }); }); describe('StorySceneCtrl — skip (req 19.4)', () => { it('onSkip immediately finishes and marks seen', () => { const storage = new StorageMgr(mem()); const onFinished = jest.fn(); const ctrl = new StorySceneCtrl(SCENE, storage, { onFinished }); ctrl.start(); ctrl.onSkip(); expect(ctrl.status).toBe('finished'); expect(onFinished).toHaveBeenCalledWith(true); expect(storage.get(STORAGE_KEY.StoryIntroSeen, false)).toBe(true); }); it('re-skipping after finish is a no-op', () => { const ctrl = new StorySceneCtrl(SCENE, new StorageMgr(mem())); ctrl.start(); ctrl.onSkip(); expect(() => ctrl.onSkip()).not.toThrow(); }); }); describe('StorySceneCtrl — reset() (req 19.6)', () => { it('clears the "seen" flag', () => { const storage = new StorageMgr(mem()); const ctrl = new StorySceneCtrl(SCENE, storage); ctrl.start(); ctrl.onSkip(); expect(ctrl.hasBeenSeen()).toBe(true); ctrl.reset(); expect(ctrl.hasBeenSeen()).toBe(false); }); }); describe('StorySceneCtrl — natural finish (all pages)', () => { it('calls onFinished(false) after advancing past the last page', () => { const onFinished = jest.fn(); const ctrl = new StorySceneCtrl(SCENE, new StorageMgr(mem()), { onFinished }); ctrl.start(); // Full page 1. ctrl.onTap(); ctrl.onTap(); // → page 2 ctrl.onTap(); // reveal page 2 ctrl.onTap(); // → page 3 ctrl.onTap(); // reveal page 3 ctrl.onTap(); // → finish expect(onFinished).toHaveBeenCalledWith(false); expect(ctrl.status).toBe('finished'); }); });