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 & { 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( '这些素材还没登记为项目资源,不能 @ 引用', ); }); });