/* @vitest-environment jsdom */ import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import { createRef, useState } from 'react'; import { describe, expect, it, vi } from 'vitest'; import type { GenerateDialogState, SpecFormValues, UploadTarget, } from './ImageCanvasEditorTypes'; import { ImageCanvasSpecGenerationPanelView } from './ImageCanvasSpecGenerationPanelView'; function createSpecDialog( patch: Partial = {}, ): 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( , ); } function UiDesignHarness({ initialDialog, }: { initialDialog: GenerateDialogState; }) { const [dialog, setDialog] = useState( initialDialog, ); const referenceButtonRef = createRef(); return dialog ? ( node} buildPortalMenuStyle={() => ({ position: 'fixed', left: 0, top: 0 })} onUpdateSpecFormValue={vi.fn()} onRequestUpload={vi.fn()} onSubmit={vi.fn()} /> ) : null; } 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( , ); 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(prompt.getAttribute('placeholder')).toBe('你希望这个 UI 长什么样?'); expect(prompt.className).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('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('uses gameplay and art style hints for icon specs', () => { renderPanel({ dialog: createSpecDialog({ specType: 'ui', specValues: { playSetting: '', artStyle: '', bodyRatio: '3', characterView: '右向斜侧身站姿', customPrompt: '', }, }), }); expect((screen.getByLabelText('玩法设定') as HTMLInputElement).value).toBe( '', ); expect( (screen.getByLabelText('玩法设定') as HTMLInputElement).placeholder, ).toBe('这是什么类型的游戏?'); expect((screen.getByLabelText('美术风格') as HTMLInputElement).value).toBe( '', ); expect( (screen.getByLabelText('美术风格') as HTMLInputElement).placeholder, ).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( '自定义规范提示词', ) as HTMLTextAreaElement; expect(customPrompt.placeholder).toBe( '写下你想要的任何内容,陶泥儿帮你形成新的约束', ); fireEvent.change(screen.getByLabelText('自定义规范提示词'), { target: { value: '新的规范提示' }, }); 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( , ); fireEvent.click(screen.getByRole('button', { name: '提交生成规范' })); expect( (screen.getByLabelText('玩法设定') as HTMLInputElement).disabled, ).toBe(true); expect(submitSpec).not.toHaveBeenCalled(); rerender( , ); expect(screen.getByRole('alert').textContent).toContain('生成失败'); }); });