import { describe, expect, it } from 'vitest'; import { initialResourceBookState, isResourceBookAllTarget, RESOURCE_BOOK_ALL_TARGET, resourceBookCategoryCardPresentation, resourceBookReducer, resourceBookSceneCategory, resourceBookShowsAllCategories, resourceBookShowsExitingCards, resourceBookShowsOverviewCards, } from '../src/view/project-development/resourceBookModel'; describe('resource book state machine', () => { it('keeps overview titles and preview cards visible on the steady main canvas', () => { expect(resourceBookShowsOverviewCards(initialResourceBookState)).toBe(true); }); it('commits the child target immediately and only keeps the motion phase transient', () => { const entering = resourceBookReducer(initialResourceBookState, { type: 'open-category', category: 'document', token: 1, }); expect(entering).toMatchObject({ view: 'child', category: 'document', phase: 'entering', token: 1, }); expect(resourceBookSceneCategory(entering)).toBe('document'); }); it('retargets directly from one child canvas to another', () => { const current = resourceBookReducer(initialResourceBookState, { type: 'open-category', category: 'document', token: 1, }); const switching = resourceBookReducer(current, { type: 'open-category', category: 'scene', token: 2, }); expect(switching).toMatchObject({ view: 'child', category: 'scene', phase: 'entering', }); expect(resourceBookSceneCategory(switching)).toBe('scene'); }); it('marks a return to the main canvas separately from a child enter transition', () => { const entering = resourceBookReducer(initialResourceBookState, { type: 'open-category', category: 'document', token: 1, }); const returning = resourceBookReducer(entering, { type: 'return-to-main', token: 2, }); expect(returning).toMatchObject({ view: 'main', category: null, phase: 'returning-main', }); }); it('lets an explicit collapse cancel an in-flight child switch', () => { const entering = resourceBookReducer(initialResourceBookState, { type: 'open-category', category: 'document', token: 1, }); const switching = resourceBookReducer(entering, { type: 'open-category', category: 'scene', token: 2, }); const main = resourceBookReducer(switching, { type: 'return-to-main', token: 3, }); expect(main).toMatchObject({ view: 'main', category: null, phase: 'returning-main', token: 3, }); }); it('ignores stale transition callbacks', () => { const entering = resourceBookReducer(initialResourceBookState, { type: 'open-category', category: 'document', token: 1, }); const switched = resourceBookReducer(entering, { type: 'open-category', category: 'scene', token: 2, }); expect( resourceBookReducer(switched, { type: 'finish-transition', token: 1 }), ).toEqual(switched); }); }); describe('resource book card presentation', () => { it('uses the overview stack on the steady main canvas', () => { expect( resourceBookCategoryCardPresentation(initialResourceBookState, 'scene'), ).toBe('overview'); }); it('keeps other categories mounted while entering so they can fade out', () => { const entering = resourceBookReducer(initialResourceBookState, { type: 'open-category', category: 'scene', token: 1, }); expect(resourceBookCategoryCardPresentation(entering, 'scene')).toBe( 'child', ); expect(resourceBookCategoryCardPresentation(entering, 'code')).toBe( 'overview', ); }); it('drops other categories once the child canvas is steady', () => { const steady = resourceBookReducer( resourceBookReducer(initialResourceBookState, { type: 'open-category', category: 'scene', token: 1, }), { type: 'finish-transition', token: 1 }, ); expect(resourceBookCategoryCardPresentation(steady, 'scene')).toBe('child'); expect(resourceBookCategoryCardPresentation(steady, 'code')).toBeNull(); }); it('returns every category to the stack and exposes the exiting window', () => { const returning = resourceBookReducer( resourceBookReducer(initialResourceBookState, { type: 'open-category', category: 'scene', token: 1, }), { type: 'return-to-main', token: 2 }, ); expect(resourceBookCategoryCardPresentation(returning, 'scene')).toBe( 'overview', ); expect(resourceBookCategoryCardPresentation(returning, 'code')).toBe( 'overview', ); expect(resourceBookShowsExitingCards(returning)).toBe(true); expect(resourceBookShowsExitingCards(initialResourceBookState)).toBe(false); }); }); describe('resource book all-resources target', () => { it('takes the overview stack as the first tile on the main canvas', () => { expect( resourceBookCategoryCardPresentation( initialResourceBookState, RESOURCE_BOOK_ALL_TARGET, ), ).toBe('overview'); }); it('spreads every real category on the canvas and keeps the all tile as a titlebar', () => { const all = resourceBookReducer( resourceBookReducer(initialResourceBookState, { type: 'open-category', category: RESOURCE_BOOK_ALL_TARGET, token: 1, }), { type: 'finish-transition', token: 1 }, ); expect(all).toMatchObject({ view: 'child', category: RESOURCE_BOOK_ALL_TARGET, phase: 'idle', }); expect(resourceBookSceneCategory(all)).toBe(RESOURCE_BOOK_ALL_TARGET); expect(resourceBookShowsAllCategories(all)).toBe(true); expect(resourceBookShowsAllCategories(initialResourceBookState)).toBe( false, ); // 展开态把每个真实栏目铺在自己的分带上(卡片仍由画本场景渲染,每张卡只有一个宿主: // 资源只属于一个栏目,所以 `card:*` 键不会撞车)。 for (const category of [ 'ui-interaction', 'character', 'scene', 'audio', 'document', 'unclassified', 'version', ] as const) { expect(resourceBookCategoryCardPresentation(all, category)).toBe('child'); } // 「所有资源」自己只出钉住的标题栏与全量计数,不再复制一份卡片:否则同一张资源卡会 // 在里面那一组和各栏目组各挂一次(DOM 查询与 FLIP 键都会撞车)。 expect( resourceBookCategoryCardPresentation(all, RESOURCE_BOOK_ALL_TARGET), ).toBeNull(); }); it('keeps a plain category page on the original single-column presentation', () => { const child = resourceBookReducer( resourceBookReducer(initialResourceBookState, { type: 'open-category', category: 'document', token: 1, }), { type: 'finish-transition', token: 1 }, ); expect(resourceBookShowsAllCategories(child)).toBe(false); expect(resourceBookCategoryCardPresentation(child, 'document')).toBe( 'child', ); // 静止态的非活动栏目一张都不挂。 expect(resourceBookCategoryCardPresentation(child, 'audio')).toBeNull(); }); it('identifies the render-layer sentinel without widening the section contract', () => { expect(isResourceBookAllTarget(RESOURCE_BOOK_ALL_TARGET)).toBe(true); expect(isResourceBookAllTarget('unclassified')).toBe(false); expect(isResourceBookAllTarget(null)).toBe(false); }); });