- web/: React + Vite + TypeScript 前端 - backend/: Django + DRF + SimpleJWT 后端 - prototype/: HTML 设计原型 - docs/: PRD 和设计评审文档 - test: 单元测试 + E2E 极限测试
224 lines
8.0 KiB
TypeScript
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();
|
|
});
|
|
});
|