锁定画布UI设计图模型选择
固定生成UI设计图模型展示为gpt-image-2 对齐角色规范面板的只读模型展示样式 提交层兜底强制UI设计图使用gpt-image-2 补充模型锁定与旧草稿兜底测试
This commit is contained in:
@@ -25,7 +25,7 @@
|
||||
## 生成参数
|
||||
|
||||
- 支持自定义画面比例和大小尺寸。
|
||||
- 模型固定为 `gpt-image-2`,面板不提供切换到其他模型的能力。
|
||||
- 模型固定为 `gpt-image-2`,模型展示对齐角色规范面板底部固定模型样式,不响应点击、不弹出模型切换菜单;历史草稿如果残留其他模型,提交时也必须强制改为 `gpt-image-2`。
|
||||
- 默认画面比例为 `16:9`,默认大小为 `1K`。
|
||||
|
||||
## 提示词契约
|
||||
|
||||
@@ -16,10 +16,12 @@ function ImageOptionsHarness({
|
||||
initialDialog,
|
||||
includeDimensions = true,
|
||||
onRememberImageModel = vi.fn(),
|
||||
lockedModel,
|
||||
}: {
|
||||
initialDialog: GenerateDialogState;
|
||||
includeDimensions?: boolean;
|
||||
onRememberImageModel?: (model: string) => void;
|
||||
lockedModel?: string;
|
||||
}) {
|
||||
const [dialog, setDialog] = useState<GenerateDialogState | null>(
|
||||
initialDialog,
|
||||
@@ -32,8 +34,9 @@ function ImageOptionsHarness({
|
||||
setGenerateDialog={setDialog}
|
||||
includeDimensions={includeDimensions}
|
||||
onRememberImageModel={onRememberImageModel}
|
||||
lockedModel={lockedModel}
|
||||
cost={calculateEditorImageGenerationPrice({
|
||||
model: dialog.imageModel,
|
||||
model: lockedModel ?? dialog.imageModel,
|
||||
imageSize: dialog.imageSize,
|
||||
})}
|
||||
submitLabel="生成"
|
||||
@@ -121,15 +124,11 @@ describe('ImageCanvasGenerationImageOptionsView', () => {
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: '生成图片尺寸 1:1·1K' }),
|
||||
);
|
||||
expect(
|
||||
screen.getByRole('menu', { name: '生成图片尺寸选项' }),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByRole('menu', { name: '生成图片尺寸选项' })).toBeTruthy();
|
||||
|
||||
fireEvent.click(screen.getByLabelText('当前模型'));
|
||||
|
||||
expect(
|
||||
screen.queryByRole('menu', { name: '生成图片尺寸选项' }),
|
||||
).toBeNull();
|
||||
expect(screen.queryByRole('menu', { name: '生成图片尺寸选项' })).toBeNull();
|
||||
});
|
||||
|
||||
it('renders model choices as one model per row and uses a check mark for selection', () => {
|
||||
@@ -153,7 +152,9 @@ describe('ImageCanvasGenerationImageOptionsView', () => {
|
||||
);
|
||||
const panel = screen.getByRole('menu', { name: '生成图片模型选项' });
|
||||
|
||||
expect(within(panel).getByRole('button', { name: 'nanobanana2' })).toBeTruthy();
|
||||
expect(
|
||||
within(panel).getByRole('button', { name: 'nanobanana2' }),
|
||||
).toBeTruthy();
|
||||
fireEvent.click(within(panel).getByRole('button', { name: 'gpt-image-2' }));
|
||||
|
||||
expect(screen.getByRole('menu', { name: '生成图片模型选项' })).toBeTruthy();
|
||||
@@ -166,9 +167,40 @@ describe('ImageCanvasGenerationImageOptionsView', () => {
|
||||
expect(
|
||||
screen.getByRole('button', { name: '生成图片模型 gpt-image-2' }),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '生成角色形象' }).textContent).toBe(
|
||||
'生成20泥点',
|
||||
expect(
|
||||
screen.getByRole('button', { name: '生成角色形象' }).textContent,
|
||||
).toBe('生成3泥点');
|
||||
});
|
||||
|
||||
it('locks the model trigger when a fixed model is provided', () => {
|
||||
const rememberImageModel = vi.fn();
|
||||
render(
|
||||
<ImageOptionsHarness
|
||||
initialDialog={{
|
||||
mode: 'ui-design',
|
||||
prompt: '',
|
||||
status: 'idle',
|
||||
imageModel: 'nanobanana2',
|
||||
aspectRatio: '16:9',
|
||||
imageSize: '1K',
|
||||
}}
|
||||
lockedModel={IMAGE_MODEL_GPT_IMAGE_2}
|
||||
onRememberImageModel={rememberImageModel}
|
||||
/>,
|
||||
);
|
||||
|
||||
const modelButton = screen.getByRole('button', {
|
||||
name: '生成图片模型 gpt-image-2',
|
||||
}) as HTMLButtonElement;
|
||||
|
||||
expect(modelButton.disabled).toBe(true);
|
||||
expect(modelButton.closest('div')?.className).toContain(
|
||||
'image-canvas-editor__readonly-generation-option',
|
||||
);
|
||||
expect(modelButton.textContent).not.toContain('nanobanana2');
|
||||
fireEvent.click(modelButton);
|
||||
expect(screen.queryByRole('menu', { name: '生成图片模型选项' })).toBeNull();
|
||||
expect(rememberImageModel).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders option menus and shows mud point text in the submit button', () => {
|
||||
@@ -189,9 +221,7 @@ describe('ImageCanvasGenerationImageOptionsView', () => {
|
||||
screen.getByRole('button', { name: '生成图片尺寸 1:1·1K' }),
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByRole('menu', { name: '生成图片尺寸选项' }),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByRole('menu', { name: '生成图片尺寸选项' })).toBeTruthy();
|
||||
const submit = screen.getByRole('button', { name: '生成角色形象' });
|
||||
expect(submit.textContent).toBe('生成12泥点');
|
||||
});
|
||||
@@ -219,9 +249,7 @@ describe('ImageCanvasGenerationImageOptionsView', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /生成图片尺寸/u }),
|
||||
).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: /生成图片尺寸/u })).toBeNull();
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: '生成图片模型 nanobanana2' }),
|
||||
|
||||
@@ -33,6 +33,7 @@ type ImageCanvasGenerationImageOptionsViewProps = {
|
||||
cost?: number;
|
||||
submitLabel?: string;
|
||||
submitAriaLabel?: string;
|
||||
lockedModel?: string;
|
||||
renderEditorPortal?: (node: ReactNode) => ReactNode;
|
||||
buildPortalMenuStyle?: (
|
||||
anchor: HTMLElement | null,
|
||||
@@ -53,13 +54,16 @@ function resetFailedDialogStatus(dialog: GenerateDialogState) {
|
||||
function getImageDimensionOptions(model: string | null | undefined) {
|
||||
return (
|
||||
EDITOR_IMAGE_DIMENSION_OPTIONS[
|
||||
(model ?? IMAGE_MODEL_NANOBANANA2) as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS
|
||||
(model ??
|
||||
IMAGE_MODEL_NANOBANANA2) as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS
|
||||
] ?? EDITOR_IMAGE_DIMENSION_OPTIONS[IMAGE_MODEL_NANOBANANA2]
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeImageDialogSelection(dialog: GenerateDialogState) {
|
||||
const model = normalizeEditorImageModel(dialog.imageModel ?? IMAGE_MODEL_NANOBANANA2);
|
||||
const model = normalizeEditorImageModel(
|
||||
dialog.imageModel ?? IMAGE_MODEL_NANOBANANA2,
|
||||
);
|
||||
const options = getImageDimensionOptions(model);
|
||||
const aspectRatios = options.aspectRatios as readonly string[];
|
||||
const imageSizes = options.imageSizes as readonly string[];
|
||||
@@ -113,19 +117,28 @@ export function ImageCanvasGenerationImageOptionsView({
|
||||
cost,
|
||||
submitLabel = '生成',
|
||||
submitAriaLabel = '生成',
|
||||
lockedModel,
|
||||
renderEditorPortal = (node) => node,
|
||||
buildPortalMenuStyle = () => ({}),
|
||||
}: ImageCanvasGenerationImageOptionsViewProps) {
|
||||
const [openPanel, setOpenPanel] = useState<OpenPanel>(null);
|
||||
const dimensionsButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const modelButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const selection = normalizeImageDialogSelection(dialog);
|
||||
const normalizedLockedModel = lockedModel
|
||||
? normalizeEditorImageModel(lockedModel)
|
||||
: null;
|
||||
const selection = normalizeImageDialogSelection(
|
||||
normalizedLockedModel
|
||||
? { ...dialog, imageModel: normalizedLockedModel }
|
||||
: dialog,
|
||||
);
|
||||
const selectedModelLabel = getEditorImageModelDisplayName(selection.model);
|
||||
const selectedSizeLabel = resolveEditorImageSizeLabel({
|
||||
aspectRatio: selection.aspectRatio,
|
||||
imageSize: selection.imageSize,
|
||||
});
|
||||
const isGenerating = dialog.status === 'generating';
|
||||
const isModelLocked = Boolean(normalizedLockedModel);
|
||||
const labelPrefix = optionLabelPrefix ?? '生成图片';
|
||||
const dismissOpenPanel = useCallback(() => setOpenPanel(null), []);
|
||||
|
||||
@@ -257,24 +270,42 @@ export function ImageCanvasGenerationImageOptionsView({
|
||||
: null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="image-canvas-editor__option-popover-anchor image-canvas-editor__option-popover-anchor--model">
|
||||
<div
|
||||
className={[
|
||||
'image-canvas-editor__option-popover-anchor image-canvas-editor__option-popover-anchor--model',
|
||||
isModelLocked
|
||||
? 'image-canvas-editor__readonly-generation-option'
|
||||
: '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
<PlatformInlineOptionButton
|
||||
ref={modelButtonRef}
|
||||
className="image-canvas-editor__option-cluster image-canvas-editor__option-cluster--model"
|
||||
aria-label={`${labelPrefix}模型 ${selectedModelLabel}`}
|
||||
aria-expanded={openPanel === 'model'}
|
||||
disabled={isGenerating}
|
||||
trailingIcon={<ChevronDown className="h-3 w-3" />}
|
||||
onClick={() => togglePanel('model')}
|
||||
aria-expanded={isModelLocked ? undefined : openPanel === 'model'}
|
||||
disabled={isGenerating || isModelLocked}
|
||||
trailingIcon={
|
||||
isModelLocked ? undefined : <ChevronDown className="h-3 w-3" />
|
||||
}
|
||||
onClick={() => {
|
||||
if (!isModelLocked) {
|
||||
togglePanel('model');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="image-canvas-editor__model-trigger-label">
|
||||
<span className="image-canvas-editor__model-icon" aria-hidden="true">
|
||||
<span
|
||||
className="image-canvas-editor__model-icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Cpu />
|
||||
</span>
|
||||
<span>{selectedModelLabel}</span>
|
||||
</span>
|
||||
</PlatformInlineOptionButton>
|
||||
{openPanel === 'model'
|
||||
{!isModelLocked && openPanel === 'model'
|
||||
? renderEditorPortal(
|
||||
<PlatformFloatingMenu
|
||||
className="image-canvas-editor__option-popover image-canvas-editor__option-popover--model image-canvas-editor__portal-menu"
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
calculateEditorVideoPrice,
|
||||
CHARACTER_ANIMATION_MODEL,
|
||||
DEFAULT_SOUND_EFFECT_MODEL,
|
||||
IMAGE_MODEL_GPT_IMAGE_2,
|
||||
IMAGE_MODEL_NANOBANANA2,
|
||||
} from './ImageCanvasGenerationModel';
|
||||
|
||||
@@ -167,20 +168,22 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
title: '图标规范 8',
|
||||
},
|
||||
});
|
||||
expect(plan.kind === 'image' ? plan.result.generationInputs : null).toEqual({
|
||||
fields: [
|
||||
{ title: '玩法设定', value: '休闲消除' },
|
||||
{ title: '美术风格', value: '清爽卡通' },
|
||||
],
|
||||
references: [
|
||||
{
|
||||
title: '参考图',
|
||||
label: '参考.png',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-spec-ref',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(plan.kind === 'image' ? plan.result.generationInputs : null).toEqual(
|
||||
{
|
||||
fields: [
|
||||
{ title: '玩法设定', value: '休闲消除' },
|
||||
{ title: '美术风格', value: '清爽卡通' },
|
||||
],
|
||||
references: [
|
||||
{
|
||||
title: '参考图',
|
||||
label: '参考.png',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-spec-ref',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('treats the renamed UI spec entry as an icon spec asset', () => {
|
||||
@@ -265,22 +268,62 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
},
|
||||
rememberImageModel: 'gpt-image-2',
|
||||
});
|
||||
expect(plan.kind === 'image' ? plan.result.generationInputs : null).toEqual({
|
||||
fields: [{ title: '角色设定', value: '白发骑士' }],
|
||||
references: [
|
||||
{
|
||||
title: '角色规范',
|
||||
label: '角色规范',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-character-spec',
|
||||
expect(plan.kind === 'image' ? plan.result.generationInputs : null).toEqual(
|
||||
{
|
||||
fields: [{ title: '角色设定', value: '白发骑士' }],
|
||||
references: [
|
||||
{
|
||||
title: '角色规范',
|
||||
label: '角色规范',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-character-spec',
|
||||
},
|
||||
{
|
||||
title: '常规参考图 1',
|
||||
label: '盔甲参考',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-armor',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('locks UI design generation submissions to gpt-image-2', () => {
|
||||
const plan = buildImageGenerationSubmissionPlan({
|
||||
dialog: {
|
||||
mode: 'ui-design',
|
||||
prompt: ' 设计移动端商城首页 ',
|
||||
status: 'idle',
|
||||
imageModel: 'nanobanana2',
|
||||
aspectRatio: '16:9',
|
||||
imageSize: '0.5K',
|
||||
uiDesignSpecReference: {
|
||||
id: 'ui-spec',
|
||||
label: 'UI图标规范',
|
||||
src: 'data:image/png;base64,ui-spec',
|
||||
resourceId: 'resource-ui-spec',
|
||||
},
|
||||
{
|
||||
title: '常规参考图 1',
|
||||
label: '盔甲参考',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-armor',
|
||||
},
|
||||
],
|
||||
},
|
||||
layers: [],
|
||||
nextGeneratedIndex: 6,
|
||||
});
|
||||
|
||||
expect(plan).toMatchObject({
|
||||
kind: 'image',
|
||||
normalizedPrompt: '设计移动端商城首页',
|
||||
input: {
|
||||
kind: 'ui-design',
|
||||
model: IMAGE_MODEL_GPT_IMAGE_2,
|
||||
aspectRatio: '16:9',
|
||||
imageSize: '1K',
|
||||
referenceImageSrcs: ['data:image/png;base64,ui-spec'],
|
||||
},
|
||||
result: {
|
||||
assetKind: 'ui-design',
|
||||
title: 'UI设计图 6',
|
||||
},
|
||||
rememberImageModel: IMAGE_MODEL_GPT_IMAGE_2,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -607,7 +650,6 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('builds seedance video plans with multimodal references', () => {
|
||||
const plan = buildImageGenerationSubmissionPlan({
|
||||
dialog: {
|
||||
@@ -788,7 +830,11 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
normalizedPrompt: '角色绕场一周',
|
||||
input: {
|
||||
model: 'seedance2.0-fast',
|
||||
priceMudPoints: calculateEditorVideoPrice('seedance2.0-fast', '480p', 4),
|
||||
priceMudPoints: calculateEditorVideoPrice(
|
||||
'seedance2.0-fast',
|
||||
'480p',
|
||||
4,
|
||||
),
|
||||
},
|
||||
result: {
|
||||
title: '生成视频 3',
|
||||
@@ -846,7 +892,9 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
prompt: '金币掉落叮当声',
|
||||
model: 'audio1.0',
|
||||
duration: 7,
|
||||
priceMudPoints: calculateEditorSoundEffectPrice(DEFAULT_SOUND_EFFECT_MODEL),
|
||||
priceMudPoints: calculateEditorSoundEffectPrice(
|
||||
DEFAULT_SOUND_EFFECT_MODEL,
|
||||
),
|
||||
},
|
||||
result: {
|
||||
title: '游戏音效 4',
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
DEFAULT_VIDEO_SOUND,
|
||||
DEFAULT_VIDEO_WEB_SEARCH_ENABLED,
|
||||
ICON_DESCRIPTION_LIMIT,
|
||||
IMAGE_MODEL_GPT_IMAGE_2,
|
||||
normalizeEditorImageModel,
|
||||
resolveCharacterAnimationSourceImageSrc,
|
||||
SEEDANCE_VIDEO_REFERENCE_LIMITS,
|
||||
@@ -68,6 +69,10 @@ export function isSeedanceEditorVideoModel(model: string) {
|
||||
return isSeedanceVideoModel(model);
|
||||
}
|
||||
|
||||
function normalizeGptImageSize(imageSize: string | null | undefined) {
|
||||
return imageSize?.trim().toUpperCase() === '2K' ? '2K' : '1K';
|
||||
}
|
||||
|
||||
function estimateReferenceRequestBytes(references: Array<{ src: string }>) {
|
||||
// 中文注释:JSON 包装字段还有少量额外字节;这里按 src 字符串长度估算,前后端都会再兜底校验 64MB。
|
||||
return references.reduce((sum, reference) => sum + reference.src.length, 0);
|
||||
@@ -76,8 +81,10 @@ function estimateReferenceRequestBytes(references: Array<{ src: string }>) {
|
||||
function buildSeedanceVideoReferenceInput(
|
||||
references: NonNullable<GenerateDialogState['generationReferences']>,
|
||||
) {
|
||||
const resolveReferenceSource = (reference: { src: string; objectKey?: string | null }) =>
|
||||
reference.objectKey?.trim() || reference.src;
|
||||
const resolveReferenceSource = (reference: {
|
||||
src: string;
|
||||
objectKey?: string | null;
|
||||
}) => reference.objectKey?.trim() || reference.src;
|
||||
const imageReferences = references.filter(
|
||||
(reference) => (reference.mediaType ?? 'image') === 'image',
|
||||
);
|
||||
@@ -97,7 +104,11 @@ function buildSeedanceVideoReferenceInput(
|
||||
if (audioReferences.length > SEEDANCE_VIDEO_REFERENCE_LIMITS.audio) {
|
||||
throw new Error('参考音频最多 3 个');
|
||||
}
|
||||
if (audioReferences.length && !imageReferences.length && !videoReferences.length) {
|
||||
if (
|
||||
audioReferences.length &&
|
||||
!imageReferences.length &&
|
||||
!videoReferences.length
|
||||
) {
|
||||
throw new Error('参考音频必须搭配参考图片或参考视频');
|
||||
}
|
||||
if (
|
||||
@@ -202,7 +213,9 @@ export function buildImageGenerationSubmissionPlan({
|
||||
dialog.prompt.trim() || getDialogDefaultPrompt(dialog.mode);
|
||||
|
||||
if (dialog.mode === 'edit') {
|
||||
const sourceLayer = layers.find((layer) => layer.id === dialog.sourceLayerId);
|
||||
const sourceLayer = layers.find(
|
||||
(layer) => layer.id === dialog.sourceLayerId,
|
||||
);
|
||||
if (!sourceLayer) {
|
||||
throw new Error('未找到要修改的图片');
|
||||
}
|
||||
@@ -284,7 +297,7 @@ export function buildImageGenerationSubmissionPlan({
|
||||
}
|
||||
|
||||
if (dialog.mode === 'ui-design') {
|
||||
const imageModel = normalizeEditorImageModel(dialog.imageModel);
|
||||
const imageModel = IMAGE_MODEL_GPT_IMAGE_2;
|
||||
const referenceImageSrcs = [dialog.uiDesignSpecReference?.src].filter(
|
||||
(src): src is string => Boolean(src),
|
||||
);
|
||||
@@ -296,7 +309,7 @@ export function buildImageGenerationSubmissionPlan({
|
||||
kind: 'ui-design',
|
||||
model: imageModel,
|
||||
aspectRatio: dialog.aspectRatio ?? '16:9',
|
||||
imageSize: dialog.imageSize ?? '1K',
|
||||
imageSize: normalizeGptImageSize(dialog.imageSize),
|
||||
...(referenceImageSrcs.length ? { referenceImageSrcs } : {}),
|
||||
},
|
||||
result: {
|
||||
@@ -448,7 +461,8 @@ export function buildImageGenerationSubmissionPlan({
|
||||
},
|
||||
result: {
|
||||
title: `游戏背景音乐 ${nextGeneratedIndex}`,
|
||||
generationInputs: buildBackgroundMusicGenerationInputs(normalizedPrompt),
|
||||
generationInputs:
|
||||
buildBackgroundMusicGenerationInputs(normalizedPrompt),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -484,9 +498,10 @@ export function buildImageGenerationSubmissionPlan({
|
||||
export function buildIconSpritesheetGenerationSubmissionPlan(
|
||||
dialog: GenerateDialogState,
|
||||
): IconSpritesheetGenerationSubmissionPlan {
|
||||
const iconDescriptions = (dialog.prompt.trim()
|
||||
? dialog.prompt.split(/[\r\n,,、;;/|]+/u)
|
||||
: (dialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS)
|
||||
const iconDescriptions = (
|
||||
dialog.prompt.trim()
|
||||
? dialog.prompt.split(/[\r\n,,、;;/|]+/u)
|
||||
: (dialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS)
|
||||
)
|
||||
.map((description) => description.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -88,7 +88,7 @@ describe('ImageCanvasSpecGenerationPanelView', () => {
|
||||
),
|
||||
).toBe(true);
|
||||
const submitButton = screen.getByRole('button', { name: '提交生成规范' });
|
||||
expect(submitButton.textContent).toBe('生成40泥点');
|
||||
expect(submitButton.textContent).toBe('生成5泥点');
|
||||
const dimensionsButton = screen.getByRole('button', {
|
||||
name: '生成图片尺寸 16:9·2K',
|
||||
}) as HTMLButtonElement;
|
||||
@@ -135,19 +135,23 @@ describe('ImageCanvasSpecGenerationPanelView', () => {
|
||||
),
|
||||
).toBe(true);
|
||||
expect(panel.textContent).not.toContain('UI设计要求');
|
||||
expect(prompt.getAttribute('placeholder')).toBe(
|
||||
'你希望这个 UI 长什么样?',
|
||||
);
|
||||
expect(prompt.getAttribute('placeholder')).toBe('你希望这个 UI 长什么样?');
|
||||
expect(prompt.className).toContain(
|
||||
'image-canvas-editor__generation-prompt--borderless',
|
||||
);
|
||||
const submitButton = screen.getByRole('button', { name: '生成UI设计图' });
|
||||
expect(submitButton.textContent).toBe('生成12泥点');
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成图片模型 nanobanana2' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'gpt-image-2' }));
|
||||
expect(screen.getByRole('button', { name: '生成UI设计图' }).textContent).toBe(
|
||||
'生成20泥点',
|
||||
const modelButton = screen.getByRole('button', {
|
||||
name: '生成图片模型 gpt-image-2',
|
||||
}) as HTMLButtonElement;
|
||||
|
||||
expect(submitButton.textContent).toBe('生成3泥点');
|
||||
expect(modelButton.disabled).toBe(true);
|
||||
expect(modelButton.closest('div')?.className).toContain(
|
||||
'image-canvas-editor__readonly-generation-option',
|
||||
);
|
||||
fireEvent.click(modelButton);
|
||||
expect(screen.queryByRole('menu', { name: '生成图片模型选项' })).toBeNull();
|
||||
expect(screen.queryByText('nanobanana2')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders character spec fields and forwards updates', () => {
|
||||
@@ -232,7 +236,7 @@ describe('ImageCanvasSpecGenerationPanelView', () => {
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: '提交生成规范' });
|
||||
|
||||
expect(submitButton.textContent).toBe('生成40泥点');
|
||||
expect(submitButton.textContent).toBe('生成5泥点');
|
||||
});
|
||||
|
||||
it('disables controls while generating and renders failure state', () => {
|
||||
@@ -249,9 +253,9 @@ describe('ImageCanvasSpecGenerationPanelView', () => {
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交生成规范' }));
|
||||
|
||||
expect((screen.getByLabelText('玩法设定') as HTMLInputElement).disabled).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
(screen.getByLabelText('玩法设定') as HTMLInputElement).disabled,
|
||||
).toBe(true);
|
||||
expect(submitSpec).not.toHaveBeenCalled();
|
||||
|
||||
rerender(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user