video-shuoshan/web/test/unit/components.test.tsx
zyc ffe92f7b15 Initial commit: 即梦视频生成平台
- web/: React + Vite + TypeScript 前端
- backend/: Django + DRF + SimpleJWT 后端
- prototype/: HTML 设计原型
- docs/: PRD 和设计评审文档
- test: 单元测试 + E2E 极限测试
2026-03-13 09:59:33 +08:00

224 lines
8.0 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { render, screen, fireEvent, act } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { useInputBarStore } from '../../src/store/inputBar';
// Mock the auth store to prevent localStorage access issues at module load
vi.mock('../../src/store/auth', () => ({
useAuthStore: Object.assign(
(selector: (s: any) => any) => selector({
user: null,
isAuthenticated: false,
isLoading: false,
quota: null,
logout: vi.fn(),
initialize: vi.fn(),
}),
{ getState: () => ({ initialize: vi.fn(), logout: vi.fn() }) }
),
}));
// We need to test that key components render and integrate correctly.
// Since CSS Modules are not fully resolved in jsdom, we focus on DOM structure and logic.
describe('PromptInput Component', () => {
beforeEach(() => {
useInputBarStore.getState().reset();
});
it('should render textarea with universal placeholder', async () => {
const { PromptInput } = await import('../../src/components/PromptInput');
render(<PromptInput />);
const textarea = screen.getByPlaceholderText(/上传1-5张参考图/);
expect(textarea).toBeInTheDocument();
});
it('should render textarea with keyframe placeholder after mode switch', async () => {
useInputBarStore.getState().switchMode('keyframe');
const { PromptInput } = await import('../../src/components/PromptInput');
render(<PromptInput />);
const textarea = screen.getByPlaceholderText(/输入描述,定义首帧到尾帧/);
expect(textarea).toBeInTheDocument();
});
it('should update store on input', async () => {
const { PromptInput } = await import('../../src/components/PromptInput');
render(<PromptInput />);
const textarea = screen.getByRole('textbox');
fireEvent.change(textarea, { target: { value: 'test prompt' } });
expect(useInputBarStore.getState().prompt).toBe('test prompt');
});
});
describe('UniversalUpload Component', () => {
beforeEach(() => {
useInputBarStore.getState().reset();
});
it('should render trigger button when no files', async () => {
const { UniversalUpload } = await import('../../src/components/UniversalUpload');
render(<UniversalUpload />);
expect(screen.getByText('参考内容')).toBeInTheDocument();
});
it('should render thumbnails when files are added', async () => {
const file = new File(['mock'], 'test.jpg', { type: 'image/jpeg' });
useInputBarStore.getState().addReferences([file]);
const { UniversalUpload } = await import('../../src/components/UniversalUpload');
render(<UniversalUpload />);
// Should not show the trigger
expect(screen.queryByText('参考内容')).not.toBeInTheDocument();
});
it('should have hidden file input with correct accept', async () => {
const { UniversalUpload } = await import('../../src/components/UniversalUpload');
const { container } = render(<UniversalUpload />);
const input = container.querySelector('input[type="file"]') as HTMLInputElement;
expect(input).toBeTruthy();
expect(input.accept).toBe('image/*,video/*');
expect(input.multiple).toBe(true);
});
});
describe('KeyframeUpload Component', () => {
beforeEach(() => {
useInputBarStore.getState().reset();
useInputBarStore.getState().switchMode('keyframe');
});
it('should render first and last frame triggers', async () => {
const { KeyframeUpload } = await import('../../src/components/KeyframeUpload');
render(<KeyframeUpload />);
expect(screen.getByText('首帧')).toBeInTheDocument();
expect(screen.getByText('尾帧')).toBeInTheDocument();
});
it('should have file inputs accepting only images', async () => {
const { KeyframeUpload } = await import('../../src/components/KeyframeUpload');
const { container } = render(<KeyframeUpload />);
const inputs = container.querySelectorAll('input[type="file"]');
expect(inputs).toHaveLength(2);
inputs.forEach((input) => {
expect((input as HTMLInputElement).accept).toBe('image/*');
});
});
});
describe('Dropdown Component', () => {
it('should render trigger and not show menu initially', async () => {
const { Dropdown } = await import('../../src/components/Dropdown');
render(
<Dropdown
items={[
{ label: 'Option A', value: 'a' },
{ label: 'Option B', value: 'b' },
]}
value="a"
onSelect={vi.fn()}
trigger={<button>Click me</button>}
/>
);
expect(screen.getByText('Click me')).toBeInTheDocument();
expect(screen.getByText('Option A')).toBeInTheDocument();
expect(screen.getByText('Option B')).toBeInTheDocument();
});
it('should call onSelect when item clicked', async () => {
const onSelect = vi.fn();
const { Dropdown } = await import('../../src/components/Dropdown');
render(
<Dropdown
items={[
{ label: 'Option A', value: 'a' },
{ label: 'Option B', value: 'b' },
]}
value="a"
onSelect={onSelect}
trigger={<button>Click me</button>}
/>
);
// Open dropdown
fireEvent.click(screen.getByText('Click me'));
// Select option B
fireEvent.click(screen.getByText('Option B'));
expect(onSelect).toHaveBeenCalledWith('b');
});
});
describe('Toast Component', () => {
it('should render and be controllable via showToast', async () => {
const { Toast, showToast } = await import('../../src/components/Toast');
render(<Toast />);
act(() => {
showToast('Test message');
});
expect(screen.getByText('Test message')).toBeInTheDocument();
});
});
describe('Toolbar Component', () => {
beforeEach(() => {
useInputBarStore.getState().reset();
});
it('should render all toolbar buttons in universal mode', async () => {
const { Toolbar } = await import('../../src/components/Toolbar');
render(<Toolbar />);
// These texts may appear in both trigger and dropdown items
expect(screen.getAllByText('视频生成').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('Seedance 2.0').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('全能参考').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('21:9').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('15s').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('@')).toBeInTheDocument();
});
it('should show auto-match and hide @ button in keyframe mode', async () => {
useInputBarStore.getState().switchMode('keyframe');
const { Toolbar } = await import('../../src/components/Toolbar');
render(<Toolbar />);
expect(screen.getByText('自动匹配')).toBeInTheDocument();
expect(screen.getAllByText('首尾帧').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('5s').length).toBeGreaterThanOrEqual(1);
expect(screen.queryByText('@')).not.toBeInTheDocument();
});
it('should show credits section with +30', async () => {
const { Toolbar } = await import('../../src/components/Toolbar');
render(<Toolbar />);
expect(screen.getByText('30')).toBeInTheDocument();
});
});
describe('VideoGenerationPage Component', () => {
beforeEach(() => {
useInputBarStore.getState().reset();
});
it('should render the page with hint text and input bar', async () => {
const { VideoGenerationPage } = await import('../../src/components/VideoGenerationPage');
render(
<MemoryRouter>
<VideoGenerationPage />
</MemoryRouter>
);
expect(screen.getByText('在下方输入提示词,开始创作 AI 视频')).toBeInTheDocument();
});
});
describe('Sidebar Component', () => {
it('should render navigation items', async () => {
const { Sidebar } = await import('../../src/components/Sidebar');
render(<Sidebar />);
expect(screen.getByText('灵感')).toBeInTheDocument();
expect(screen.getByText('生成')).toBeInTheDocument();
expect(screen.getByText('资产')).toBeInTheDocument();
expect(screen.getByText('画布')).toBeInTheDocument();
expect(screen.getByText('API')).toBeInTheDocument();
});
});