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