334 lines
8.7 KiB
TypeScript
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',
|
|
},
|
|
});
|
|
});
|
|
});
|