Files
Genarrative/src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx
T
menghao e4e3cab559
Project CI / Repository checks (push) Successful in 3m3s
Project CI / Frontend tests (push) Successful in 3m32s
Project CI / Backend tests (push) Successful in 5m45s
Project CI / Native shell tests (push) Successful in 16m17s
修复画布参考图上传删除链路 (#164)
主站规范生成支持多张参考图

修复普通生成参考图删除与提交状态不同步

优化触屏设备删除按钮可达性并补充回归测试

---------

Co-authored-by: kdletters <kdletters@qq.com>
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/164
Co-authored-by: 孟豪 <mh18530625731@163.com>
Co-committed-by: 孟豪 <mh18530625731@163.com>
2026-08-21 11:55:32 +08:00

789 lines
26 KiB
TypeScript

/* @vitest-environment jsdom */
import {
act,
cleanup,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import { createRef, useState } from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
getPlainTextEditorHost,
setPlainTextEditorValue,
} from '../common/AutoGrowTextArea.test-utils';
import type {
GenerateDialogState,
SpecFormValues,
UploadTarget,
} from './ImageCanvasEditorTypes';
import { ImageCanvasSpecGenerationPanelView } from './ImageCanvasSpecGenerationPanelView';
const iconSpecClientMocks = vi.hoisted(() => ({
refineGamePlay: vi.fn(),
refineArtStyle: vi.fn(),
}));
vi.mock('../../services/image-editor/editorProjectClient', () => ({
EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH: 200,
refineEditorIconSpecPlaySetting: iconSpecClientMocks.refineGamePlay,
refineEditorIconSpecArtStyle: iconSpecClientMocks.refineArtStyle,
}));
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
function createSpecDialog(
patch: Partial<GenerateDialogState> = {},
): GenerateDialogState {
return {
mode: 'spec',
prompt: '',
status: 'idle',
specType: 'character',
specValues: {
playSetting: 'RPG玩法',
artStyle: '像素风',
bodyRatio: '3',
characterView: '右向斜侧身站姿',
customPrompt: '',
},
...patch,
};
}
function renderPanel({
dialog,
onUpdateSpecFormValue = vi.fn(),
onRequestUpload = vi.fn(),
onSubmit = vi.fn(),
}: {
dialog: GenerateDialogState;
onUpdateSpecFormValue?: (key: keyof SpecFormValues, value: string) => void;
onRequestUpload?: (target: UploadTarget) => void;
onSubmit?: (dialog: GenerateDialogState) => void;
}) {
render(
<ImageCanvasSpecGenerationPanelView
dialog={dialog}
style={{ left: 10, top: 20 }}
onUpdateSpecFormValue={onUpdateSpecFormValue}
onRequestUpload={onRequestUpload}
onSubmit={onSubmit}
/>,
);
}
function UiDesignHarness({
initialDialog,
onRequestUpload = vi.fn(),
}: {
initialDialog: GenerateDialogState;
onRequestUpload?: (target: UploadTarget) => void;
}) {
const [dialog, setDialog] = useState<GenerateDialogState | null>(
initialDialog,
);
const referenceButtonRef = createRef<HTMLButtonElement>();
return dialog ? (
<ImageCanvasSpecGenerationPanelView
dialog={dialog}
style={{ left: 10, top: 20 }}
generationReferenceButtonRef={referenceButtonRef}
setGenerateDialog={setDialog}
renderEditorPortal={(node) => node}
buildPortalMenuStyle={() => ({ position: 'fixed', left: 0, top: 0 })}
onUpdateSpecFormValue={vi.fn()}
onRequestUpload={onRequestUpload}
onSubmit={vi.fn()}
/>
) : null;
}
function CharacterSpecReferenceMenuHarness({
onRequestUpload = vi.fn(),
}: {
onRequestUpload?: (target: UploadTarget) => void;
}) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const referenceButtonRef = createRef<HTMLButtonElement>();
return (
<ImageCanvasSpecGenerationPanelView
dialog={createSpecDialog({ generationReferences: [] })}
style={{ left: 10, top: 20 }}
generationReferenceButtonRef={referenceButtonRef}
isGenerationReferenceMenuOpen={isMenuOpen}
setIsGenerationReferenceMenuOpen={setIsMenuOpen}
setIsPickingGenerationReferenceFromCanvas={vi.fn()}
renderEditorPortal={(node) => node}
buildPortalMenuStyle={() => ({ position: 'fixed', left: 0, top: 0 })}
onUpdateSpecFormValue={vi.fn()}
onRequestUpload={onRequestUpload}
onSubmit={vi.fn()}
/>
);
}
function IconSpecHarness({
playSetting = '回合制占点',
artStyle = '低多边形',
onSubmit = vi.fn(),
}: {
playSetting?: string;
artStyle?: string;
onSubmit?: (dialog: GenerateDialogState) => void;
}) {
const [dialog, setDialog] = useState<GenerateDialogState>(
createSpecDialog({
specType: 'icon',
specValues: {
playSetting,
artStyle,
bodyRatio: '3',
characterView: '右向斜侧身站姿',
customPrompt: '',
},
}),
);
return (
<ImageCanvasSpecGenerationPanelView
dialog={dialog}
style={{ left: 10, top: 20 }}
onUpdateSpecFormValue={(key, value) =>
setDialog((current) => ({
...current,
specValues: {
...current.specValues!,
[key]: value,
},
}))
}
onRequestUpload={vi.fn()}
onSubmit={onSubmit}
/>
);
}
function createDeferred<T>() {
let resolve!: (value: T) => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
resolve = resolvePromise;
reject = rejectPromise;
});
return { promise, resolve, reject };
}
describe('ImageCanvasSpecGenerationPanelView', () => {
it('keeps the reference row above fields for spec panels and shows mud point text', () => {
renderPanel({ dialog: createSpecDialog() });
const panel = screen.getByRole('dialog', { name: '生成规范' });
expect(
panel.firstElementChild?.className.includes(
'image-canvas-editor__reference-strip',
),
).toBe(true);
const submitButton = screen.getByRole('button', { name: '提交生成规范' });
expect(submitButton.textContent).toBe('生成5泥点');
const dimensionsButton = screen.getByRole('button', {
name: '生成图片尺寸 16:9·2K',
}) as HTMLButtonElement;
const modelButton = screen.getByRole('button', {
name: '生成图片模型 gpt-image-2',
}) as HTMLButtonElement;
expect(dimensionsButton.disabled).toBe(true);
expect(modelButton.disabled).toBe(true);
expect(dimensionsButton.className).toContain(
'image-canvas-editor__option-cluster--dimensions',
);
expect(modelButton.className).toContain(
'image-canvas-editor__option-cluster--model',
);
fireEvent.click(dimensionsButton);
fireEvent.click(modelButton);
expect(screen.queryByRole('menu', { name: '生成图片尺寸选项' })).toBeNull();
expect(screen.queryByRole('menu', { name: '生成图片模型选项' })).toBeNull();
});
it('renders UI design as a single borderless prompt below the first reference row', () => {
render(
<UiDesignHarness
initialDialog={{
mode: 'ui-design',
prompt: '',
status: 'idle',
composerOpen: true,
uiDesignSpecReference: null,
imageModel: 'nanobanana2',
aspectRatio: '16:9',
imageSize: '1K',
}}
/>,
);
const panel = screen.getByRole('dialog', { name: '生成UI设计图' });
const prompt = screen.getByRole('textbox', { name: 'UI设计要求' });
expect(
panel.firstElementChild?.className.includes(
'image-canvas-editor__reference-strip',
),
).toBe(true);
expect(panel.textContent).not.toContain('UI设计要求');
expect(
getPlainTextEditorHost(prompt).querySelector(
'.auto-grow-text-area__placeholder',
)?.textContent,
).toBe('你希望这个 UI 长什么样?');
expect(getPlainTextEditorHost(prompt).className).toContain(
'auto-grow-text-area',
);
expect(prompt.className).not.toContain('platform-text-field');
expect(getPlainTextEditorHost(prompt).className).toContain(
'image-canvas-editor__generation-prompt',
);
expect(prompt.className).not.toContain(
'image-canvas-editor__generation-prompt--borderless',
);
const submitButton = screen.getByRole('button', { name: '生成UI设计图' });
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('uploads and removes UI design generation references', () => {
const requestUpload = vi.fn();
render(
<UiDesignHarness
onRequestUpload={requestUpload}
initialDialog={{
mode: 'ui-design',
prompt: '',
status: 'idle',
composerOpen: true,
uiDesignSpecReference: null,
generationReferences: [
{
id: 'ref-a',
label: '界面风格参考',
src: 'data:image/png;base64,ref',
},
],
imageModel: 'gpt-image-2',
aspectRatio: '16:9',
imageSize: '1K',
}}
/>,
);
expect(screen.getByLabelText('界面风格参考')).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '上传UI设计参考图' }));
expect(requestUpload).toHaveBeenCalledWith('generation-reference');
fireEvent.click(screen.getByRole('button', { name: '删除界面风格参考' }));
expect(screen.queryByLabelText('界面风格参考')).toBeNull();
});
it('removes only the selected character spec reference and keeps its action visible', () => {
render(
<UiDesignHarness
initialDialog={createSpecDialog({
generationReferences: [
{ id: 'ref-a', label: '参考一', src: 'data:image/png;base64,a' },
{ id: 'ref-b', label: '参考二', src: 'data:image/png;base64,b' },
{ id: 'ref-c', label: '参考三', src: 'data:image/png;base64,c' },
],
})}
/>,
);
expect(screen.getAllByRole('button', { name: /删除参考/ })).toHaveLength(3);
fireEvent.click(screen.getByRole('button', { name: '删除参考二' }));
expect(screen.queryByLabelText('参考二')).toBeNull();
expect(screen.getByLabelText('参考一')).toBeTruthy();
expect(screen.getByLabelText('参考三')).toBeTruthy();
});
it('shows and removes a legacy non-icon spec reference', () => {
render(
<UiDesignHarness
initialDialog={createSpecDialog({
generationReferences: [],
specReference: {
id: 'legacy-reference',
label: '历史角色参考',
src: 'data:image/png;base64,legacy',
},
})}
/>,
);
expect(screen.getByLabelText('历史角色参考')).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '删除历史角色参考' }));
expect(screen.queryByLabelText('历史角色参考')).toBeNull();
expect(screen.getByRole('button', { name: '参考图' })).toBeTruthy();
});
it('hides a stale legacy spec reference when canonical references exist', () => {
render(
<UiDesignHarness
initialDialog={createSpecDialog({
specReference: {
id: 'legacy-reference',
label: '过期角色参考',
src: 'data:image/png;base64,legacy',
},
generationReferences: [
{
id: 'canonical-reference',
label: '当前角色参考',
src: 'data:image/png;base64,current',
},
],
})}
/>,
);
expect(screen.getByLabelText('当前角色参考')).toBeTruthy();
expect(screen.queryByLabelText('过期角色参考')).toBeNull();
});
it('renders character spec fields and forwards updates', () => {
const updateSpecFormValue = vi.fn();
renderPanel({
dialog: createSpecDialog(),
onUpdateSpecFormValue: updateSpecFormValue,
});
expect(
(screen.getByLabelText('玩法设定') as HTMLInputElement).placeholder,
).toBe('这是什么类型的游戏?');
expect(
(screen.getByLabelText('美术风格') as HTMLInputElement).placeholder,
).toBe('游戏的画风是怎样的?');
fireEvent.change(screen.getByLabelText('玩法设定'), {
target: { value: '战棋玩法' },
});
fireEvent.change(screen.getByLabelText('美术风格'), {
target: { value: '水彩' },
});
fireEvent.change(screen.getByLabelText('头身比'), {
target: { value: '5' },
});
fireEvent.change(screen.getByLabelText('角色视角'), {
target: { value: '左向三分之二侧身站姿' },
});
expect(updateSpecFormValue).toHaveBeenCalledWith('playSetting', '战棋玩法');
expect(updateSpecFormValue).toHaveBeenCalledWith('artStyle', '水彩');
expect(updateSpecFormValue).toHaveBeenCalledWith('bodyRatio', '5');
expect(updateSpecFormValue).toHaveBeenCalledWith(
'characterView',
'左向三分之二侧身站姿',
);
});
it('keeps the character spec reference source menu open after clicking the add slot', () => {
render(<CharacterSpecReferenceMenuHarness />);
const addButton = screen.getByRole('button', { name: '参考图' });
fireEvent.click(addButton);
expect(screen.getByRole('menu', { name: '参考图来源' })).toBeTruthy();
fireEvent.click(addButton);
expect(screen.queryByRole('menu', { name: '参考图来源' })).toBeNull();
});
it('uses gameplay and art style hints for icon specs', () => {
renderPanel({
dialog: createSpecDialog({
specType: 'icon',
specValues: {
playSetting: '',
artStyle: '',
bodyRatio: '3',
characterView: '右向斜侧身站姿',
customPrompt: '',
},
}),
});
const playSetting = screen.getByLabelText('玩法设定');
const artStyle = screen.getByLabelText('美术风格');
expect(playSetting.textContent).toBe('');
expect(
getPlainTextEditorHost(playSetting).querySelector(
'.auto-grow-text-area__placeholder',
)?.textContent,
).toBe('游戏的核心玩法是什么?');
expect(artStyle.textContent).toBe('');
expect(
getPlainTextEditorHost(artStyle).querySelector(
'.auto-grow-text-area__placeholder',
)?.textContent,
).toBe('游戏的画风是怎样的?');
expect(
(
screen.getByRole('button', {
name: '一键优化玩法设定',
}) as HTMLButtonElement
).disabled,
).toBe(true);
expect(
(
screen.getByRole('button', {
name: '提交生成规范',
}) as HTMLButtonElement
).disabled,
).toBe(true);
});
it('enforces the icon spec prompt limit in the fields and submit state', () => {
const overLimit = '玩'.repeat(201);
const onSubmit = vi.fn();
render(<IconSpecHarness onSubmit={onSubmit} />);
const playSetting = screen.getByLabelText('玩法设定');
const artStyle = screen.getByLabelText('美术风格');
expect(screen.getByLabelText('玩法设定字符计数').textContent).toBe('5/200');
expect(screen.getByLabelText('美术风格字符计数').textContent).toBe('4/200');
expect(screen.getByLabelText('玩法设定字符计数').className).toContain(
'ml-auto',
);
expect(playSetting.hasAttribute('maxlength')).toBe(false);
expect(artStyle.hasAttribute('maxlength')).toBe(false);
setPlainTextEditorValue(playSetting, overLimit);
expect(Array.from(playSetting.textContent ?? '')).toHaveLength(200);
setPlainTextEditorValue(playSetting, '🎮'.repeat(201));
expect(Array.from(playSetting.textContent ?? '')).toHaveLength(200);
expect(screen.getByLabelText('玩法设定字符计数').textContent).toBe(
'200/200',
);
fireEvent.click(screen.getByRole('button', { name: '提交生成规范' }));
expect(onSubmit).toHaveBeenCalledTimes(1);
cleanup();
renderPanel({
dialog: createSpecDialog({
specType: 'icon',
specValues: {
playSetting: overLimit,
artStyle: '低多边形',
bodyRatio: '3',
characterView: '右向斜侧身站姿',
customPrompt: '',
},
}),
onSubmit,
});
expect(
(
screen.getByRole('button', {
name: '提交生成规范',
}) as HTMLButtonElement
).disabled,
).toBe(true);
expect(
(
screen.getByRole('button', {
name: '一键优化玩法设定',
}) as HTMLButtonElement
).disabled,
).toBe(true);
});
it('optimizes each icon spec field independently and disables generation until both finish', async () => {
const playSettingDeferred = createDeferred<string>();
const artStyleDeferred = createDeferred<string>();
iconSpecClientMocks.refineGamePlay.mockReturnValueOnce(
playSettingDeferred.promise,
);
iconSpecClientMocks.refineArtStyle.mockReturnValueOnce(
artStyleDeferred.promise,
);
render(<IconSpecHarness />);
fireEvent.click(screen.getByRole('button', { name: '一键优化玩法设定' }));
expect(
screen.getByLabelText('玩法设定').getAttribute('aria-disabled'),
).toBe('true');
expect(
screen.getByLabelText('美术风格').getAttribute('aria-disabled'),
).toBeNull();
expect(
screen.getByRole('button', { name: '一键优化玩法设定' }).textContent,
).toBe('正在优化');
fireEvent.click(screen.getByRole('button', { name: '一键优化美术风格' }));
expect(
(
screen.getByRole('button', {
name: '提交生成规范',
}) as HTMLButtonElement
).disabled,
).toBe(true);
await act(async () => playSettingDeferred.resolve('优化后的玩法'));
await waitFor(() =>
expect(screen.getByLabelText('玩法设定').textContent).toBe(
'优化后的玩法',
),
);
expect(
(
screen.getByRole('button', {
name: '提交生成规范',
}) as HTMLButtonElement
).disabled,
).toBe(true);
await act(async () => artStyleDeferred.resolve('优化后的画风'));
await waitFor(() =>
expect(screen.getByLabelText('美术风格').textContent).toBe(
'优化后的画风',
),
);
expect(
(
screen.getByRole('button', {
name: '提交生成规范',
}) as HTMLButtonElement
).disabled,
).toBe(false);
});
it('ignores an optimization result after the active dialog changes', async () => {
const deferred = createDeferred<string>();
const updateSpecFormValue = vi.fn();
iconSpecClientMocks.refineGamePlay.mockReturnValueOnce(deferred.promise);
const { rerender } = render(
<ImageCanvasSpecGenerationPanelView
dialog={createSpecDialog({ id: 'dialog-a', specType: 'icon' })}
style={{ left: 10, top: 20 }}
onUpdateSpecFormValue={updateSpecFormValue}
onRequestUpload={vi.fn()}
onSubmit={vi.fn()}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '一键优化玩法设定' }));
rerender(
<ImageCanvasSpecGenerationPanelView
dialog={createSpecDialog({ id: 'dialog-b', specType: 'icon' })}
style={{ left: 10, top: 20 }}
onUpdateSpecFormValue={updateSpecFormValue}
onRequestUpload={vi.fn()}
onSubmit={vi.fn()}
/>,
);
await act(async () => deferred.resolve('不应写入新对话框'));
expect(updateSpecFormValue).not.toHaveBeenCalled();
expect(screen.queryByRole('alert')).toBeNull();
});
it('keeps one undo snapshot per icon spec field and replaces it after a later successful optimization', async () => {
iconSpecClientMocks.refineGamePlay
.mockResolvedValueOnce('第一次优化')
.mockResolvedValueOnce('第二次优化');
render(<IconSpecHarness />);
fireEvent.click(screen.getByRole('button', { name: '一键优化玩法设定' }));
await waitFor(() =>
expect(screen.getByLabelText('玩法设定').textContent).toBe('第一次优化'),
);
fireEvent.click(screen.getByRole('button', { name: '撤销玩法设定优化' }));
expect(screen.getByLabelText('玩法设定').textContent).toBe('回合制占点');
expect(
screen.queryByRole('button', { name: '撤销玩法设定优化' }),
).toBeNull();
setPlainTextEditorValue(screen.getByLabelText('玩法设定'), '手动修改玩法');
fireEvent.click(screen.getByRole('button', { name: '一键优化玩法设定' }));
await waitFor(() =>
expect(screen.getByLabelText('玩法设定').textContent).toBe('第二次优化'),
);
fireEvent.click(screen.getByRole('button', { name: '撤销玩法设定优化' }));
expect(screen.getByLabelText('玩法设定').textContent).toBe('手动修改玩法');
});
it('restores the exact pre-optimization value including surrounding whitespace', async () => {
iconSpecClientMocks.refineGamePlay.mockResolvedValueOnce('优化后的玩法');
render(<IconSpecHarness playSetting=" 回合制占点 " />);
fireEvent.click(screen.getByRole('button', { name: '一键优化玩法设定' }));
await waitFor(() =>
expect(screen.getByLabelText('玩法设定').textContent).toBe(
'优化后的玩法',
),
);
expect(iconSpecClientMocks.refineGamePlay).toHaveBeenCalledWith(
'回合制占点',
);
fireEvent.click(screen.getByRole('button', { name: '撤销玩法设定优化' }));
expect(screen.getByLabelText('玩法设定').textContent).toBe(
' 回合制占点 ',
);
});
it('clears the field undo snapshot after a manual edit', async () => {
iconSpecClientMocks.refineGamePlay.mockResolvedValueOnce('成功优化');
render(<IconSpecHarness />);
fireEvent.click(screen.getByRole('button', { name: '一键优化玩法设定' }));
await waitFor(() =>
expect(screen.getByLabelText('玩法设定').textContent).toBe('成功优化'),
);
expect(
screen.getByRole('button', { name: '撤销玩法设定优化' }),
).toBeTruthy();
setPlainTextEditorValue(screen.getByLabelText('玩法设定'), '手动修改玩法');
expect(
screen.queryByRole('button', { name: '撤销玩法设定优化' }),
).toBeNull();
});
it('leaves text and the existing valid undo snapshot unchanged when optimization fails', async () => {
iconSpecClientMocks.refineGamePlay
.mockResolvedValueOnce('成功优化')
.mockRejectedValueOnce(new Error('服务暂不可用'));
render(<IconSpecHarness />);
fireEvent.click(screen.getByRole('button', { name: '一键优化玩法设定' }));
await waitFor(() =>
expect(screen.getByLabelText('玩法设定').textContent).toBe('成功优化'),
);
fireEvent.click(screen.getByRole('button', { name: '一键优化玩法设定' }));
await waitFor(() =>
expect(screen.getByRole('alert').textContent).toContain('服务暂不可用'),
);
expect(screen.getByLabelText('玩法设定').textContent).toBe('成功优化');
fireEvent.click(screen.getByRole('button', { name: '撤销玩法设定优化' }));
expect(screen.getByLabelText('玩法设定').textContent).toBe('回合制占点');
});
it('renders custom prompt and hides reference upload for icon specs', () => {
const updateSpecFormValue = vi.fn();
renderPanel({
dialog: createSpecDialog({
specType: 'custom',
specValues: {
playSetting: '',
artStyle: '',
bodyRatio: '3',
characterView: '',
customPrompt: '自定义提示',
},
}),
onUpdateSpecFormValue: updateSpecFormValue,
});
const customPrompt = screen.getByLabelText('自定义规范提示词');
expect(customPrompt.textContent).toBe('自定义提示');
expect(getPlainTextEditorHost(customPrompt).className).toContain(
'auto-grow-text-area',
);
expect(customPrompt.className).not.toContain('platform-text-field');
expect(getPlainTextEditorHost(customPrompt).className).toContain(
'image-canvas-editor__generation-prompt',
);
setPlainTextEditorValue(customPrompt, '新的规范提示');
expect(updateSpecFormValue).toHaveBeenCalledWith(
'customPrompt',
'新的规范提示',
);
cleanup();
renderPanel({ dialog: createSpecDialog({ specType: 'icon' }) });
expect(screen.getByRole('button', { name: '参考图' })).toBeTruthy();
});
it('requests reference upload and submits while idle', () => {
const requestUpload = vi.fn();
const submitSpec = vi.fn();
const dialog = createSpecDialog();
renderPanel({
dialog,
onRequestUpload: requestUpload,
onSubmit: submitSpec,
});
fireEvent.click(screen.getByRole('button', { name: '参考图' }));
fireEvent.click(screen.getByRole('button', { name: '提交生成规范' }));
expect(requestUpload).toHaveBeenCalledWith('spec-reference');
expect(submitSpec).toHaveBeenCalledWith(dialog);
});
it('renders compact spec submit cost with mud point text', () => {
renderPanel({ dialog: createSpecDialog() });
const submitButton = screen.getByRole('button', { name: '提交生成规范' });
expect(submitButton.textContent).toBe('生成5泥点');
});
it('disables controls while generating and renders failure state', () => {
const submitSpec = vi.fn();
const { rerender } = render(
<ImageCanvasSpecGenerationPanelView
dialog={createSpecDialog({ status: 'generating' })}
style={{ left: 10, top: 20 }}
onUpdateSpecFormValue={vi.fn()}
onRequestUpload={vi.fn()}
onSubmit={submitSpec}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '提交生成规范' }));
expect(
(screen.getByLabelText('玩法设定') as HTMLInputElement).disabled,
).toBe(true);
expect(submitSpec).not.toHaveBeenCalled();
rerender(
<ImageCanvasSpecGenerationPanelView
dialog={createSpecDialog({
status: 'failed',
errorMessage: '生成失败',
})}
style={{ left: 10, top: 20 }}
onUpdateSpecFormValue={vi.fn()}
onRequestUpload={vi.fn()}
onSubmit={submitSpec}
/>,
);
expect(screen.getByRole('alert').textContent).toContain('生成失败');
});
});