- web/: React + Vite + TypeScript 前端 - backend/: Django + DRF + SimpleJWT 后端 - prototype/: HTML 设计原型 - docs/: PRD 和设计评审文档 - test: 单元测试 + E2E 极限测试
219 lines
7.6 KiB
TypeScript
219 lines
7.6 KiB
TypeScript
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
|
|
// Stub localStorage before auth store initialization (imported transitively by generation store)
|
|
vi.stubGlobal('localStorage', {
|
|
getItem: vi.fn(() => null),
|
|
setItem: vi.fn(),
|
|
removeItem: vi.fn(),
|
|
clear: vi.fn(),
|
|
length: 0,
|
|
key: vi.fn(() => null),
|
|
});
|
|
|
|
// Mock auth API to prevent network calls from auth store
|
|
vi.mock('../../src/lib/api', () => ({
|
|
authApi: {
|
|
login: vi.fn(),
|
|
register: vi.fn(),
|
|
refreshToken: vi.fn(),
|
|
getMe: vi.fn(),
|
|
},
|
|
videoApi: { generate: vi.fn().mockResolvedValue({ data: { remaining_quota: 45 } }) },
|
|
adminApi: { getStats: vi.fn(), getUserRankings: vi.fn(), updateUserQuota: vi.fn() },
|
|
default: { interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() } } },
|
|
}));
|
|
|
|
vi.mock('../../src/components/Toast', () => ({
|
|
showToast: vi.fn(),
|
|
}));
|
|
|
|
import { useGenerationStore } from '../../src/store/generation';
|
|
import { useInputBarStore } from '../../src/store/inputBar';
|
|
|
|
function createMockFile(name: string, type: string): File {
|
|
return new File(['mock'], name, { type });
|
|
}
|
|
|
|
describe('Generation Store', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
useInputBarStore.getState().reset();
|
|
useGenerationStore.setState({ tasks: [] });
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
describe('addTask', () => {
|
|
it('should return null when canSubmit is false', () => {
|
|
const result = useGenerationStore.getState().addTask();
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should create a task when prompt has text', () => {
|
|
useInputBarStore.getState().setPrompt('test prompt');
|
|
const id = useGenerationStore.getState().addTask();
|
|
expect(id).not.toBeNull();
|
|
expect(useGenerationStore.getState().tasks).toHaveLength(1);
|
|
});
|
|
|
|
it('should create task with correct properties', () => {
|
|
useInputBarStore.getState().setPrompt('a beautiful scene');
|
|
useInputBarStore.getState().setModel('seedance_2.0_fast');
|
|
useInputBarStore.getState().setAspectRatio('16:9');
|
|
useInputBarStore.getState().setDuration(10);
|
|
|
|
useGenerationStore.getState().addTask();
|
|
const task = useGenerationStore.getState().tasks[0];
|
|
|
|
expect(task.prompt).toBe('a beautiful scene');
|
|
expect(task.model).toBe('seedance_2.0_fast');
|
|
expect(task.aspectRatio).toBe('16:9');
|
|
expect(task.duration).toBe(10);
|
|
expect(task.mode).toBe('universal');
|
|
expect(task.status).toBe('generating');
|
|
expect(task.progress).toBe(0);
|
|
});
|
|
|
|
it('should snapshot references in universal mode', () => {
|
|
useInputBarStore.getState().addReferences([
|
|
createMockFile('img1.jpg', 'image/jpeg'),
|
|
createMockFile('img2.jpg', 'image/jpeg'),
|
|
]);
|
|
|
|
useGenerationStore.getState().addTask();
|
|
const task = useGenerationStore.getState().tasks[0];
|
|
|
|
expect(task.references).toHaveLength(2);
|
|
expect(task.references[0].type).toBe('image');
|
|
});
|
|
|
|
it('should snapshot frames in keyframe mode', () => {
|
|
useInputBarStore.getState().switchMode('keyframe');
|
|
useInputBarStore.getState().setFirstFrame(createMockFile('first.jpg', 'image/jpeg'));
|
|
useInputBarStore.getState().setLastFrame(createMockFile('last.jpg', 'image/jpeg'));
|
|
|
|
useGenerationStore.getState().addTask();
|
|
const task = useGenerationStore.getState().tasks[0];
|
|
|
|
expect(task.references).toHaveLength(2);
|
|
expect(task.references[0].label).toBe('首帧');
|
|
expect(task.references[1].label).toBe('尾帧');
|
|
});
|
|
|
|
it('should clear input after submit', () => {
|
|
useInputBarStore.getState().setPrompt('test');
|
|
useInputBarStore.getState().addReferences([createMockFile('img.jpg', 'image/jpeg')]);
|
|
|
|
useGenerationStore.getState().addTask();
|
|
|
|
expect(useInputBarStore.getState().prompt).toBe('');
|
|
expect(useInputBarStore.getState().references).toHaveLength(0);
|
|
});
|
|
|
|
it('should prepend new tasks (newest first)', () => {
|
|
useInputBarStore.getState().setPrompt('first');
|
|
useGenerationStore.getState().addTask();
|
|
|
|
useInputBarStore.getState().setPrompt('second');
|
|
useGenerationStore.getState().addTask();
|
|
|
|
const tasks = useGenerationStore.getState().tasks;
|
|
expect(tasks).toHaveLength(2);
|
|
expect(tasks[0].prompt).toBe('second');
|
|
expect(tasks[1].prompt).toBe('first');
|
|
});
|
|
|
|
it('should simulate progress over time', () => {
|
|
useInputBarStore.getState().setPrompt('test');
|
|
useGenerationStore.getState().addTask();
|
|
|
|
// Advance timers to trigger progress
|
|
vi.advanceTimersByTime(2000);
|
|
|
|
const task = useGenerationStore.getState().tasks[0];
|
|
expect(task.progress).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('removeTask', () => {
|
|
it('should remove a task by id', () => {
|
|
useInputBarStore.getState().setPrompt('test');
|
|
const id = useGenerationStore.getState().addTask()!;
|
|
expect(useGenerationStore.getState().tasks).toHaveLength(1);
|
|
|
|
useGenerationStore.getState().removeTask(id);
|
|
expect(useGenerationStore.getState().tasks).toHaveLength(0);
|
|
});
|
|
|
|
it('should not affect other tasks', () => {
|
|
useInputBarStore.getState().setPrompt('first');
|
|
const id1 = useGenerationStore.getState().addTask()!;
|
|
|
|
useInputBarStore.getState().setPrompt('second');
|
|
useGenerationStore.getState().addTask();
|
|
|
|
useGenerationStore.getState().removeTask(id1);
|
|
expect(useGenerationStore.getState().tasks).toHaveLength(1);
|
|
expect(useGenerationStore.getState().tasks[0].prompt).toBe('second');
|
|
});
|
|
});
|
|
|
|
describe('reEdit', () => {
|
|
it('should restore prompt from task', () => {
|
|
useInputBarStore.getState().setPrompt('original prompt');
|
|
const id = useGenerationStore.getState().addTask()!;
|
|
|
|
// Input is cleared after submit
|
|
expect(useInputBarStore.getState().prompt).toBe('');
|
|
|
|
useGenerationStore.getState().reEdit(id);
|
|
expect(useInputBarStore.getState().prompt).toBe('original prompt');
|
|
});
|
|
|
|
it('should restore settings from task', () => {
|
|
useInputBarStore.getState().setPrompt('test');
|
|
useInputBarStore.getState().setAspectRatio('16:9');
|
|
useInputBarStore.getState().setDuration(10);
|
|
const id = useGenerationStore.getState().addTask()!;
|
|
|
|
// Reset to defaults
|
|
useInputBarStore.getState().setAspectRatio('21:9');
|
|
useInputBarStore.getState().setDuration(15);
|
|
|
|
useGenerationStore.getState().reEdit(id);
|
|
expect(useInputBarStore.getState().aspectRatio).toBe('16:9');
|
|
expect(useInputBarStore.getState().duration).toBe(10);
|
|
});
|
|
|
|
it('should do nothing for non-existent task', () => {
|
|
useInputBarStore.getState().setPrompt('existing');
|
|
useGenerationStore.getState().reEdit('non_existent_id');
|
|
expect(useInputBarStore.getState().prompt).toBe('existing');
|
|
});
|
|
});
|
|
|
|
describe('regenerate', () => {
|
|
it('should create a new task based on existing one', () => {
|
|
useInputBarStore.getState().setPrompt('test');
|
|
useGenerationStore.getState().addTask();
|
|
|
|
const originalId = useGenerationStore.getState().tasks[0].id;
|
|
useGenerationStore.getState().regenerate(originalId);
|
|
|
|
expect(useGenerationStore.getState().tasks).toHaveLength(2);
|
|
const newTask = useGenerationStore.getState().tasks[0]; // newest first
|
|
expect(newTask.id).not.toBe(originalId);
|
|
expect(newTask.prompt).toBe('test');
|
|
expect(newTask.status).toBe('generating');
|
|
expect(newTask.progress).toBe(0);
|
|
});
|
|
|
|
it('should do nothing for non-existent task', () => {
|
|
useGenerationStore.getState().regenerate('non_existent');
|
|
expect(useGenerationStore.getState().tasks).toHaveLength(0);
|
|
});
|
|
});
|
|
});
|