ea29887d9c
- 快速编辑支持最多8张额外参考图,并将原图自动作为最后一张参考图提交 - 快速编辑尺寸和模型沿用原图配置,按禁用态参数胶囊展示 - 快速编辑提交前同步改写原图引用为实际图序号 - 快速编辑生成改为创建独立画布生成占位,并支持键盘删除生成中对象 - 对齐生成类面板参考图图标、规范类固定参数和相关文档测试
254 lines
7.8 KiB
TypeScript
254 lines
7.8 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
|
||
import { ApiClientError } from '../../services/apiClient';
|
||
import type {
|
||
CanvasGenerationDialogState,
|
||
CanvasLayer,
|
||
} from './ImageCanvasEditorTypes';
|
||
import {
|
||
buildCharacterGenerationInputs,
|
||
buildEditGenerationInputs,
|
||
buildIconGenerationInputs,
|
||
buildImageGenerationInputs,
|
||
buildQuickEditGenerationInputs,
|
||
buildQuickEditModelOptions,
|
||
buildSoundEffectGenerationInputs,
|
||
buildSpecGenerationInputs,
|
||
buildSpecPrompt,
|
||
DEFAULT_IMAGE_MODEL,
|
||
getGenerationFrameAriaLabel,
|
||
getGenerationFrameLabel,
|
||
normalizeQuickEditPromptSourceReference,
|
||
resolveCharacterAnimationSourceImageSrc,
|
||
resolveImageGenerationErrorMessage,
|
||
} from './ImageCanvasGenerationModel';
|
||
|
||
describe('ImageCanvasGenerationModel', () => {
|
||
it('builds user-facing generation input snapshots instead of backend prompts', () => {
|
||
expect(buildImageGenerationInputs(' 一张明亮主视觉 ')).toEqual({
|
||
fields: [{ title: '生成提示词', value: '一张明亮主视觉' }],
|
||
references: [],
|
||
});
|
||
|
||
expect(
|
||
buildSpecGenerationInputs('character', {
|
||
playSetting: '平台跳跃',
|
||
artStyle: '像素风',
|
||
bodyRatio: '3',
|
||
characterView: '右向三分之二侧身',
|
||
customPrompt: '',
|
||
}),
|
||
).toEqual({
|
||
fields: [
|
||
{ title: '玩法设定', value: '平台跳跃' },
|
||
{ title: '美术风格', value: '像素风' },
|
||
{ title: '头身比', value: '3' },
|
||
{ title: '角色视角', value: '右向三分之二侧身' },
|
||
],
|
||
references: [],
|
||
});
|
||
});
|
||
|
||
it('builds character, icon and edit reference snapshots', () => {
|
||
const sourceLayer = buildSourceLayer();
|
||
|
||
expect(
|
||
buildCharacterGenerationInputs(
|
||
'主角骑士',
|
||
{ id: 'spec', label: '角色规范', src: '/spec.png' },
|
||
[{ id: 'ref-1', label: '盔甲参考', src: '/armor.png' }],
|
||
),
|
||
).toEqual({
|
||
fields: [{ title: '角色设定', value: '主角骑士' }],
|
||
references: [
|
||
{ title: '角色规范', label: '角色规范', src: '/spec.png' },
|
||
{ title: '常规参考图 1', label: '盔甲参考', src: '/armor.png' },
|
||
],
|
||
});
|
||
|
||
expect(
|
||
buildIconGenerationInputs(['返回按钮', '设置按钮'], {
|
||
id: 'icon-spec',
|
||
label: '图标规范',
|
||
src: '/icon-spec.png',
|
||
}),
|
||
).toEqual({
|
||
fields: [
|
||
{ title: '素材描述', value: '返回按钮\n设置按钮' },
|
||
],
|
||
references: [
|
||
{ title: '图标规范', label: '图标规范', src: '/icon-spec.png' },
|
||
],
|
||
});
|
||
|
||
expect(
|
||
buildEditGenerationInputs('修改要求', '换成夜晚', sourceLayer),
|
||
).toEqual({
|
||
fields: [{ title: '修改要求', value: '换成夜晚' }],
|
||
references: [
|
||
{ title: '参考图', label: '原图', src: '/source.png' },
|
||
],
|
||
});
|
||
expect(
|
||
buildQuickEditGenerationInputs('快速编辑提示词', '让图2下雨', sourceLayer, [
|
||
{ id: 'ref-1', label: '天空', src: '/sky.png' },
|
||
]),
|
||
).toEqual({
|
||
fields: [{ title: '快速编辑提示词', value: '让图2下雨' }],
|
||
references: [
|
||
{ title: '参考图 1', label: '天空', src: '/sky.png' },
|
||
{ title: '参考图 2', label: '原图', src: '/source.png' },
|
||
],
|
||
});
|
||
});
|
||
|
||
it('rewrites quick edit source image references to the final image index', () => {
|
||
expect(normalizeQuickEditPromptSourceReference('把图1改成夜晚', 3)).toBe(
|
||
'把图3改成夜晚',
|
||
);
|
||
expect(normalizeQuickEditPromptSourceReference('参考原图增加暖光', 2)).toBe(
|
||
'参考图2增加暖光',
|
||
);
|
||
expect(normalizeQuickEditPromptSourceReference('保留图1构图', 1)).toBe(
|
||
'保留图1构图',
|
||
);
|
||
expect(
|
||
normalizeQuickEditPromptSourceReference('保持当前图片构图', 1),
|
||
).toBe('保持图1构图');
|
||
expect(
|
||
normalizeQuickEditPromptSourceReference('不要影响图10内容', 2),
|
||
).toBe('不要影响图10内容');
|
||
});
|
||
|
||
it('keeps generated prompts and quick edit options stable', () => {
|
||
const prompt = buildSpecPrompt('ui', {
|
||
playSetting: '消除玩法',
|
||
artStyle: '清爽卡通',
|
||
bodyRatio: '3',
|
||
characterView: '',
|
||
customPrompt: '',
|
||
});
|
||
|
||
expect(prompt).toContain('生成一张完整游戏UI规范汇总设定展板');
|
||
expect(prompt).toContain('玩法设定:消除玩法');
|
||
expect(buildSpecPrompt('custom', { ...blankSpecValues, customPrompt: '自定义' }))
|
||
.toBe('自定义');
|
||
expect(buildQuickEditModelOptions('nano-banana')).toEqual([
|
||
{ label: 'nano-banana', value: 'nano-banana' },
|
||
{ label: 'nanobanana2', value: DEFAULT_IMAGE_MODEL },
|
||
{ label: 'gpt-image-2', value: 'gpt-image-2' },
|
||
]);
|
||
});
|
||
|
||
it('adds reference image semantics and snapshots for spec generation references', () => {
|
||
const prompt = buildSpecPrompt(
|
||
'ui',
|
||
{
|
||
playSetting: '消除玩法',
|
||
artStyle: '清爽卡通',
|
||
bodyRatio: '3',
|
||
characterView: '',
|
||
customPrompt: '',
|
||
},
|
||
true,
|
||
);
|
||
|
||
expect(prompt).toContain('参考图生成规范');
|
||
expect(prompt).toContain('参考图1');
|
||
expect(prompt).toContain('生成一张完整游戏UI规范汇总设定展板');
|
||
expect(
|
||
buildSpecPrompt(
|
||
'custom',
|
||
{ ...blankSpecValues, customPrompt: '生成一张怪兽规范图' },
|
||
true,
|
||
),
|
||
).toContain('生成一张怪兽规范图');
|
||
expect(
|
||
buildSpecGenerationInputs(
|
||
'custom',
|
||
{ ...blankSpecValues, customPrompt: '生成一张怪兽规范图' },
|
||
{ id: 'spec-ref', label: '参考.png', src: '/ref.png' },
|
||
),
|
||
).toEqual({
|
||
fields: [{ title: '自定义规范提示词', value: '生成一张怪兽规范图' }],
|
||
references: [{ title: '参考图', label: '参考.png', src: '/ref.png' }],
|
||
});
|
||
});
|
||
|
||
it('uses objectKey for character animation references before falling back to src', () => {
|
||
expect(
|
||
resolveCharacterAnimationSourceImageSrc({
|
||
...buildSourceLayer(),
|
||
objectKey: 'generated/character.png',
|
||
}),
|
||
).toBe('generated/character.png');
|
||
expect(resolveCharacterAnimationSourceImageSrc(buildSourceLayer())).toBe(
|
||
'/source.png',
|
||
);
|
||
});
|
||
|
||
it('maps generation dialog mode and authorization errors to user-facing copy', () => {
|
||
const iconDialog: CanvasGenerationDialogState = {
|
||
id: 'dialog-icon',
|
||
mode: 'icon',
|
||
prompt: '',
|
||
status: 'idle',
|
||
};
|
||
|
||
expect(getGenerationFrameAriaLabel(iconDialog)).toBe('图标素材生成占位图');
|
||
expect(getGenerationFrameLabel(iconDialog)).toBe('Icon Generator');
|
||
expect(
|
||
getGenerationFrameAriaLabel({
|
||
...iconDialog,
|
||
mode: 'quick-edit',
|
||
}),
|
||
).toBe('快速编辑生成占位图');
|
||
expect(getGenerationFrameLabel({ ...iconDialog, mode: 'quick-edit' })).toBe(
|
||
'Quick Edit Generator',
|
||
);
|
||
expect(
|
||
buildSoundEffectGenerationInputs('金币掉落叮当声', 'one-shot', null),
|
||
).toEqual({
|
||
fields: [
|
||
{ title: 'sound', value: '金币掉落叮当声' },
|
||
{ title: 'type', value: 'one-shot' },
|
||
],
|
||
references: [],
|
||
});
|
||
expect(
|
||
resolveImageGenerationErrorMessage(
|
||
new ApiClientError({
|
||
message: '未授权访问(requestId: one)',
|
||
status: 401,
|
||
code: 'UNAUTHORIZED',
|
||
}),
|
||
),
|
||
).toBe('请先登录后再生成图片');
|
||
});
|
||
});
|
||
|
||
const blankSpecValues = {
|
||
playSetting: '',
|
||
artStyle: '',
|
||
bodyRatio: '3',
|
||
characterView: '',
|
||
customPrompt: '',
|
||
};
|
||
|
||
function buildSourceLayer(): CanvasLayer {
|
||
return {
|
||
id: 'layer-source',
|
||
resourceId: 'resource-source',
|
||
title: '原图',
|
||
src: '/source.png',
|
||
x: 0,
|
||
y: 0,
|
||
width: 512,
|
||
height: 512,
|
||
originalWidth: 512,
|
||
originalHeight: 512,
|
||
zIndex: 1,
|
||
sourceType: 'uploaded',
|
||
};
|
||
}
|