import { describe, expect, it } from 'vitest'; import type { CanvasLayer, CharacterAnimationPanelState, GenerateDialogState, } from './ImageCanvasEditorTypes'; import { appendCharacterReference, appendGenerationReference, assignCharacterSpecReference, assignIconSpecReference, assignUiDesignSpecReference, closeGenerateComposerDialog, createBackgroundMusicGenerationDialogDraft, createCharacterAnimationPanelDraft, createCharacterGenerationDialogDraft, createEditDialogDraft, createGenerateDialogDraft, createIconGenerationDialogDraft, createQuickEditPanelDraft, createSoundEffectGenerationDialogDraft, createSpecDialogDraft, createUiDesignGenerationDialogDraft, createVideoGenerationDialogDraft, formatIconDescriptionsText, hideGeneratedLayerComposerAfterBlur, parseIconDescriptionsText, updateCharacterAnimationDurationPanel, updateIconDescriptionsTextInDialog, updateSpecFormDialogValue, } from './ImageCanvasGenerationDialogModel'; function createLayer(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: 1024, originalHeight: 768, zIndex: 2, sourceType: 'uploaded', ...overrides, }; } describe('ImageCanvasGenerationDialogModel', () => { it('creates centered drafts for image and spec generation dialogs', () => { const canvasSize = { width: 1000, height: 800 }; const viewport = { x: 100, y: 40, scale: 2 }; expect(createGenerateDialogDraft({ canvasSize, viewport })).toMatchObject({ mode: 'generate', status: 'idle', composerOpen: true, placeholder: { x: -10, y: -30, width: 420, height: 420, originalWidth: 2048, originalHeight: 2048, }, }); expect( createSpecDialogDraft({ canvasSize, viewport, specType: 'icon' }), ).toMatchObject({ mode: 'spec', specType: 'icon', specValues: { playSetting: '休闲小游戏', artStyle: '清爽卡通', }, placeholder: { x: -80, y: 22.5, width: 560, height: 315, }, }); }); it('creates character and icon generation drafts with model dimensions', () => { const canvasSize = { width: 960, height: 720 }; const viewport = { x: 0, y: 0, scale: 1 }; expect( createCharacterGenerationDialogDraft({ canvasSize, viewport, imageModel: 'gpt-image-2', }), ).toMatchObject({ mode: 'character', imageModel: 'gpt-image-2', aspectRatio: '1:1', imageSize: '1K', characterSpecReference: null, characterReferences: [], placeholder: { x: 270, y: 150, width: 420, height: 420, }, }); expect( createIconGenerationDialogDraft({ canvasSize, viewport, imageModel: 'unknown-model', }), ).toMatchObject({ mode: 'icon', imageModel: 'unknown-model', aspectRatio: '1:1', imageSize: '1K', prompt: '返回按钮\n设置按钮\n下一关按钮\n提示按钮\n原图按钮\n冻结按钮', iconSpecReference: null, iconDescriptions: [ '返回按钮', '设置按钮', '下一关按钮', '提示按钮', '原图按钮', '冻结按钮', ], placeholder: { x: 300, y: 180, width: 360, height: 360, }, }); }); it('creates video and UI design drafts restored from the original generation entry work', () => { const canvasSize = { width: 960, height: 720 }; const viewport = { x: 0, y: 0, scale: 1 }; expect(createVideoGenerationDialogDraft({ canvasSize, viewport })) .toMatchObject({ mode: 'video', status: 'idle', composerOpen: true, generationReferences: [], videoModel: 'seedance2.0-fast', videoAspectRatio: '16:9', videoResolution: '480p', videoDurationSeconds: 4, videoMode: 'std', videoSound: 'off', placeholder: { x: 200, y: 202.5, width: 560, height: 315, originalWidth: 1280, originalHeight: 720, }, }); expect( createUiDesignGenerationDialogDraft({ canvasSize, viewport, imageModel: 'gpt-image-2', }), ).toMatchObject({ mode: 'ui-design', imageModel: 'gpt-image-2', aspectRatio: '16:9', imageSize: '1K', uiDesignSpecReference: null, placeholder: { x: 200, y: 202.5, width: 560, height: 315, }, }); }); it('creates audio generation drafts for sound effects and background music', () => { const canvasSize = { width: 960, height: 720 }; const viewport = { x: 0, y: 0, scale: 1 }; expect(createSoundEffectGenerationDialogDraft({ canvasSize, viewport })) .toMatchObject({ mode: 'audio-sound-effect', status: 'idle', composerOpen: true, soundType: 'one-shot', soundTempo: null, placeholder: { x: 270, y: 300, width: 420, height: 120, originalWidth: 420, originalHeight: 120, }, }); expect(createBackgroundMusicGenerationDialogDraft({ canvasSize, viewport })) .toMatchObject({ mode: 'audio-background-music', status: 'idle', composerOpen: true, makeInstrumental: true, placeholder: { x: 270, y: 300, width: 420, height: 120, }, }); }); it('creates edit, quick-edit, and character animation panel drafts', () => { const sourceLayer = createLayer({ prompt: '原图提示', model: 'gpt-image-2', assetKind: 'character', }); expect(createEditDialogDraft(sourceLayer)).toEqual({ mode: 'edit', prompt: '原图提示,在保持主体结构的基础上优化画面细节', status: 'idle', composerOpen: true, sourceLayerId: 'layer-source', }); expect(createQuickEditPanelDraft(sourceLayer)).toEqual({ mode: 'quick-edit', sourceLayerId: 'layer-source', prompt: '', size: '1024x768', model: 'gpt-image-2', status: 'idle', }); expect(createCharacterAnimationPanelDraft(sourceLayer)).toEqual({ sourceLayerId: 'layer-source', promptText: '', resolution: '480p', ratio: 'same', frameCount: 32, durationSeconds: 4, status: 'idle', }); expect(createCharacterAnimationPanelDraft(createLayer())).toBeNull(); }); it('resets failed character and icon dialogs when picking references', () => { const characterDialog: GenerateDialogState = { mode: 'character', prompt: '', status: 'failed', errorMessage: '请选择角色规范', characterReferences: [], }; const sourceLayer = createLayer({ title: '参考图' }); expect(assignCharacterSpecReference(characterDialog, sourceLayer)).toEqual( expect.objectContaining({ status: 'idle', errorMessage: undefined, composerOpen: true, characterSpecReference: { id: 'canvas-layer-source', label: '参考图', src: 'data:image/png;base64,source', }, }), ); expect( appendCharacterReference(characterDialog, sourceLayer), ).toMatchObject({ status: 'idle', errorMessage: undefined, characterReferences: [{ label: '参考图' }], }); const iconDialog: GenerateDialogState = { mode: 'icon', prompt: '', status: 'failed', errorMessage: '请选择图标素材规范', }; expect( assignIconSpecReference( iconDialog, createLayer({ assetKind: 'icon-spec' }), ), ).toMatchObject({ status: 'idle', errorMessage: undefined, iconSpecReference: { id: 'canvas-layer-source' }, }); expect(assignIconSpecReference(iconDialog, sourceLayer)).toBe(iconDialog); }); it('routes picked canvas references to spec, image, video, and UI design fields', () => { const sourceLayer = createLayer({ title: '参考图' }); const videoLayer = createLayer({ id: 'layer-video', title: '动作参考', src: 'https://signed.example.com/reference.mp4', mediaType: 'video', objectKey: 'generated-character-drafts/editor/seedance-references/video/reference.mp4', assetObjectId: 'asset-object-video', }); expect( appendGenerationReference( { mode: 'spec', prompt: '', status: 'failed', errorMessage: '失败', }, sourceLayer, ), ).toMatchObject({ status: 'idle', errorMessage: undefined, specReference: { label: '参考图' }, }); expect( appendGenerationReference( { mode: 'video', prompt: '', status: 'idle', generationReferences: [], }, sourceLayer, ), ).toMatchObject({ generationReferences: [{ label: '参考图' }], }); expect( appendGenerationReference( { mode: 'video', prompt: '', status: 'idle', videoModel: 'seedance2.0-fast', generationReferences: [], }, videoLayer, ), ).toMatchObject({ generationReferences: [ { label: '动作参考', mediaType: 'video', objectKey: 'generated-character-drafts/editor/seedance-references/video/reference.mp4', assetObjectId: 'asset-object-video', }, ], }); const imageDialog: GenerateDialogState = { mode: 'generate', prompt: '', status: 'idle', generationReferences: [], }; expect(appendGenerationReference(imageDialog, videoLayer)).toBe(imageDialog); const specDialog: GenerateDialogState = { mode: 'spec', prompt: '', status: 'idle', }; expect(appendGenerationReference(specDialog, videoLayer)).toBe(specDialog); const uiDialog: GenerateDialogState = { mode: 'ui-design', prompt: '', status: 'failed', errorMessage: '失败', }; expect(assignUiDesignSpecReference(uiDialog, sourceLayer)).toBe(uiDialog); expect( assignUiDesignSpecReference( uiDialog, createLayer({ title: '图标规范', assetKind: 'icon-spec' }), ), ).toMatchObject({ status: 'idle', errorMessage: undefined, uiDesignSpecReference: { label: '图标规范' }, }); }); it('limits Seedance canvas-picked references by media type', () => { const videoDialog: GenerateDialogState = { mode: 'video', prompt: '', status: 'idle', videoModel: 'seedance2.0', generationReferences: Array.from({ length: 3 }, (_, index) => ({ id: `video-${index}`, label: `视频${index + 1}`, src: `generated-character-drafts/video-${index}.mp4`, mediaType: 'video', })), }; expect( appendGenerationReference( videoDialog, createLayer({ id: 'video-extra', title: '额外视频', mediaType: 'video', src: 'generated-character-drafts/video-extra.mp4', }), ), ).toMatchObject({ generationReferences: [ { label: '视频1' }, { label: '视频2' }, { label: '视频3' }, ], }); expect( appendGenerationReference( videoDialog, createLayer({ id: 'audio-first', title: '旁白', mediaType: 'audio', src: 'generated-character-drafts/audio.mp3', }), ), ).toMatchObject({ generationReferences: [ { label: '视频1' }, { label: '视频2' }, { label: '视频3' }, { label: '旁白', mediaType: 'audio' }, ], }); }); it('updates failed spec and icon dialog fields back to idle state', () => { const specDialog: GenerateDialogState = { mode: 'spec', prompt: '', status: 'failed', errorMessage: '生成失败', specType: 'custom', specValues: { playSetting: '', artStyle: '', bodyRatio: '3', characterView: '', customPrompt: '', }, }; expect( updateSpecFormDialogValue(specDialog, 'customPrompt', '新规范'), ).toEqual( expect.objectContaining({ status: 'idle', errorMessage: undefined, specValues: expect.objectContaining({ customPrompt: '新规范', bodyRatio: '3', }), }), ); const iconDialog: GenerateDialogState = { mode: 'icon', prompt: '', status: 'failed', errorMessage: '描述错误', iconDescriptions: ['旧描述'], }; expect(updateIconDescriptionsTextInDialog(iconDialog, '剑\n盾、药水')).toEqual( expect.objectContaining({ status: 'idle', errorMessage: undefined, prompt: '剑\n盾、药水', iconDescriptions: ['剑', '盾', '药水'], }), ); expect(parseIconDescriptionsText('剑,盾\n药水/宝箱|钥匙')).toEqual([ '剑', '盾', '药水', '宝箱', '钥匙', ]); expect(formatIconDescriptionsText(['剑', '盾'])).toBe('剑\n盾'); }); it('updates character animation duration and composer visibility', () => { const failedPanel: CharacterAnimationPanelState = { sourceLayerId: 'layer-character', promptText: '', resolution: '480p', ratio: 'same', frameCount: 32, durationSeconds: 4, status: 'failed', errorMessage: '失败', }; expect(updateCharacterAnimationDurationPanel(failedPanel, '48')).toEqual({ ...failedPanel, frameCount: 48, durationSeconds: 6, status: 'idle', errorMessage: undefined, }); expect(updateCharacterAnimationDurationPanel(failedPanel, '999')).toBe( failedPanel, ); const generateDialog: GenerateDialogState = { mode: 'generate', prompt: '', status: 'idle', composerOpen: true, }; expect(hideGeneratedLayerComposerAfterBlur(generateDialog)).toEqual({ ...generateDialog, composerOpen: false, }); expect(closeGenerateComposerDialog(generateDialog)).toEqual({ ...generateDialog, composerOpen: false, }); }); });