Files
Genarrative/src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts
T
k88936 3485bad2d3 重构:统一编辑器工作流中的 "icon-spec" 处理
- 将所有相关工作流从 "ui-spec" 迁移至 "icon-spec"。
- 优化 `playSetting` 和 `artStyle` 的输入处理逻辑。
- 更新编辑器提示词、提交模型及测试用例以适配 "icon-spec"。
- 在 UI 及后端调用中集成 `EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH` 及相关校验。
- 新增 "icon-spec" 生成与微调的专用 API 路径。
- 移除旧版 "ui-spec" 相关定义及提示词构建器。
2026-08-04 20:04:07 +08:00

1212 lines
33 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import type {
CanvasLayer,
CharacterAnimationPanelState,
GenerateDialogState,
} from './ImageCanvasEditorTypes';
import {
appendCharacterReference,
appendGenerationReference,
assignCharacterSpecReference,
assignIconSpecReference,
assignUiDesignSpecReference,
closeGenerateComposerDialog,
createAudioRedrawGenerationDialogDraft,
createBackgroundMusicGenerationDialogDraft,
createCharacterAnimationGenerationDialogDraft,
createCharacterAnimationPanelDraft,
createCharacterGenerationDialogDraft,
createEditDialogDraft,
createGenerateDialogDraft,
createIconGenerationDialogDraft,
createLayerGenerationDialogDraft,
createPublicationGenerationDialogDraft,
createQuickEditGenerationDialogDraft,
createQuickEditPanelDraft,
createRedrawPanelDraft,
createSameSourceGenerationDialogDraft,
createSoundEffectGenerationDialogDraft,
createSpecDialogDraft,
createUiDesignGenerationDialogDraft,
createVideoGenerationDialogDraft,
hideGeneratedLayerComposerAfterBlur,
updateCharacterAnimationDurationPanel,
updateIconDescriptionsTextInDialog,
updateSpecFormDialogValue,
} from './ImageCanvasGenerationDialogModel';
import {
IMAGE_MODEL_GPT_IMAGE_2,
IMAGE_MODEL_NANOBANANA2,
} from './ImageCanvasGenerationModel';
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('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,
style: 'none',
placeholder: {
x: -312,
y: -332,
width: 1024,
height: 1024,
originalWidth: 1024,
originalHeight: 1024,
},
});
expect(
createSpecDialogDraft({ canvasSize, viewport, specType: 'character' }),
).toMatchObject({
mode: 'spec',
specType: 'character',
specValues: {
playSetting: '',
artStyle: '',
bodyRatio: '3',
},
});
expect(
createSpecDialogDraft({ canvasSize, viewport, specType: 'icon' }),
).toMatchObject({
mode: 'spec',
specType: 'icon',
specValues: {
playSetting: '',
artStyle: '',
},
placeholder: {
x: -824,
y: -396,
width: 2048,
height: 1152,
originalWidth: 2048,
originalHeight: 1152,
},
});
});
it('creates publication material drafts with the workflow output size', () => {
const canvasSize = { width: 960, height: 720 };
const viewport = { x: 0, y: 0, scale: 1 };
expect(
createPublicationGenerationDialogDraft({
canvasSize,
viewport,
workflowId: 'publication-promo-poster',
}),
).toMatchObject({
mode: 'publication',
publicationWorkflowId: 'publication-promo-poster',
placeholder: {
x: -160,
y: 0,
width: 1280,
height: 720,
originalWidth: 1280,
originalHeight: 720,
},
});
});
it('creates character and icon generation drafts with the selected 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',
style: 'none',
imageModel: 'gpt-image-2',
aspectRatio: '1:1',
imageSize: '1K',
characterSpecReference: null,
characterReferences: [],
placeholder: {
x: -32,
y: -152,
width: 1024,
height: 1024,
},
});
expect(
createIconGenerationDialogDraft({
canvasSize,
viewport,
imageModel: 'unknown-model',
}),
).toMatchObject({
mode: 'icon',
style: 'none',
imageModel: 'unknown-model',
aspectRatio: '1:1',
imageSize: '1K',
prompt: '',
iconSpecReference: null,
iconDescriptions: [],
placeholder: {
x: -32,
y: -152,
width: 1024,
height: 1024,
originalWidth: 1024,
originalHeight: 1024,
},
});
});
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: 'on',
videoWebSearchEnabled: true,
placeholder: {
x: 53,
y: 120,
width: 854,
height: 480,
originalWidth: 854,
originalHeight: 480,
},
});
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: -32,
y: 72,
width: 1024,
height: 576,
},
});
});
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',
prompt: '',
status: 'idle',
composerOpen: true,
soundModel: 'audio1.0',
soundDurationSeconds: 5,
placeholder: {
x: 270,
y: 300,
width: 420,
height: 120,
originalWidth: 420,
originalHeight: 120,
},
});
expect(
createBackgroundMusicGenerationDialogDraft({ canvasSize, viewport }),
).toMatchObject({
mode: 'audio-background-music',
prompt: '',
status: 'idle',
composerOpen: true,
makeInstrumental: true,
placeholder: {
x: 270,
y: 300,
width: 420,
height: 120,
},
});
});
it('creates audio remodel drafts from generated audio layers without references', () => {
const soundLayer = createLayer({
id: 'layer-sfx',
title: '游戏音效',
mediaType: 'audio',
assetKind: 'sound-effect',
prompt: '【系统内置】完整音效提示词:金币收集音,自动混音,内部参数。',
generationInputs: {
fields: [
{ title: 'prompt', value: '金币收集音' },
{ title: 'model', value: 'audio1.0' },
{ title: 'duration', value: '7秒' },
],
references: [],
},
});
const musicLayer = createLayer({
id: 'layer-music',
title: '游戏背景音乐',
mediaType: 'audio',
assetKind: 'background-music',
prompt: '',
generationInputs: {
fields: [{ title: 'gpt_description_prompt', value: '地牢循环音乐' }],
references: [],
},
});
expect(createAudioRedrawGenerationDialogDraft(soundLayer)).toMatchObject({
mode: 'audio-sound-effect',
sourceLayerId: 'layer-sfx',
prompt: '金币收集音',
soundModel: 'audio1.0',
soundDurationSeconds: 7,
composerOpen: true,
placeholder: {
x: 472,
y: 140,
width: 420,
height: 120,
},
});
expect(createAudioRedrawGenerationDialogDraft(musicLayer)).toMatchObject({
mode: 'audio-background-music',
sourceLayerId: 'layer-music',
prompt: '地牢循环音乐',
makeInstrumental: true,
composerOpen: true,
placeholder: {
x: 472,
y: 140,
width: 420,
height: 120,
},
});
expect(createAudioRedrawGenerationDialogDraft(createLayer())).toBeNull();
});
it('keeps audio remodel prompts blank when no user input snapshot exists', () => {
expect(
createAudioRedrawGenerationDialogDraft(
createLayer({
mediaType: 'audio',
assetKind: 'sound-effect',
prompt: '【系统内置】完整音效提示词',
generationInputs: { fields: [], references: [] },
}),
),
).toMatchObject({
mode: 'audio-sound-effect',
prompt: '',
});
expect(
createAudioRedrawGenerationDialogDraft(
createLayer({
mediaType: 'audio',
assetKind: 'background-music',
title: '游戏背景音乐',
prompt: '【系统内置】完整背景音乐提示词',
generationInputs: {
fields: [{ title: 'duration', value: '10秒' }],
references: [],
},
}),
),
).toMatchObject({
mode: 'audio-background-music',
prompt: '',
});
});
it('creates edit, quick-edit, and character animation panel drafts', () => {
const sourceLayer = createLayer({
prompt: '【系统内置】完整拼接提示词:原图提示,自动画质优化。',
model: 'gpt-image-2',
assetKind: 'character',
generationInputs: {
fields: [{ title: '生成提示词', value: '原图提示' }],
references: [],
},
});
expect(createEditDialogDraft(sourceLayer)).toEqual({
id: 'edit-dialog:layer-source',
mode: 'edit',
prompt: '原图提示',
status: 'idle',
composerOpen: true,
sourceLayerId: 'layer-source',
imageModel: 'gpt-image-2',
aspectRatio: '4:3',
imageSize: '1K',
});
expect(createQuickEditPanelDraft(sourceLayer)).toEqual({
mode: 'quick-edit',
sourceLayerId: 'layer-source',
prompt: '',
size: '1024x768',
aspectRatio: '4:3',
imageSize: '1K',
model: 'gpt-image-2',
quickEditReferences: [],
status: 'idle',
});
expect(createRedrawPanelDraft(sourceLayer).prompt).toBe('原图提示');
expect(createCharacterAnimationPanelDraft(sourceLayer)).toEqual({
sourceLayerId: 'layer-source',
promptText: '',
resolution: '480p',
ratio: 'same',
frameCount: 32,
durationSeconds: 4,
status: 'idle',
});
expect(createCharacterAnimationPanelDraft(createLayer())).toBeNull();
});
it('preserves the resolved asset label in quick-edit dialog snapshots', () => {
expect(
createQuickEditGenerationDialogDraft({
sourceLayer: createLayer(),
prompt: '让视频下雨',
assetLabel: '雨天版本',
}),
).toMatchObject({
mode: 'quick-edit',
prompt: '让视频下雨',
assetLabel: '雨天版本',
});
});
it('restores remodel prompts from user-visible generation input snapshots only', () => {
const cases: Array<{
fields: NonNullable<CanvasLayer['generationInputs']>['fields'];
expectedPrompt: string;
}> = [
{
fields: [{ title: '视频描述', value: '用户的视频描述' }],
expectedPrompt: '用户的视频描述',
},
{
fields: [{ title: '角色设定', value: '用户的角色设定' }],
expectedPrompt: '用户的角色设定',
},
{
fields: [{ title: '用户输入', value: '用户的 UI 需求' }],
expectedPrompt: '用户的 UI 需求',
},
{
fields: [{ title: '素材描述', value: '返回按钮\n设置按钮' }],
expectedPrompt: '返回按钮\n设置按钮',
},
{
fields: [{ title: '自定义规范提示词', value: '用户自定义规范' }],
expectedPrompt: '用户自定义规范',
},
{
fields: [
{ title: '玩法设定', value: '平台跳跃' },
{ title: '美术风格', value: '像素风' },
],
expectedPrompt: '玩法设定:平台跳跃\n美术风格:像素风',
},
{
fields: [
{ title: '游戏名', value: '星海远征' },
{ title: '游戏分类', value: '冒险' },
{ title: '一句话描述游戏', value: '驾驶飞船探索星云' },
],
expectedPrompt:
'游戏名:星海远征\n游戏分类:冒险\n一句话描述游戏:驾驶飞船探索星云',
},
{
fields: [{ title: '动作描述', value: '待机动作' }],
expectedPrompt: '待机动作',
},
];
for (const testCase of cases) {
expect(
createRedrawPanelDraft(
createLayer({
prompt: '【系统内置】完整拼接提示词,包含默认规则和模型约束。',
generationInputs: {
fields: testCase.fields,
references: [],
},
}),
).prompt,
).toBe(testCase.expectedPrompt);
}
expect(
createRedrawPanelDraft(
createLayer({
prompt: '【系统内置】固定素材提取提示词',
generationInputs: {
fields: [
{
title: '提取提示词',
value:
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选单一纯色抠图背景。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。',
},
{ title: 'model', value: 'gpt-image-2' },
],
references: [],
},
}),
).prompt,
).toBe('');
});
it('creates same-source generator drafts for generated image layers', () => {
const canvasSize = { width: 960, height: 720 };
const viewport = { x: 0, y: 0, scale: 1 };
const characterDraft = createSameSourceGenerationDialogDraft({
sourceLayer: createLayer({
assetKind: 'character',
generationInputs: {
fields: [
{ title: '角色设定', value: '红发骑士' },
{ title: '抠图背景色', value: '暖浅桃色 #FFD6C2' },
{ title: '抠图模型', value: '动漫风格 anime-seg' },
],
references: [],
},
}),
canvasSize,
viewport,
mode: 'redraw',
});
expect(characterDraft).toMatchObject({
mode: 'character',
prompt: '红发骑士',
sourceLayerId: 'layer-source',
placeholder: {
x: 472,
y: 140,
width: 1024,
height: 768,
originalWidth: 1024,
originalHeight: 768,
},
});
const publicationDraft = createSameSourceGenerationDialogDraft({
sourceLayer: createLayer({
assetKind: 'publication-material',
originalWidth: 720,
originalHeight: 1280,
generationInputs: {
fields: [
{ title: '游戏名', value: '星海远征' },
{ title: '游戏分类', value: '冒险' },
{ title: '一句话描述游戏', value: '驾驶飞船探索星云' },
],
references: [],
},
}),
canvasSize,
viewport,
mode: 'redraw',
});
expect(publicationDraft).toMatchObject({
mode: 'publication',
publicationWorkflowId: 'publication-detail-gallery',
publicationGameInfo: {
gameName: '星海远征',
gameCategories: '冒险',
gameDescription: '驾驶飞船探索星云',
},
});
expect(
createSameSourceGenerationDialogDraft({
sourceLayer: createLayer(),
canvasSize,
viewport,
mode: 'redraw',
}),
).toBeNull();
});
it('restores the plain image generator when a source generation dialog exists', () => {
const canvasSize = { width: 960, height: 720 };
const viewport = { x: 0, y: 0, scale: 1 };
expect(
createSameSourceGenerationDialogDraft({
sourceLayer: createLayer({
prompt: '【系统内置】普通提示词',
generationInputs: {
fields: [{ title: '生成提示词', value: '普通图' }],
references: [],
},
}),
canvasSize,
viewport,
sourceDialog: {
id: 'generation-dialog-1',
mode: 'generate',
prompt: '普通图',
status: 'idle',
generatedLayerId: 'layer-source',
imageModel: 'gpt-image-2',
aspectRatio: '2:3',
imageSize: '2K',
style: 'pixelArt',
},
mode: 'redraw',
}),
).toMatchObject({
mode: 'generate',
prompt: '普通图',
sourceLayerId: 'layer-source',
imageModel: 'gpt-image-2',
aspectRatio: '2:3',
imageSize: '2K',
style: 'none',
placeholder: {
x: 472,
y: 140,
width: 1365,
height: 2048,
originalWidth: 1365,
originalHeight: 2048,
},
});
});
it.each([
['character', 'character'],
['icon', 'icon-spritesheet'],
['ui-design', 'ui-design'],
] as const)(
'restores %s remodel placeholders to the selected 2K dimensions',
(mode, assetKind) => {
expect(
createSameSourceGenerationDialogDraft({
sourceLayer: createLayer({ assetKind }),
canvasSize: { width: 960, height: 720 },
viewport: { x: 0, y: 0, scale: 1 },
sourceDialog: {
id: `generation-dialog-${mode}`,
mode,
prompt: '来源生成器',
status: 'idle',
generatedLayerId: 'layer-source',
imageModel: IMAGE_MODEL_GPT_IMAGE_2,
aspectRatio: '2:3',
imageSize: '2K',
},
mode: 'redraw',
}),
).toMatchObject({
mode,
imageModel: IMAGE_MODEL_GPT_IMAGE_2,
aspectRatio: '2:3',
imageSize: '2K',
placeholder: {
x: 472,
y: 140,
width: 1365,
height: 2048,
originalWidth: 1365,
originalHeight: 2048,
},
});
},
);
it('restores a plain generated layer from its own metadata', () => {
const canvasSize = { width: 960, height: 720 };
const viewport = { x: 0, y: 0, scale: 1 };
expect(
createLayerGenerationDialogDraft({
sourceLayer: createLayer({
sourceType: 'generated',
prompt: '图层提示词',
model: 'gpt-image-2',
generationInputs: {
fields: [{ title: '生成提示词', value: '元数据提示词' }],
references: [],
},
}),
canvasSize,
viewport,
}),
).toMatchObject({
mode: 'generate',
prompt: '元数据提示词',
generatedLayerId: 'layer-source',
imageModel: 'gpt-image-2',
placeholder: {
x: 120,
y: 140,
width: 320,
height: 240,
originalWidth: 1024,
originalHeight: 768,
},
});
});
it('restores a tagged uploaded layer from its current asset kind', () => {
expect(
createLayerGenerationDialogDraft({
sourceLayer: createLayer({
sourceType: 'uploaded',
assetKind: 'character',
generationInputs: {
fields: [{ title: '角色设定', value: '黑衣剑士' }],
references: [],
},
}),
canvasSize: { width: 960, height: 720 },
viewport: { x: 0, y: 0, scale: 1 },
}),
).toMatchObject({
mode: 'character',
prompt: '黑衣剑士',
generatedLayerId: 'layer-source',
});
});
it('preserves 2K dimensions when reopening a persisted character layer', () => {
expect(
createLayerGenerationDialogDraft({
sourceLayer: createLayer({
sourceType: 'generated',
assetKind: 'character',
model: IMAGE_MODEL_NANOBANANA2,
originalWidth: 2048,
originalHeight: 2048,
generationInputs: {
fields: [{ title: '角色设定', value: '香水师角色' }],
references: [],
},
}),
canvasSize: { width: 960, height: 720 },
viewport: { x: 0, y: 0, scale: 1 },
}),
).toMatchObject({
mode: 'character',
prompt: '香水师角色',
imageModel: IMAGE_MODEL_NANOBANANA2,
aspectRatio: '1:1',
imageSize: '2K',
generatedLayerId: 'layer-source',
});
});
it('restores video and audio generators from media layer metadata', () => {
const context = {
canvasSize: { width: 960, height: 720 },
viewport: { x: 0, y: 0, scale: 1 },
};
expect(
createLayerGenerationDialogDraft({
...context,
sourceLayer: createLayer({
title: '技能演示',
mediaType: 'video',
assetKind: 'video',
sourceType: 'generated',
generationInputs: {
fields: [{ title: '视频描述', value: '火球飞行动画' }],
references: [],
},
}),
}),
).toMatchObject({
mode: 'video',
prompt: '火球飞行动画',
generatedLayerId: 'layer-source',
});
expect(
createLayerGenerationDialogDraft({
...context,
sourceLayer: createLayer({
title: '金币音效',
mediaType: 'audio',
assetKind: 'sound-effect',
sourceType: 'generated',
generationInputs: {
fields: [{ title: 'prompt', value: '金币弹出声' }],
references: [],
},
}),
}),
).toMatchObject({
mode: 'audio-sound-effect',
prompt: '金币弹出声',
generatedLayerId: 'layer-source',
});
expect(
createLayerGenerationDialogDraft({
...context,
sourceLayer: createLayer({
title: '背景音乐',
mediaType: 'audio',
assetKind: 'background-music',
sourceType: 'generated',
generationInputs: {
fields: [
{ title: 'gpt_description_prompt', value: '轻松森林音乐' },
],
references: [],
},
}),
}),
).toMatchObject({
mode: 'audio-background-music',
prompt: '轻松森林音乐',
generatedLayerId: 'layer-source',
});
});
it('inherits and normalizes quick edit model parameters from the source', () => {
expect(
createQuickEditPanelDraft(
createLayer({
model: 'nano-banana',
}),
),
).toMatchObject({
model: IMAGE_MODEL_NANOBANANA2,
aspectRatio: '4:3',
imageSize: '1K',
});
expect(
createQuickEditPanelDraft(createLayer({ model: 'nanobanana2' }), {
imageModel: IMAGE_MODEL_GPT_IMAGE_2,
aspectRatio: '16:9',
imageSize: '2K',
}),
).toMatchObject({
model: IMAGE_MODEL_GPT_IMAGE_2,
aspectRatio: '16:9',
imageSize: '2K',
});
expect(
createQuickEditPanelDraft(
createLayer({ originalWidth: 512, originalHeight: 512 }),
{ imageModel: IMAGE_MODEL_GPT_IMAGE_2, imageSize: '0.5K' },
).imageSize,
).toBe('1K');
expect(
createQuickEditPanelDraft(createLayer({ model: 'legacy-image-model' })),
).toMatchObject({ model: IMAGE_MODEL_NANOBANANA2 });
});
it('creates character animation generation dialog drafts for character layers', () => {
const canvasSize = { width: 960, height: 720 };
const viewport = { x: 0, y: 0, scale: 1 };
const characterLayer = createLayer({
id: 'layer-character',
assetKind: 'character',
});
expect(
createCharacterAnimationGenerationDialogDraft({
canvasSize,
viewport,
layer: characterLayer,
}),
).toMatchObject({
mode: 'character-animation',
sourceLayerId: 'layer-character',
status: 'idle',
composerOpen: true,
characterAnimationResolution: '480p',
characterAnimationRatio: 'same',
characterAnimationFrameCount: 32,
characterAnimationDurationSeconds: 4,
placeholder: {
x: 270,
y: 150,
width: 420,
height: 420,
originalWidth: 1024,
originalHeight: 1024,
},
});
expect(
createCharacterAnimationGenerationDialogDraft({
canvasSize,
viewport,
layer: 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: '参考图' });
const characterSpecLayer = createLayer({
title: '角色规范',
assetKind: 'spec',
});
expect(
assignCharacterSpecReference(characterDialog, characterSpecLayer),
).toEqual(
expect.objectContaining({
status: 'idle',
errorMessage: undefined,
composerOpen: true,
characterSpecReference: expect.objectContaining({
id: 'canvas-layer-source',
label: '角色规范',
src: 'data:image/png;base64,source',
resourceId: 'resource-source',
}),
}),
);
expect(assignCharacterSpecReference(characterDialog, sourceLayer)).toBe(
characterDialog,
);
expect(
assignCharacterSpecReference(
characterDialog,
createLayer({ title: '角色形象', assetKind: 'character' }),
),
).toBe(characterDialog);
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);
expect(
assignIconSpecReference(
iconDialog,
createLayer({
title: '旧UI素材规范',
assetKind: 'spec',
generationInputs: {
fields: [
{
title: '玩法设定',
value: '生成一张完整游戏UI规范汇总设定展板',
},
],
references: [],
},
}),
),
).toMatchObject({
status: 'idle',
errorMessage: undefined,
iconSpecReference: { label: '旧UI素材规范' },
});
});
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: [],
}),
);
});
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,
});
});
});