import { describe, expect, it } from 'vitest'; import type { CanvasGenerationInputs, CanvasLayer, } from './ImageCanvasEditorTypes'; import { createAudioResultLayer, createCharacterAnimationResultLayer, createGeneratedResultLayer, createIconSpritesheetResultLayers, createQuickEditResultLayer, } from './ImageCanvasGenerationLayerModel'; function createGenerated(overrides = {}) { return { imageSrc: 'data:image/png;base64,generated', width: 1024, height: 1024, sourceType: 'generated' as const, prompt: '生成提示词', actualPrompt: '实际提示词', model: 'gpt-image-2', provider: 'VectorEngine', taskId: 'task-generated', objectKey: 'generated/object.png', assetObjectId: 'asset-object-generated', ...overrides, }; } function createSourceLayer(overrides: Partial = {}): CanvasLayer { return { id: 'layer-source', resourceId: 'resource-source', title: '源图', src: 'data:image/png;base64,source', x: 120, y: 140, width: 320, height: 240, originalWidth: 1536, originalHeight: 1024, zIndex: 2, sourceType: 'generated', groupId: 'group-a', assetKind: 'spec', ...overrides, }; } function createGenerationInputs(): CanvasGenerationInputs { return { fields: [{ title: '生成提示词', value: '生成提示词' }], references: [], }; } describe('ImageCanvasGenerationLayerModel', () => { it('creates generated result layers centered on the active placeholder', () => { const layer = createGeneratedResultLayer({ generated: createGenerated({ width: 2048, height: 1152 }), generatedIndex: 7, canvasSize: { width: 900, height: 640 }, viewport: { x: 10, y: 20, scale: 2 }, frame: { x: 80, y: 60, width: 560, height: 315, originalWidth: 2048, originalHeight: 1152, }, assetKind: 'spec', title: '图标规范 7', generationInputs: createGenerationInputs(), }); expect(layer).toMatchObject({ id: 'layer-generated-7', resourceId: 'local-resource-generated-7', title: '图标规范 7', x: -664, y: -358.5, width: 2048, height: 1152, originalWidth: 2048, originalHeight: 1152, zIndex: 17, sourceType: 'generated', assetKind: 'spec', prompt: '生成提示词', actualPrompt: '实际提示词', objectKey: 'generated/object.png', assetObjectId: 'asset-object-generated', }); expect(layer.generationInputs?.fields[0]?.value).toBe('生成提示词'); }); it('creates edit result layers beside the source image', () => { const sourceLayer = createSourceLayer(); const layer = createGeneratedResultLayer({ generated: createGenerated({ prompt: '修改要求' }), generatedIndex: 8, canvasSize: { width: 900, height: 640 }, viewport: { x: 0, y: 0, scale: 1 }, sourceLayer, generationInputs: createGenerationInputs(), }); expect(layer).toMatchObject({ id: 'layer-edit-8', resourceId: 'local-resource-edit-8', title: '源图 修改结果', x: 472, y: 140, width: 1024, height: 1024, sourceResourceId: 'resource-source', prompt: '修改要求', }); }); it('creates quick edit layers centered on the generating placeholder', () => { const sourceLayer = createSourceLayer(); const layer = createQuickEditResultLayer({ generated: createGenerated({ width: 1536, height: 1024 }), generatedIndex: 10, sourceLayer, generationInputs: createGenerationInputs(), frame: { x: 600, y: 300, width: 320, height: 240, originalWidth: 1536, originalHeight: 1024, }, }); expect(layer).toMatchObject({ id: 'layer-quick-edit-10', x: -8, y: -92, width: 1536, height: 1024, }); }); it('creates quick edit layers with source group and asset kind preserved', () => { const sourceLayer = createSourceLayer(); const layer = createQuickEditResultLayer({ generated: createGenerated({ width: 1536, height: 1024 }), generatedIndex: 9, sourceLayer, generationInputs: createGenerationInputs(), }); expect(layer).toMatchObject({ id: 'layer-quick-edit-9', resourceId: 'local-resource-quick-edit-9', title: '源图 快速编辑', x: 472, y: 140, width: 1536, height: 1024, originalWidth: 1536, originalHeight: 1024, sourceResourceId: 'resource-source', groupId: 'group-a', assetKind: 'spec', }); }); it('creates wrapped icon layers with icon metadata', () => { const layers = createIconSpritesheetResultLayers({ generated: { spritesheetImageSrc: 'data:image/png;base64,sheet', spritesheetWidth: 512, spritesheetHeight: 512, iconImageSrcs: [], prompt: '图标 prompt', actualPrompt: '图标 actual prompt', model: 'gpt-image-2', provider: 'VectorEngine', taskId: 'task-icons', priceMudPoints: 12, }, iconResults: [ { name: '返回按钮', imageSrc: 'data:image/png;base64,back', width: 256, height: 256, }, { name: '设置按钮', imageSrc: 'data:image/png;base64,settings', width: 512, height: 256, }, { name: '提示按钮', imageSrc: 'data:image/png;base64,hint', width: 128, height: 128, }, ], startIndex: 11, canvasSize: { width: 900, height: 640 }, viewport: { x: 0, y: 0, scale: 1 }, frame: { x: 200, y: 100, width: 360, height: 360, originalWidth: 512, originalHeight: 512, }, generationInputs: createGenerationInputs(), }); expect(layers).toHaveLength(3); expect(layers[0]).toMatchObject({ id: 'layer-icon-11', title: '返回按钮', x: 200, y: 100, width: 256, height: 256, assetKind: 'icon', prompt: '图标 prompt', actualPrompt: '图标 actual prompt', }); expect(layers[1]).toMatchObject({ id: 'layer-icon-12', title: '设置按钮', x: 200, y: 380, width: 512, height: 256, assetKind: 'icon', }); expect(layers[2]).toMatchObject({ id: 'layer-icon-13', title: '提示按钮', x: 200, y: 660, width: 128, height: 128, assetKind: 'icon', }); }); it('creates audio result layers centered on the active placeholder', () => { const layer = createAudioResultLayer({ generated: { audioSrc: '/generated-character-drafts/editor-audios/sfx.mp3', width: 420, height: 120, sourceType: 'generated', prompt: '金币掉落叮当声', actualPrompt: '金币掉落叮当声', model: 'audio1.0', provider: 'VectorEngine', taskId: 'sound-task-1', priceMudPoints: 10, audioKind: 'sound-effect', }, generatedIndex: 14, title: '游戏音效 14', canvasSize: { width: 900, height: 640 }, viewport: { x: 10, y: 20, scale: 2 }, frame: { x: 80, y: 60, width: 420, height: 120, originalWidth: 420, originalHeight: 120, }, generationInputs: { fields: [{ title: 'sound', value: '金币掉落叮当声' }], references: [], }, }); expect(layer).toMatchObject({ id: 'layer-audio-14', resourceId: 'local-resource-audio-14', title: '游戏音效 14', src: '/generated-character-drafts/editor-audios/sfx.mp3', mediaType: 'audio', assetKind: 'sound-effect', x: 80, y: 60, width: 420, height: 120, originalWidth: 420, originalHeight: 120, sourceType: 'generated', prompt: '金币掉落叮当声', model: 'audio1.0', provider: 'VectorEngine', taskId: 'sound-task-1', }); }); it('creates character animation result layers with action metadata', () => { const layer = createCharacterAnimationResultLayer({ generated: { taskId: 'character-animation-task-1', model: 'seedance2.0-fast', prompt: '待机动作', previewVideoPath: '/generated-character-drafts/editor/preview.mp4', frames: [ { frameIndex: 1, imageSrc: '/generated-character-drafts/editor/frame1.png', width: 1024, height: 1024, }, ], frameCount: 32, durationSeconds: 4, fps: 8, priceMudPoints: 40, }, generatedIndex: 15, title: '角色动作', canvasSize: { width: 900, height: 640 }, viewport: { x: 10, y: 20, scale: 2 }, frame: { x: 80, y: 60, width: 420, height: 420, originalWidth: 1024, originalHeight: 1024, }, generationInputs: { fields: [{ title: '动作描述', value: '待机动作' }], references: [], }, }); expect(layer).toMatchObject({ id: 'layer-character-animation-15', resourceId: 'local-resource-character-animation-15', title: '角色动作', src: '/generated-character-drafts/editor/frame1.png', x: -222, y: -242, width: 1024, height: 1024, originalWidth: 1024, originalHeight: 1024, zIndex: 25, sourceType: 'generated', prompt: '待机动作', actualPrompt: '待机动作', model: 'seedance2.0-fast', provider: 'ark', taskId: 'character-animation-task-1', assetKind: 'character-animation', }); expect(layer?.mediaType).toBeUndefined(); expect(layer?.generationInputs?.fields[0]?.value).toBe('待机动作'); }); });