Files
Genarrative/apps/ai-game-creator-shell/tests/resourceCardReferenceDropModel.test.ts
T
suzmii 5467ed6d7b 自查修正:拖到对话落点逐值比较、替换面板锚进画布容器并与任务面板互斥
- 拖动落点改逐值比较 sameResourceCardReferenceDrop(条数 + 对话栏矩形四值),指针在对话栏上移动不再每帧 setState、不再每帧重渲染工作台。
- 替换面板从 stage 顶层移进画布容器 .game-resource-book-manager(本身 position: relative),top 3.2rem 排在任务开关下方;相对 stage 写死的偏移会在工具条换行时压上去,与刚修的任务开关是同一类隐患。
- 打开替换会话时收起「生成任务」面板:画布右上角同一时刻只留一块浮层(任务照旧在账本里推进)。
- 面板上方补一行「替换源素材:<显示名>」:非模态之后画布上的源素材光环会被空白点击清掉,源身份得在面板这一侧留一份可读说法。
- 测试:替换用例新增「面板锚在画布容器里、写明源素材、并与任务面板互斥」(20 passed)、落点模型新增逐值比较用例(6 passed)。
- 文档:decision-log 新增本批自查条目、待办文档补登记、验收用例 C9 注脚改为 nonModal 形态。
2026-09-21 06:21:19 +08:00

170 lines
5.6 KiB
TypeScript

import { describe, expect, test } from 'vitest';
import {
resourceCardReferenceDropContains,
resourceCardReferenceDropHintLabel,
resourceCardReferenceDropRect,
resourceCardReferenceDropReferences,
sameResourceCardReferenceDrop,
} from '../src/view/project-development/resourceCardReferenceDropModel';
import type { ProjectResource } from '../src/view/project-development/resourceProjectionModel';
function resource(
patch: Partial<ProjectResource> & { id: string },
): ProjectResource {
return {
category: 'character',
subtype: 'character',
label: patch.id,
path: `assets/${patch.id}.png`,
mediaType: 'image/png',
sourceLabel: '本地上传',
taskTitle: null,
manifestAssetId: null,
producerTaskId: null,
externalResourceId: null,
referenceResourceIds: [],
dependencies: [],
dependencyDepth: 0,
...patch,
};
}
function elementWithRect(rect: {
left: number;
top: number;
width: number;
height: number;
}): Element {
return {
getBoundingClientRect: () =>
({
...rect,
right: rect.left + rect.width,
bottom: rect.top + rect.height,
x: rect.left,
y: rect.top,
toJSON: () => ({}),
}) as DOMRect,
} as unknown as Element;
}
describe('拖动素材到对话:落点与引用构造', () => {
test('没有元素或没量出尺寸都不算落点', () => {
expect(resourceCardReferenceDropRect(null)).toBeNull();
// 0 x 0 必须当「没有落点」:否则任何点都会被判成落在里面,画布内拖动松手也会被当成拖到对话。
expect(
resourceCardReferenceDropRect(
elementWithRect({ left: 0, top: 0, width: 0, height: 0 }),
),
).toBeNull();
expect(
resourceCardReferenceDropRect(
elementWithRect({ left: 10, top: 20, width: 400, height: 600 }),
),
).toEqual({ left: 10, top: 20, width: 400, height: 600 });
});
test('命中判据按矩形边界闭区间算', () => {
const rect = { left: 800, top: 0, width: 400, height: 600 };
expect(resourceCardReferenceDropContains(rect, 800, 0)).toBe(true);
expect(resourceCardReferenceDropContains(rect, 1200, 600)).toBe(true);
expect(resourceCardReferenceDropContains(rect, 1000, 300)).toBe(true);
expect(resourceCardReferenceDropContains(rect, 799, 300)).toBe(false);
expect(resourceCardReferenceDropContains(rect, 1000, 601)).toBe(false);
});
test('只把已登记的 manifest 素材折成引用,顺序与拖动集合一致', () => {
const references = resourceCardReferenceDropReferences(
['asset:registered-b', 'featured:not-registered', 'asset:registered-a'],
[
resource({
id: 'asset:registered-b',
manifestAssetId: 'registered-b',
subtype: 'character',
label: '主角立绘',
mediaType: 'image/png',
assetCategory: 'character',
assetTags: ['主角', '立绘'],
}),
resource({ id: 'featured:not-registered', label: '任务产物' }),
resource({
id: 'asset:registered-a',
manifestAssetId: 'registered-a',
subtype: 'background-music',
label: '主城 BGM',
mediaType: 'audio/mpeg',
assetCategory: 'audio',
}),
],
);
expect(references.map((reference) => reference.resourceId)).toEqual([
'registered-b',
'registered-a',
]);
expect(references[0]).toMatchObject({
type: 'resource',
// 引用认的是 manifest 资产 id,不是画布投影 id。
resourceId: 'registered-b',
kind: 'character',
label: '主角立绘',
mediaType: 'image/png',
category: 'character',
tags: ['主角', '立绘'],
// 与资源卡工具条那枚「引用」按钮同一个来源标记:同一素材两处进来是同一枚引用。
source: 'resource-card',
});
expect(references[1]).toMatchObject({
resourceId: 'registered-a',
kind: 'background-music',
category: 'audio',
});
});
test('拖动集合里一条登记素材都没有时返回空数组', () => {
expect(
resourceCardReferenceDropReferences(
['featured:task'],
[resource({ id: 'featured:task' })],
),
).toEqual([]);
expect(resourceCardReferenceDropReferences([], [])).toEqual([]);
});
test('提示文案把条数说清楚,0 条时说原因而不是「引用 0 项」', () => {
expect(resourceCardReferenceDropHintLabel(1)).toBe(
'松手即可 @ 引用 1 项素材',
);
expect(resourceCardReferenceDropHintLabel(3)).toBe(
'松手即可 @ 引用 3 项素材',
);
expect(resourceCardReferenceDropHintLabel(0)).toBe(
'这些素材还没登记为项目资源,不能 @ 引用',
);
});
test('落点状态逐值比较:条数或矩形变了才算变化', () => {
const rect = { left: 900, top: 40, width: 300, height: 600 };
const base = { count: 2, rect };
// 指针在对话栏上移动时每帧都会算出新的对象:逐值相同必须判成「没变」,
// 否则每一帧都会 setState 一次、把工作台整棵子树重渲染一遍。
expect(
sameResourceCardReferenceDrop(base, { count: 2, rect: { ...rect } }),
).toBe(true);
expect(sameResourceCardReferenceDrop(base, { count: 1, rect })).toBe(false);
expect(
sameResourceCardReferenceDrop(base, {
count: 2,
rect: { ...rect, left: rect.left + 1 },
}),
).toBe(false);
expect(
sameResourceCardReferenceDrop(base, {
count: 2,
rect: { ...rect, height: rect.height + 1 },
}),
).toBe(false);
});
});