e4e3cab559
主站规范生成支持多张参考图 修复普通生成参考图删除与提交状态不同步 优化触屏设备删除按钮可达性并补充回归测试 --------- 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>
789 lines
26 KiB
TypeScript
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('生成失败');
|
|
});
|
|
});
|