Files
Genarrative/apps/ai-game-creator-shell/tests/resourceCardReferenceDropModel.test.ts
T
suzmii 9639a080c6 资源卡拖到对话栏实现批量 @ 引用
- 新增落点模型 resourceCardReferenceDropModel:对话栏矩形量取(0 x 0 判成没有落点,避免画布内拖动被误判)、命中判据、引用构造与落点提示文案。
- 引用构造与工具条「引用」按钮逐字一致(只认已登记 manifest 素材、source 为 resource-card、kind/分类/标签取自资源投影),一条都构造不出来时用提示条说明原因。
- 拖动中指针落到对话栏上即从排版切到引用:卡片不再跟指针走,改铺虚线落点浮层并提示「松手即可 @ 引用 N 项素材」;拖回画布内松手仍是原来的排版语义。
- 批量范围与拖动位移同一个集合(多选整批、单选只引用按住的那张);新增批量事件 RESOURCE_REFERENCE_INSERT_MANY_EVENT,App 侧一次插入整批并只聚焦一次,空批次不派发。
- 对话栏挂 ref(不 document.querySelector)、落点浮层按对话栏当时的屏幕矩形铺在根节点上,不改对话栏自身的定位与排版。
- 测试:新增落点模型用例 5 条与画布集成用例 4 条(单卡/多选/拖回画布/pointercancel),resourceReferenceInput 新增批量事件与一次插入整批 chip 两条,共 39 条定向全绿。
- 文档:功能说明新增「拖拽引用」一节与完成清单条目、PRD 更新时间、decision-log 新增 2026-09-21 条目。
2026-09-21 03:37:43 +08:00

146 lines
4.7 KiB
TypeScript

import { describe, expect, test } from 'vitest';
import {
resourceCardReferenceDropContains,
resourceCardReferenceDropHintLabel,
resourceCardReferenceDropRect,
resourceCardReferenceDropReferences,
} 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(
'这些素材还没登记为项目资源,不能 @ 引用',
);
});
});