import { describe, expect, it } from 'vitest'; import { createGameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp'; import { resolveResourceCanvasToolbarActions } from '../src/features/resource-canvas/resourceCanvasToolbarModel'; import type { ProjectResource, ProjectVersionResourceSummary, } from '../src/view/project-development/resourceProjectionModel'; function createResource( overrides: Partial = {}, ): ProjectResource { return { id: 'asset:hero', category: 'ui-interaction', subtype: 'icon', label: '主角立绘', path: 'assets/hero.png', mediaType: 'image/png', sourceLabel: '生成', taskTitle: null, manifestAssetId: 'hero', producerTaskId: null, externalResourceId: null, referenceResourceIds: [], dependencies: [], dependencyDepth: 0, ...overrides, }; } /** 虚拟版本条目:只有版本摘要,没有对应文件。 */ function createVersionSummary(): ProjectVersionResourceSummary { return { versionId: 'version-1', parentVersionId: null, projectRevision: 3, resourceBindings: [], createdReason: 'initial', createdAt: 1, label: '版本 1', childVersionIds: [], }; } function createManifest() { return createGameCreationAppManifest( 'project-toolbar-actions', '工具条动作测试', ); } /** 把种子任务的第一个任务改成已完成并登记指定产物,用于覆盖任务产物正规化门禁。 */ function createManifestWithProducingTask( status: 'completed' | 'running', artifactPath: string, ) { const manifest = createManifest(); manifest.tasks = manifest.tasks.map((task, index) => index === 0 ? { ...task, status, artifacts: [artifactPath] } : task, ); return manifest; } function toolbarActions( manifest: ReturnType, resource: ProjectResource, ) { return [...resolveResourceCanvasToolbarActions(manifest, resource)]; } /** * AGC 资源画布工具条的放行判据只有一条:**进集合的动作必须真能跑通**。 * * `ImageCanvasSelectedLayerToolbarView` 收到 `supportedActions` 后完全以集合为准, * 宿主编排层里还是空回调的动作一旦进集合就会渲染成"点了没反应"的按钮,因此这里 * 逐条钉住当前真正接通的动作集合(见 * `docs/project-memory/shared-memory/pitfalls.md` 的「资源画布工具条的『改造』 * 『角色动画』按钮点了没反应」)。 * * 当前真实接通的是「快速编辑」(normalize → derive(editKind='image-reference'))、 * 「生成动画」(normalize → derive(editKind='character-animation') → 服务端 * `/api/editor/character-animations/generations`)与「下载」(资源面板同一条原生保存 * 对话框 + `save_local_project_asset_file` 链路)。 */ describe('resource canvas toolbar actions', () => { it('已登记 manifest 资产的栅格图片放行快速编辑与下载', () => { expect(toolbarActions(createManifest(), createResource())).toEqual([ 'quick-edit', 'download', ]); }); it('音频与视频资源不再放行空回调的改造动作,但仍放行下载', () => { const manifest = createManifest(); expect( toolbarActions( manifest, createResource({ id: 'asset:bgm', category: 'audio', subtype: 'background-music', label: '背景音乐', path: 'assets/bgm.mp3', mediaType: 'audio/mpeg', manifestAssetId: 'bgm', }), ), ).toEqual(['download']); expect( toolbarActions( manifest, createResource({ id: 'asset:intro', subtype: 'video', label: '开场动画', path: 'assets/intro.mp4', mediaType: 'video/mp4', manifestAssetId: 'intro', }), ), ).toEqual(['download']); }); it('角色资源放行「生成动画」:源是已登记的栅格图片', () => { // 这条断言由「不再放行空回调的角色动画动作」改写而来(四不写:旧断言描述的是 // 未接通状态,已被真实链路取代,不留墓碑)。宿主编排层现在把 // `onOpenCharacterAnimationPanel` 接到 `submitResourceCharacterAnimation` // (derive editKind='character-animation'),所以进集合不再是假按钮。 expect( toolbarActions( createManifest(), createResource({ id: 'asset:hero-action', subtype: 'character' }), ), ).toEqual(['quick-edit', 'character-animation', 'download']); }); it('非角色的图片资源不放行生成动画,避免渲染出与源类型不匹配的按钮', () => { expect( toolbarActions( createManifest(), createResource({ id: 'asset:scene', subtype: 'scene' }), ), ).toEqual(['quick-edit', 'download']); expect( toolbarActions( createManifest(), createResource({ id: 'asset:icon', subtype: 'icon' }), ), ).toEqual(['quick-edit', 'download']); }); it('角色资源没登记成正式素材时不放行生成动画,只留下真能跑通的下载', () => { expect( toolbarActions( createManifest(), createResource({ id: 'asset:hero-artifact', subtype: 'character', manifestAssetId: null, producerTaskId: 'task-hero', }), ), ).toEqual(['download']); }); it('非栅格的角色资源不放行生成动画:Rust 要解码源图取宽高,SVG 会直接失败', () => { expect( toolbarActions( createManifest(), createResource({ id: 'asset:hero-svg', subtype: 'character', path: 'assets/hero.svg', mediaType: 'image/svg+xml', }), ), ).toEqual(['download']); }); it('未登记 manifest 资产且没有已完成任务产物背书的资源只放行下载', () => { expect( toolbarActions( createManifest(), createResource({ manifestAssetId: null, producerTaskId: 'task-art' }), ), ).toEqual(['download']); }); it('只认已完成任务的栅格产物,未完成任务的产物不放行快速编辑但可下载', () => { const artifactPath = 'assets/hero.png'; const completedManifest = createManifestWithProducingTask( 'completed', artifactPath, ); expect( toolbarActions( completedManifest, createResource({ manifestAssetId: null, producerTaskId: completedManifest.tasks[0].id, }), ), ).toEqual(['quick-edit', 'download']); const runningManifest = createManifestWithProducingTask( 'running', artifactPath, ); expect( toolbarActions( runningManifest, createResource({ manifestAssetId: null, producerTaskId: runningManifest.tasks[0].id, }), ), ).toEqual(['download']); }); it('非栅格图片不进快速编辑的派生门禁,但仍可下载', () => { expect( toolbarActions( createManifest(), createResource({ id: 'asset:icon', path: 'assets/icon.svg', mediaType: 'image/svg+xml', }), ), ).toEqual(['download']); }); it('拿得到本地文件路径的资源才放行下载', () => { expect(toolbarActions(createManifest(), createResource())).toContain( 'download', ); }); it.each([ ['image/png', 'character', 'assets/hero.png'], ['image/svg+xml', 'icon', 'assets/icon.svg'], ['image/png', 'character-animation', 'assets/walk.png'], ['video/mp4', 'video', 'assets/intro.mp4'], ['audio/wav', 'sound-effect', 'assets/hit.wav'], ['audio/mpeg', 'background-music', 'assets/music.mp3'], ['text/html', 'ui-prototype', 'ui/menu.html'], ['text/markdown', 'document', 'docs/design.md'], ['application/json', 'other', 'assets/data.json'], ['application/octet-stream', 'other', 'assets/model.bin'], ])( '%s 素材导出不依赖媒体类型、预览或 manifest 身份', (mediaType, subtype, path) => { expect( toolbarActions( createManifest(), createResource({ mediaType, subtype, path, manifestAssetId: null, }), ), ).toContain('download'); }, ); it('拿不到本地文件路径的资源不放行下载,避免渲染出点了报错的按钮', () => { const manifest = createManifest(); expect( toolbarActions( manifest, createResource({ path: '', manifestAssetId: 'virtual' }), ), ).not.toContain('download'); expect( toolbarActions( manifest, createResource({ id: 'version:1', category: 'version', subtype: 'version', label: '版本 1', path: '', mediaType: 'application/vnd.genarrative.project-version+json', manifestAssetId: null, version: createVersionSummary(), }), ), ).not.toContain('download'); }); });