Files
Genarrative/apps/ai-game-creator-shell/tests/resourceBookModel.test.ts
T
suzmii 1cc9a23815 资源总览新增「所有资源」汇总卡与全部资源页
- 渲染层新增 `RESOURCE_BOOK_ALL_TARGET = 'all'` 特殊项:跨端契约 `ProjectResourceCanvasCategory` 不加 `'all'`,sidecar schema 不升版,落盘布局的 `section` 也不会出现它
- 资源总览第 0 张卡即「所有资源」:排在全部栏目卡之前,复用同一套缩略图类名/圆角/底色/阴影与栏目标题栏组件,不新增卡片样式
- 该卡的计数徽标取全量资源数,口径等于各栏目计数之和(同一份画布投影,不另算一套)
- 画本场景的栏目顺序与资源分组各多一个渲染层入口;`buildResourceBookScenePlan` 对特殊项只出计数与标题栏,不铺卡片,避免同一张资源卡在总览里挂载两份(`data-resource-card-id` 查询与 FLIP 的 `card:*` 键都会撞车)
- 新增「所有资源」页:栏目大纲 + 收起资源按钮 + 按栏目分组的滚动卡片网格;卡片复用栏目页同一颗渲染器与卡宽高,容器与网格样式补齐在 styles.css
- 该页的卡片显式关掉拖拽(卡片坐标不属于任何栏目画布,写回会污染布局 sidecar),保留选中/播放/菜单交互,并把光标与 `touch-action: pan-y` 交给滚动容器
- 栏目大纲抽成 `ResourceBookOutlineNav`,栏目页与全部资源页共用同一条入口带;渲染出的 DOM 与抽取前一致
- `renderResourceBookCard` 增加可选 `draggable` 参数,`ResourceCard` 的三个移动端指针回调改为可选
- 断言:总览存在「所有资源」卡且为第 0 张、计数等于全量资源数且等于各栏目计数之和、点它进入的页面按栏目分组且每组都挂真实资源卡、场景计划里特殊项不出卡片且整份计划无重复卡片键、全部资源页分组只保留非空栏目且保持总览顺序
- 既有「N 项」徽标断言从全文档文本查找收窄到 character 栏自己的标题栏(新增第 0 张卡后同文案会撞车,收窄而非放宽)
2026-09-11 11:21:03 +08:00

218 lines
6.5 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
initialResourceBookState,
isResourceBookAllTarget,
RESOURCE_BOOK_ALL_TARGET,
resourceBookCategoryCardPresentation,
resourceBookReducer,
resourceBookSceneCategory,
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('lets the all-resources page render every category without a second copy', () => {
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);
// 「所有资源」页自己按栏目渲染全部资源卡;画本场景不再复制一份,否则同一张
// 资源卡会挂载两次(DOM 查询与 FLIP 键都会撞车)。
for (const category of [
'ui-interaction',
'character',
'scene',
'audio',
'document',
'unclassified',
'version',
] as const) {
expect(resourceBookCategoryCardPresentation(all, category)).toBeNull();
}
expect(
resourceBookCategoryCardPresentation(all, RESOURCE_BOOK_ALL_TARGET),
).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);
});
});