// @vitest-environment jsdom import { cleanup, fireEvent, render, screen, within, } from '@testing-library/react'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { isResourceCanvasExportable, resolveDownloadablePanelEntries, resolveResourceCanvasPanelEntries, resolveSelectedPanelEntries, } from '../src/features/resource-canvas/resourceCanvasAssetTransferModel'; import { ResourceCanvasPanelView } from '../src/features/resource-canvas/ResourceCanvasPanelView'; import type { ProjectResource, ProjectVersionResourceSummary, } from '../src/view/project-development/resourceProjectionModel'; afterEach(cleanup); function createResource( overrides: Partial = {}, ): ProjectResource { return { id: 'asset:hero', category: 'character', subtype: 'character', label: '主角立绘', path: 'assets/hero.png', mediaType: 'image/png', sourceLabel: '生成', taskTitle: null, manifestAssetId: 'hero', producerTaskId: null, externalResourceId: null, referenceResourceIds: [], dependencies: [], dependencyDepth: 0, ...overrides, }; } /** 虚拟版本条目:只有版本摘要与合成展示路径,没有对应文件。 */ function createVersionResource(): ProjectResource { const version: ProjectVersionResourceSummary = { versionId: 'version-1', parentVersionId: null, projectRevision: 3, resourceBindings: [], createdReason: 'initial', createdAt: 1, label: '版本 1', childVersionIds: [], }; return createResource({ id: 'version:version-1', category: 'version', subtype: 'version', label: '项目版本 · 版本 1', path: '', mediaType: 'application/vnd.genarrative.project-version+json', manifestAssetId: null, version, }); } function panelEntries() { return resolveResourceCanvasPanelEntries([ { resource: createResource(), categoryLabel: '角色与对象', typeLabel: '图片', previewIdentity: null, previewStatus: 'idle', previewSourceUrl: null, previewError: null, }, { resource: createVersionResource(), categoryLabel: '项目版本', typeLabel: '版本', previewIdentity: null, previewStatus: 'idle', previewSourceUrl: null, previewError: null, }, ]); } describe('资源面板下载口径', () => { it('虚拟版本条目不可下载:合成路径不许走到落盘链路', () => { const entries = panelEntries(); const hero = entries[0]!; const version = entries[1]!; expect(isResourceCanvasExportable(hero)).toBe(true); expect(hero.downloadable).toBe(true); expect(version.downloadable).toBe(false); // 下载目标只保留真能落盘的条目:版本卡的 path 是合成展示串, // 传给 `save_local_project_asset_file` 只会失败。 expect( resolveDownloadablePanelEntries( resolveSelectedPanelEntries(entries, [ hero.resourceId, version.resourceId, ]), ).map((entry) => entry.resourceId), ).toEqual([hero.resourceId]); }); it('只选中不可下载条目时下载按钮保持禁用,不会变成点了没反应', () => { const onDownloadSelection = vi.fn(); const entries = panelEntries(); const version = entries[1]!; render( , ); const download = within( screen.getByRole('dialog', { name: '资源面板' }), ).getByRole('button', { name: '下载' }) as HTMLButtonElement; expect(download.disabled).toBe(true); fireEvent.click(download); expect(onDownloadSelection).not.toHaveBeenCalled(); }); it('可下载条目选中时按钮可用,并且上传中把三条关闭路径一起挡住', () => { const onDownloadSelection = vi.fn(); const onClose = vi.fn(); const entries = panelEntries(); const hero = entries[0]!; const { rerender } = render( , ); const panel = () => screen.getByRole('dialog', { name: '资源面板' }); fireEvent.click(within(panel()).getByRole('button', { name: '下载' })); expect(onDownloadSelection).toHaveBeenCalledTimes(1); rerender( , ); // 上传在飞:× 禁用、Escape 与遮罩都不许把面板关掉(父级上传还在跑)。 expect( ( within(panel()).getByRole('button', { name: '关闭资源面板', }) as HTMLButtonElement ).disabled, ).toBe(true); fireEvent.keyDown(window, { key: 'Escape' }); expect(onClose).not.toHaveBeenCalled(); fireEvent.mouseDown(panel().parentElement as HTMLElement); expect(onClose).not.toHaveBeenCalled(); }); });