Files
Genarrative/src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts
kdletters bf24d259a7 拆分编辑器高级生成提交模型
抽出图标素材生成校验和请求参数组装

抽出角色动画生成请求参数组装

补充高级生成提交模型单测

更新 TRACKING.md 记录第三十八阶段验证
2026-06-17 18:27:33 +08:00

334 lines
8.7 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import type { CanvasLayer } from './ImageCanvasEditorTypes';
import {
buildCharacterAnimationSubmissionPlan,
buildIconSpritesheetGenerationSubmissionPlan,
buildImageGenerationSubmissionPlan,
} from './ImageCanvasGenerationSubmissionModel';
function createLayer(overrides: Partial<CanvasLayer> = {}): 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',
},
});
});
});