import { describe, expect, it } from 'vitest'; import type { CanvasLayer } from './ImageCanvasEditorTypes'; import { buildCharacterAnimationSubmissionPlan, buildIconSpritesheetGenerationSubmissionPlan, buildImageGenerationSubmissionPlan, } from './ImageCanvasGenerationSubmissionModel'; 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('ImageCanvasGenerationSubmissionModel', () => { it('builds normal image generation submission plans', () => { const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'generate', prompt: ' 一张发光主视觉 ', status: 'idle', }, layers: [], nextGeneratedIndex: 3, }); expect(plan).toEqual({ kind: 'image', normalizedPrompt: '一张发光主视觉', input: { prompt: '一张发光主视觉', }, result: { generationInputs: { fields: [{ title: '生成提示词', value: '一张发光主视觉' }], references: [], }, }, }); }); it('builds edit submission plans with the source layer snapshot', () => { const sourceLayer = createLayer(); const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'edit', prompt: '', status: 'idle', sourceLayerId: sourceLayer.id, }, layers: [sourceLayer], nextGeneratedIndex: 4, }); expect(plan).toMatchObject({ kind: 'edit', normalizedPrompt: '修改当前图片', sourceLayer, generationInputs: { fields: [{ title: '修改要求', value: '修改当前图片' }], references: [ { title: '参考图', label: '源图', src: 'data:image/png;base64,source', }, ], }, }); }); it('builds spec generation plans with reference prompt semantics', () => { const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'spec', prompt: '', status: 'idle', specType: 'icon', specValues: { playSetting: '休闲消除', artStyle: '清爽卡通', bodyRatio: '3', characterView: '', customPrompt: '', }, specReference: { id: 'spec-ref', label: '参考.png', src: 'data:image/png;base64,ref', }, }, layers: [], nextGeneratedIndex: 8, }); expect(plan).toMatchObject({ kind: 'image', normalizedPrompt: 'AI 生成图片', input: { size: '2048x1152', kind: 'spec', referenceImageSrcs: ['data:image/png;base64,ref'], prompt: expect.stringContaining('参考图生成规范'), }, result: { assetKind: 'icon-spec', title: '图标素材规范 8', }, }); expect(plan.kind === 'image' ? plan.result.generationInputs : null).toEqual({ fields: [ { title: '玩法设定', value: '休闲消除' }, { title: '美术风格', value: '清爽卡通' }, ], references: [ { title: '参考图', label: '参考.png', src: 'data:image/png;base64,ref', }, ], }); }); it('builds character plans with references and remembered model', () => { const plan = buildImageGenerationSubmissionPlan({ dialog: { mode: 'character', prompt: ' 白发骑士 ', status: 'idle', imageModel: 'gpt-image-2', aspectRatio: '2:3', imageSize: '2K', characterSpecReference: { id: 'spec', label: '角色规范', src: 'data:image/png;base64,spec', }, characterReferences: [ { id: 'ref-1', label: '盔甲参考', src: 'data:image/png;base64,armor', }, ], }, layers: [], nextGeneratedIndex: 9, }); expect(plan).toMatchObject({ kind: 'image', normalizedPrompt: '白发骑士', input: { prompt: '白发骑士', kind: 'character', model: 'gpt-image-2', aspectRatio: '2:3', imageSize: '2K', referenceImageSrcs: [ 'data:image/png;base64,spec', 'data:image/png;base64,armor', ], }, result: { assetKind: 'character', title: '角色形象 9', }, rememberImageModel: 'gpt-image-2', }); expect(plan.kind === 'image' ? plan.result.generationInputs : null).toEqual({ fields: [{ title: '角色设定', value: '白发骑士' }], references: [ { title: '角色形象规范', label: '角色规范', src: 'data:image/png;base64,spec', }, { title: '常规参考图 1', label: '盔甲参考', src: 'data:image/png;base64,armor', }, ], }); }); it('throws when edit source layer is missing', () => { expect(() => buildImageGenerationSubmissionPlan({ dialog: { mode: 'edit', prompt: '修图', status: 'idle', sourceLayerId: 'missing-layer', }, layers: [], nextGeneratedIndex: 1, }), ).toThrow('未找到要修改的图片'); }); it('returns an icon spritesheet error when the spec reference is missing', () => { const plan = buildIconSpritesheetGenerationSubmissionPlan({ mode: 'icon', prompt: '', status: 'idle', iconDescriptions: ['返回按钮'], }); expect(plan).toEqual({ ok: false, errorMessage: '请选择图标素材规范', }); }); it('returns an icon spritesheet error when descriptions are empty', () => { const plan = buildIconSpritesheetGenerationSubmissionPlan({ mode: 'icon', prompt: '', status: 'idle', iconSpecReference: { id: 'icon-spec', label: '图标规范', src: 'data:image/png;base64,spec', }, iconDescriptions: [' ', '\n'], }); expect(plan).toEqual({ ok: false, errorMessage: '请填写素材描述', }); }); it('builds icon spritesheet plans with trimmed descriptions and references', () => { const plan = buildIconSpritesheetGenerationSubmissionPlan({ mode: 'icon', prompt: '', status: 'idle', imageModel: 'gpt-image-2', aspectRatio: '3:2', imageSize: '2K', iconSpecReference: { id: 'icon-spec', label: '图标规范', src: 'data:image/png;base64,spec', }, iconDescriptions: [' 返回按钮 ', '', '设置按钮'], }); expect(plan).toEqual({ ok: true, iconDescriptions: ['返回按钮', '设置按钮'], input: { referenceImageSrc: 'data:image/png;base64,spec', iconDescriptions: ['返回按钮', '设置按钮'], model: 'gpt-image-2', aspectRatio: '3:2', imageSize: '2K', }, generationInputs: { fields: [ { title: '素材描述 1', value: '返回按钮' }, { title: '素材描述 2', value: '设置按钮' }, ], references: [ { title: '图标素材规范', label: '图标规范', src: 'data:image/png;base64,spec', }, ], }, rememberImageModel: 'gpt-image-2', }); }); it('builds character animation plans with trimmed prompt and object key source', () => { const sourceLayer = createLayer({ id: 'character-layer', title: '角色图', objectKey: 'generated/character.png', assetKind: 'character', originalWidth: 960, originalHeight: 1280, }); const plan = buildCharacterAnimationSubmissionPlan({ panel: { sourceLayerId: 'character-layer', promptText: ' 循环奔跑动作 ', resolution: '720p', ratio: 'same', frameCount: 48, durationSeconds: 6, status: 'idle', }, sourceLayer, }); expect(plan).toEqual({ promptText: '循环奔跑动作', input: { sourceLayerId: 'character-layer', sourceImageSrc: 'generated/character.png', sourceWidth: 960, sourceHeight: 1280, promptText: '循环奔跑动作', resolution: '720p', ratio: 'same', frameCount: 48, durationSeconds: 6, priceMudPoints: 120, model: 'seedance2.0', }, }); }); });