Files
Genarrative/apps/ai-game-creator-shell/tests/resourceCanvasSectionMapping.test.ts
T
suzmii b12b0998d0 分类改动不再丢弃已落盘的画布坐标:分区不一致时归并到资源当前分类并保留 x / y
- 原缺陷(3082c3e85 的连带副作用):`kind:"UI"` 的资产投影分类由「待归类」修到「UI 交互」后,它们落盘的坐标 section 仍是现行栏目值 `unclassified`,于是 resolveResourceCanvasSection 走「现行栏目值 != 资源当前分类 ⇒ null」直接判丢弃,8 条用户手摆的坐标没能跟过来、被自动排布重算。
- resourceCanvasSectionMapping.ts:现行栏目值改为返回**资源当前分类**("这张卡属于哪一栏"的唯一真源本来就是 resource.category,协调时也按它分组),与旧栏目归并支路同一条语义 —— 只改写 section,x / y / manuallyPlaced 原样保留。判据:坐标是栏目内局部坐标(reconcileResourceCanvasLayout 先按 section 分组再算栏目带原点),所以换栏目保留 x / y 得到的正是"这张卡在新栏目里的同一个槽位";而"丢弃 + 自动重排"会连 manuallyPlaced 一起抹掉,是用户可见的损失。
- 判断与理由:**保坐标重写 section 可行**。同一函数对更难的旧栏目 case(旧 `art` 可能归并到 4 个不同栏目)已经在做同样的事,对 `unclassified → ui-interaction` 反而更窄;拒绝它是自相矛盾。
- 丢弃口径收窄:resolveResourceCanvasSection 只在**确实无从归并**时返回 null —— 无法识别的分区值,以及旧栏目归并目标集合不含资源当前分类(如旧 `code` 遇到已归为 `document` 的资源)。useProjectResourceCanvasLayout.ts 的 droppedSectionMismatch 文档与读盘统计注释按此更新(用户可见文案未变,它对新口径仍然成立)。
- 断言:resourceCanvasSectionMapping.test.ts 把原「沿用既有行为丢弃该坐标」用例改写为守新契约(归并 + changed=true),并新增 x / y / manuallyPlaced 逐项不变用例;resourceCanvasLayoutModel.test.ts 原「drops a persisted position…」改写为「rewrites … keeping the coordinates」;useProjectResourceCanvasLayout.test.ts 的读盘统计由 normalized 1 / droppedSectionMismatch 1 改为 normalized 2 / droppedSectionMismatch 0,并新增"归并必须连坐标一起保住"的断言,另补一条用例钉住 droppedSectionMismatch 只剩"无从归并"两类来源;appSurface/project-development.suite.ts 的提示文案与 data-* 计数同步,并新增写盘坐标断言。
- 变异验证(提交前已跑):把 resolveResourceCanvasSection 还原成 `section === resourceCategory ? section : null` → 6 条用例变红(映射 2 条、布局模型 1 条、读盘统计 2 条、appSurface 1 条),还原后复跑 83 passed + appSurface 该条 passed。
- 门禁:resourceCanvasSectionMapping 9 passed、resourceCanvasLayoutModel 47 passed、useProjectResourceCanvasLayout 27 passed。
2026-09-11 21:48:43 +08:00

209 lines
6.5 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
LEGACY_PROJECT_RESOURCE_CANVAS_SECTIONS,
PROJECT_RESOURCE_CANVAS_SECTIONS,
type ProjectResourceCanvasCategory,
} from '../../../packages/shared/src/contracts/gameCreationApp';
import {
LEGACY_RESOURCE_CANVAS_SECTION_FALLBACK,
LEGACY_RESOURCE_CANVAS_SECTION_TARGETS,
normalizeResourceCanvasPosition,
resolveResourceCanvasSection,
} from '../src/view/project-development/resourceCanvasSectionMapping';
function categoryByResourceId(category: ProjectResourceCanvasCategory) {
return new Map<string, ProjectResourceCanvasCategory>([
['resource-1', category],
]);
}
describe('资源画布分区轴与新旧栏目映射', () => {
it('现行分区为 6 类资产分类加末尾的项目版本栏目', () => {
expect(PROJECT_RESOURCE_CANVAS_SECTIONS).toEqual([
'ui-interaction',
'character',
'scene',
'audio',
'document',
'unclassified',
'version',
]);
});
it('每个旧栏目值都有归并目标,且回落栏目本身属于目标集合', () => {
for (const legacy of LEGACY_PROJECT_RESOURCE_CANVAS_SECTIONS) {
const targets = LEGACY_RESOURCE_CANVAS_SECTION_TARGETS[legacy];
expect(targets.length).toBeGreaterThan(0);
for (const target of targets) {
expect(PROJECT_RESOURCE_CANVAS_SECTIONS).toContain(target);
}
expect(targets).toContain(
LEGACY_RESOURCE_CANVAS_SECTION_FALLBACK[legacy],
);
}
});
it('旧栏目坐标按资源当前分区归并,x / y / manuallyPlaced 原样保留', () => {
const position = {
resourceId: 'resource-1',
section: 'art' as const,
x: -24,
y: 96,
manuallyPlaced: true,
};
for (const legacy of LEGACY_PROJECT_RESOURCE_CANVAS_SECTIONS) {
for (const target of LEGACY_RESOURCE_CANVAS_SECTION_TARGETS[legacy]) {
const normalized = normalizeResourceCanvasPosition(
{ ...position, section: legacy },
categoryByResourceId(target),
);
expect(normalized).toEqual({
position: {
resourceId: 'resource-1',
section: target,
x: -24,
y: 96,
manuallyPlaced: true,
},
changed: legacy !== target,
});
}
}
});
it('旧 art 是扩展名口径,按资源当前分区落入四类资产栏目', () => {
expect(LEGACY_RESOURCE_CANVAS_SECTION_TARGETS.art).toEqual([
'ui-interaction',
'character',
'scene',
'unclassified',
]);
for (const target of LEGACY_RESOURCE_CANVAS_SECTION_TARGETS.art) {
expect(resolveResourceCanvasSection('art', target)).toBe(target);
}
expect(LEGACY_RESOURCE_CANVAS_SECTION_FALLBACK.art).toBe('unclassified');
});
it('旧 code 统一归入待归类', () => {
expect(LEGACY_RESOURCE_CANVAS_SECTION_TARGETS.code).toEqual([
'unclassified',
]);
expect(resolveResourceCanvasSection('code', 'unclassified')).toBe(
'unclassified',
);
expect(resolveResourceCanvasSection('code', 'document')).toBeNull();
});
it('新旧同名的旧值不产生额外写入,独有旧值改写后必然判定为变化', () => {
for (const legacy of ['document', 'audio', 'version'] as const) {
const normalized = normalizeResourceCanvasPosition(
{
resourceId: 'resource-1',
section: legacy,
x: 12,
y: 34,
manuallyPlaced: false,
},
categoryByResourceId(legacy),
);
expect(normalized?.position.section).toBe(legacy);
expect(normalized?.changed).toBe(false);
}
for (const legacy of ['art', 'code'] as const) {
const target = LEGACY_RESOURCE_CANVAS_SECTION_FALLBACK[legacy];
const normalized = normalizeResourceCanvasPosition(
{
resourceId: 'resource-1',
section: legacy,
x: 12,
y: 34,
manuallyPlaced: false,
},
categoryByResourceId(target),
);
expect(normalized?.position.section).toBe(target);
expect(normalized?.changed).toBe(true);
}
});
it('现役分区与资源分区不一致时归并到资源当前分类并保留坐标', () => {
// 旧实现把"现行栏目值但不等于资源当前分类"判 null、整条坐标丢弃 —— 那等于把用户手摆的
// 位置换成自动排布。资源分类本来就会变(`kind:"UI"` 的 8 条资产从「待归类」被修到
// 「UI 交互」就是真机一例),所以这里必须归并,语义与旧栏目归并支路完全一致。
expect(resolveResourceCanvasSection('scene', 'character')).toBe(
'character',
);
expect(resolveResourceCanvasSection('version', 'document')).toBe(
'document',
);
expect(
normalizeResourceCanvasPosition(
{
resourceId: 'resource-1',
section: 'scene',
x: 0,
y: 0,
manuallyPlaced: true,
},
categoryByResourceId('character'),
),
).toEqual({
position: {
resourceId: 'resource-1',
section: 'character',
x: 0,
y: 0,
manuallyPlaced: true,
},
changed: true,
});
});
it('归并到资源当前分类时 x / y / manuallyPlaced 逐项不被改写', () => {
// 真机现场:`kind:"UI"` 资产落盘在「待归类」栏,分类被修到「UI 交互」之后
// 它们的坐标必须跟过来,而不是被丢弃后由自动排布重算。
expect(
normalizeResourceCanvasPosition(
{
resourceId: 'resource-1',
section: 'unclassified',
x: -24,
y: 96,
manuallyPlaced: true,
},
categoryByResourceId('ui-interaction'),
),
).toEqual({
position: {
resourceId: 'resource-1',
section: 'ui-interaction',
x: -24,
y: 96,
manuallyPlaced: true,
},
changed: true,
});
});
it('不猜测无法识别的分区取值和未知资源', () => {
expect(resolveResourceCanvasSection('not-a-section', 'scene')).toBeNull();
expect(resolveResourceCanvasSection(undefined, 'scene')).toBeNull();
expect(resolveResourceCanvasSection(null, 'scene')).toBeNull();
expect(
normalizeResourceCanvasPosition(
{
resourceId: 'missing-resource',
section: 'scene',
x: 0,
y: 0,
manuallyPlaced: true,
},
new Map<string, ProjectResourceCanvasCategory>(),
),
).toBeNull();
});
});