/* @vitest-environment jsdom */ import { fireEvent, render, screen } from '@testing-library/react'; import { useState } from 'react'; import { describe, expect, it, vi } from 'vitest'; import { setPlainTextEditorValue } from '../common/AutoGrowTextArea.test-utils'; import { ImageCanvasCharacterAnimationPanelView } from './ImageCanvasCharacterAnimationPanelView'; import type { CanvasLayer, CharacterAnimationPanelState, } from './ImageCanvasEditorTypes'; function createSourceLayer(): CanvasLayer { return { id: 'layer-a', resourceId: 'resource-a', title: '角色源图', src: 'data:image/png;base64,Y2hhcmFjdGVy', x: 0, y: 0, width: 128, height: 128, originalWidth: 128, originalHeight: 128, zIndex: 1, sourceType: 'uploaded', }; } function createPanel( patch: Partial = {}, ): CharacterAnimationPanelState { return { sourceLayerId: 'layer-a', promptText: '待机动作', resolution: '480p', ratio: 'same', frameCount: 32, durationSeconds: 4, status: 'idle', ...patch, }; } function CharacterAnimationPanelHarness({ initialPanel, onSubmit = vi.fn(), onUpdateDuration, }: { initialPanel: CharacterAnimationPanelState; onSubmit?: () => void; onUpdateDuration?: (frameCountValue: string) => void; }) { const [panel, setPanel] = useState( initialPanel, ); const updateDuration = onUpdateDuration ?? ((frameCountValue: string) => { const frameCount = Number(frameCountValue); setPanel((currentPanel) => currentPanel ? { ...currentPanel, frameCount: frameCount === 48 ? 48 : frameCount === 40 ? 40 : 32, durationSeconds: frameCount === 48 ? 6 : frameCount === 40 ? 5 : 4, status: currentPanel.status === 'failed' ? 'idle' : currentPanel.status, errorMessage: currentPanel.status === 'failed' ? undefined : currentPanel.errorMessage, } : currentPanel, ); }); return panel ? (
{panel.promptText} {panel.resolution} {panel.ratio} {panel.frameCount} {panel.durationSeconds} {panel.status} {panel.errorMessage ?? '-'}
) : ( closed ); } describe('ImageCanvasCharacterAnimationPanelView', () => { it('keeps source reference first and updates prompt, resolution and ratio from a menu while clearing failed state', () => { render( , ); const description = screen.getByLabelText('动画描述'); expect(description.closest('.auto-grow-text-area')?.className).toContain( 'auto-grow-text-area', ); expect(description.className).not.toContain('platform-text-field'); setPlainTextEditorValue(description, 'a'.repeat(4001)); fireEvent.click( screen.getByRole('button', { name: '动画参数 同图尺寸 · 4秒 · 480p' }), ); const menu = screen.getByRole('menu', { name: '动画参数选项' }); fireEvent.click(screen.getByRole('button', { name: '清晰度 720p' })); fireEvent.click(screen.getByRole('button', { name: '比例 16:9' })); expect(screen.getByRole('menu', { name: '动画参数选项' })).toBeTruthy(); expect(screen.getByLabelText('当前动画描述').textContent).toHaveLength( 4000, ); expect(screen.getByLabelText('当前分辨率').textContent).toBe('720p'); expect(screen.getByLabelText('当前比例').textContent).toBe('16:9'); expect(menu).toBeTruthy(); expect( menu .closest('.image-canvas-editor__portal-theme') ?.classList.contains('platform-theme--light'), ).toBe(true); expect(screen.getByLabelText('当前状态').textContent).toBe('idle'); expect(screen.getByLabelText('当前错误').textContent).toBe('-'); }); it('clicks the parent animation panel to collapse the parameter menu', () => { render(); fireEvent.click( screen.getByRole('button', { name: '动画参数 同图尺寸 · 4秒 · 480p' }), ); expect(screen.getByRole('menu', { name: '动画参数选项' })).toBeTruthy(); fireEvent.click(screen.getByRole('textbox', { name: '动画描述' })); expect(screen.queryByRole('menu', { name: '动画参数选项' })).toBeNull(); }); it('applies preset prompt and duration updates', () => { const updateDuration = vi.fn(); render( , ); fireEvent.click(screen.getByRole('button', { name: '行走' })); fireEvent.click( screen.getByRole('button', { name: '动画参数 同图尺寸 · 4秒 · 480p' }), ); fireEvent.click(screen.getByRole('button', { name: '时长 48帧·6秒' })); expect(screen.getByLabelText('当前动画描述').textContent).toBe( '循环行走动作,步伐稳定。', ); expect(screen.getByLabelText('当前状态').textContent).toBe('idle'); expect(screen.getByLabelText('当前错误').textContent).toBe('-'); expect(updateDuration).toHaveBeenCalledWith('48'); }); it('submits only when idle and closes through its interface', () => { const submitCharacterAnimation = vi.fn(); render( , ); fireEvent.click(screen.getByRole('button', { name: '生成18泥点' })); fireEvent.click( screen.getByRole('button', { name: '关闭角色动画生成面板' }), ); expect(submitCharacterAnimation).toHaveBeenCalledTimes(1); expect(screen.getByLabelText('面板状态').textContent).toBe('closed'); }); it('keeps generating panels disabled and does not submit', () => { const submitCharacterAnimation = vi.fn(); render( , ); fireEvent.click(screen.getByRole('button', { name: '生成中' })); const description = screen.getByLabelText('动画描述'); expect(description.getAttribute('aria-disabled')).toBe('true'); expect( description .closest('.auto-grow-text-area') ?.getAttribute('data-disabled'), ).toBe('true'); expect( (screen.getByRole('button', { name: '待机' }) as HTMLButtonElement) .disabled, ).toBe(true); expect(submitCharacterAnimation).not.toHaveBeenCalled(); }); it('renders completed frame count and failed error state', () => { const result = { taskId: 'task-a', model: 'seedance2.0-fast' as const, prompt: '行走动作', previewVideoPath: '/preview.mp4', frames: [], frameCount: 40, durationSeconds: 5, fps: 8, priceMudPoints: 18, }; const { rerender } = render( , ); expect(screen.getByText('已生成 40 帧')).toBeTruthy(); rerender( , ); expect(screen.getByRole('alert').textContent).toContain('生成失败'); }); });