/* @vitest-environment jsdom */ import { fireEvent, render, screen } from '@testing-library/react'; import { createRef, useState } from 'react'; import { describe, expect, it, vi } from 'vitest'; import type { GenerateDialogState, SpecGenerationType, UploadTarget, } from './ImageCanvasEditorTypes'; import { ImageCanvasCharacterGenerationComposerView } from './ImageCanvasCharacterGenerationComposerView'; import { IMAGE_MODEL_GPT_IMAGE_2 } from './ImageCanvasGenerationModel'; function createDialog( patch: Partial = {}, ): GenerateDialogState { return { mode: 'character', prompt: '旧角色设定', status: 'idle', characterSpecReference: { id: 'spec-a', label: '角色规范A', src: 'data:image/png;base64,c3BlYw==', }, characterReferences: [ { id: 'ref-a', label: '参考图A', src: 'data:image/png;base64,cmVm', }, ], ...patch, }; } function CharacterGenerationHarness({ initialDialog = createDialog(), onOpenSpecDialog = vi.fn(), onRequestUpload = vi.fn(), onSubmit = vi.fn(), onRememberImageModel = vi.fn(), }: { initialDialog?: GenerateDialogState; onOpenSpecDialog?: (specType: SpecGenerationType) => void; onRequestUpload?: (target: UploadTarget) => void; onSubmit?: (dialog: GenerateDialogState) => void; onRememberImageModel?: (model: string) => void; }) { const [dialog, setDialog] = useState( initialDialog, ); const [isCharacterSpecMenuOpen, setIsCharacterSpecMenuOpen] = useState(false); const [ isCharacterReferenceMenuOpen, setIsCharacterReferenceMenuOpen, ] = useState(false); const [isPickingSpec, setIsPickingSpec] = useState(false); const [isPickingReference, setIsPickingReference] = useState(false); const characterSpecButtonRef = createRef(); const characterReferenceButtonRef = createRef(); return dialog ? (
node} buildPortalMenuStyle={() => ({ position: 'fixed', left: 0, top: 0 })} onOpenSpecDialog={onOpenSpecDialog} onRequestUpload={onRequestUpload} onRememberImageModel={onRememberImageModel} onSubmit={onSubmit} /> {dialog.prompt} {dialog.status} {dialog.errorMessage ?? '-'} {String(isPickingSpec)} {String(isPickingReference)}
) : null; } describe('ImageCanvasCharacterGenerationComposerView', () => { it('keeps the prompt as a borderless single text input with a question placeholder', () => { render(); const panel = screen.getByRole('dialog', { name: '生成角色形象' }); const prompt = screen.getByRole('textbox', { name: '角色设定' }); expect( Array.from(panel.querySelectorAll('.image-canvas-editor__field-title')) .map((node) => node.textContent) .join(''), ).not.toContain('角色设定'); expect(prompt.getAttribute('placeholder')).toBe('你希望角色如何设计?'); expect(prompt.className).toContain( 'image-canvas-editor__generation-prompt--borderless', ); }); it('updates character prompt, clears failed state and submits', () => { const submitCharacter = vi.fn(); render( , ); fireEvent.change(screen.getByLabelText('角色设定'), { target: { value: '新的角色设定' }, }); fireEvent.click(screen.getByRole('button', { name: '生成' })); expect(screen.getByLabelText('当前角色设定').textContent).toBe( '新的角色设定', ); expect(screen.getByLabelText('当前状态').textContent).toBe('idle'); expect(screen.getByLabelText('当前错误').textContent).toBe('-'); expect(submitCharacter).toHaveBeenCalledWith( expect.objectContaining({ prompt: '新的角色设定', status: 'idle' }), ); }); it('updates the submit price when switching the character image model', () => { const rememberImageModel = vi.fn(); render( , ); expect(screen.getByRole('button', { name: '生成' }).textContent).toBe( '生成12泥点', ); fireEvent.click( screen.getByRole('button', { name: '生成图片模型 nanobanana2' }), ); fireEvent.click(screen.getByRole('button', { name: 'gpt-image-2' })); expect(rememberImageModel).toHaveBeenCalledWith(IMAGE_MODEL_GPT_IMAGE_2); expect(screen.getByRole('button', { name: '生成' }).textContent).toBe( '生成3泥点', ); }); it('routes character reference menus to canvas picking, spec creation and upload', () => { const openSpecDialog = vi.fn(); const requestUpload = vi.fn(); render( , ); fireEvent.click(screen.getByRole('button', { name: '角色规范A' })); fireEvent.click(screen.getByRole('menuitem', { name: '从画布中选择' })); expect(screen.getByLabelText('选择角色规范').textContent).toBe('true'); fireEvent.click(screen.getByRole('button', { name: '角色规范A' })); fireEvent.click(screen.getByRole('menuitem', { name: '新建角色规范' })); expect(openSpecDialog).toHaveBeenCalledWith('character'); fireEvent.click(screen.getByRole('button', { name: '角色规范A' })); fireEvent.click(screen.getByRole('menuitem', { name: '上传图片' })); expect(requestUpload).toHaveBeenCalledWith('character-spec'); fireEvent.click(screen.getByRole('button', { name: '上传常规参考图' })); fireEvent.click(screen.getByRole('menuitem', { name: '从画布中选择' })); expect(screen.getByLabelText('选择常规参考图').textContent).toBe('true'); }); it('removes bound character spec and regular references from their delete buttons', () => { render(); fireEvent.click(screen.getByRole('button', { name: '删除角色规范' })); expect(screen.getByRole('button', { name: '角色规范' })).toBeTruthy(); expect(screen.queryByRole('button', { name: '角色规范A' })).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '删除参考图A' })); expect(screen.queryByText('参考图A')).toBeNull(); }); it('clicks the parent character panel to collapse reference menus', () => { render( , ); fireEvent.click(screen.getByRole('button', { name: '角色规范A' })); expect(screen.getByRole('menu', { name: '角色规范来源' })).toBeTruthy(); fireEvent.click(screen.getByRole('textbox', { name: '角色设定' })); expect( screen.queryByRole('menu', { name: '角色规范来源' }), ).toBeNull(); }); });