import { describe, expect, it, vi } from 'vitest'; import type { GameCreationAppAssetManifestEntry, GameCreationAppManifest, } from '../../../packages/shared/src/contracts/gameCreationApp'; import { ensureUiDesignResourceForPrototype, findLinkedUiDesignResource, } from '../src/features/ui-editor/uiDesignResourceBridge'; function manifestWithPrototype(): GameCreationAppManifest { const prototype: GameCreationAppAssetManifestEntry = { id: 'prototype-asset', kind: 'ui-prototype', mediaType: 'image/png', localPath: 'assets/ui-prototype.png', source: { kind: 'canvas', referenceResourceIds: [] }, imageSequenceFrames: null, imageSequenceDurationMs: null, }; return { schemaVersion: 'game-creator-app.v1', projectId: 'project-1', projectName: 'UI bridge', tasks: [], assets: [prototype], versions: [], godotProjectRoot: null, preview: null, resourceCanvasLayouts: [], } as unknown as GameCreationAppManifest; } describe('ui design resource bridge', () => { it('finds only a JSON UI resource linked to the exact prototype asset', () => { const manifest = manifestWithPrototype(); manifest.assets.push({ id: 'unrelated-ui', kind: 'UI', mediaType: 'application/json', localPath: 'ui/unrelated.json', source: { kind: 'generated', referenceResourceIds: ['other'] }, imageSequenceFrames: null, imageSequenceDurationMs: null, }); expect(findLinkedUiDesignResource(manifest, 'prototype-asset')).toBeNull(); manifest.assets.push({ id: 'linked-ui', kind: 'UI', mediaType: 'application/json', localPath: 'ui/linked.json', source: { kind: 'generated', referenceResourceIds: ['prototype-asset'] }, imageSequenceFrames: null, imageSequenceDurationMs: null, }); expect(findLinkedUiDesignResource(manifest, 'prototype-asset')?.id).toBe( 'linked-ui', ); }); it('reuses workflow resources linked by the prototype canonical resource id', () => { const manifest = manifestWithPrototype(); manifest.assets[0]!.source.resourceId = 'canvas-resource-1'; manifest.assets.push({ id: 'workflow-ui', kind: 'UI', mediaType: 'application/json', localPath: 'ui/workflow.json', source: { kind: 'generated', resourceId: 'ui-workflow-page-1', referenceResourceIds: ['canvas-resource-1', 'design-resource-1'], }, imageSequenceFrames: null, imageSequenceDurationMs: null, }); expect(findLinkedUiDesignResource(manifest, 'prototype-asset')?.id).toBe( 'workflow-ui', ); }); it('prefers a completed workflow resource when an older bridge resource also matches', () => { const manifest = manifestWithPrototype(); manifest.assets.push( { id: 'bridge-ui', kind: 'UI', mediaType: 'application/json', localPath: 'ui/UI 设计 1.json', source: { kind: 'generated', referenceResourceIds: ['prototype-asset'], }, imageSequenceFrames: null, imageSequenceDurationMs: null, }, { id: 'completed-ui', kind: 'UI', mediaType: 'application/json', localPath: 'ui/ui-workflow-page.json', source: { kind: 'generated', generationKind: 'ui-workflow.completed', referenceResourceIds: ['prototype-asset'], }, imageSequenceFrames: null, imageSequenceDurationMs: null, }, ); expect(findLinkedUiDesignResource(manifest, 'prototype-asset')?.id).toBe( 'completed-ui', ); }); it('reuses an existing link without invoking a mutating command', async () => { const manifest = manifestWithPrototype(); const linked = { id: 'linked-ui', kind: 'UI', mediaType: 'application/json', localPath: 'ui/linked.json', source: { kind: 'generated' as const, referenceResourceIds: ['prototype-asset'], }, imageSequenceFrames: null, imageSequenceDurationMs: null, }; manifest.assets.push(linked); const invoke = vi.fn(); const result = await ensureUiDesignResourceForPrototype({ projectPath: '/project', manifest, prototypeAssetId: 'prototype-asset', invoke, }); expect(result.asset).toEqual(linked); expect(result.created).toBe(false); expect(invoke).not.toHaveBeenCalled(); }); it('invokes the atomic Tauri bridge for a missing link', async () => { const manifest = manifestWithPrototype(); const result = { asset: { id: 'new-ui', kind: 'UI', mediaType: 'application/json', localPath: 'ui/UI 设计 1.json', source: { kind: 'generated' as const, referenceResourceIds: ['prototype-asset'], }, imageSequenceFrames: null, imageSequenceDurationMs: null, }, manifest: { ...manifest, assets: [...manifest.assets] }, committedProjectRevision: 7, created: true, }; const invoke = vi.fn().mockResolvedValue(result); await expect( ensureUiDesignResourceForPrototype({ projectPath: '/project', manifest, prototypeAssetId: ' prototype-asset ', invoke, }), ).resolves.toEqual(result); expect(invoke).toHaveBeenCalledWith( 'ensure_ui_design_resource_for_prototype', { input: { projectPath: '/project', expectedProjectId: 'project-1', prototypeAssetId: 'prototype-asset', }, }, ); }); it('rejects non-prototype assets before invoking Tauri', async () => { const manifest = manifestWithPrototype(); const invoke = vi.fn(); await expect( ensureUiDesignResourceForPrototype({ projectPath: '/project', manifest, prototypeAssetId: 'missing', invoke, }), ).rejects.toThrow('目标资源不是 UI 原型图片'); expect(invoke).not.toHaveBeenCalled(); }); });