video-shuoshan/web/test/unit/generationStore.test.ts
zyc 566c3a476f
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m17s
add 存储桶
2026-03-13 15:38:08 +08:00

372 lines
12 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),
});
const mockGenerate = vi.fn().mockResolvedValue({
data: {
task_id: 'test-uuid-001',
ark_task_id: '',
status: 'queued',
estimated_time: 120,
seconds_consumed: 5,
remaining_seconds_today: 595,
},
});
const mockUpload = vi.fn().mockResolvedValue({
data: { url: 'https://tos.example.com/image/test.jpg', type: 'image', filename: 'test.jpg', size: 1234 },
});
const mockGetTasks = vi.fn().mockResolvedValue({ data: { results: [] } });
const mockGetTaskStatus = vi.fn().mockResolvedValue({
data: { task_id: 'test-uuid-001', status: 'queued', result_url: '', error_message: '', reference_urls: [] },
});
// 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: (...args: unknown[]) => mockGenerate(...args),
getTasks: (...args: unknown[]) => mockGetTasks(...args),
getTaskStatus: (...args: unknown[]) => mockGetTaskStatus(...args),
},
mediaApi: {
upload: (...args: unknown[]) => mockUpload(...args),
},
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(),
}));
// Mock fetchUserInfo to prevent network calls
vi.mock('../../src/store/auth', async (importOriginal) => {
const original = await importOriginal() as Record<string, unknown>;
return {
...original,
useAuthStore: Object.assign(
(selector: (s: Record<string, unknown>) => unknown) =>
selector({ fetchUserInfo: vi.fn() }),
{ getState: () => ({ fetchUserInfo: 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: [], isLoading: false });
mockGenerate.mockClear();
mockUpload.mockClear();
mockGetTasks.mockClear();
mockGetTaskStatus.mockClear();
// Reset generate mock to return unique IDs
let callCount = 0;
mockGenerate.mockImplementation(() => {
callCount++;
return Promise.resolve({
data: {
task_id: `test-uuid-${String(callCount).padStart(3, '0')}`,
ark_task_id: '',
status: 'queued',
estimated_time: 120,
seconds_consumed: 5,
remaining_seconds_today: 595,
},
});
});
});
afterEach(() => {
vi.useRealTimers();
});
describe('addTask', () => {
it('should return null when canSubmit is false', async () => {
const result = await useGenerationStore.getState().addTask();
expect(result).toBeNull();
});
it('should create a task when prompt has text', async () => {
vi.useRealTimers();
useInputBarStore.getState().setPrompt('test prompt');
const id = await useGenerationStore.getState().addTask();
expect(id).not.toBeNull();
expect(useGenerationStore.getState().tasks).toHaveLength(1);
vi.useFakeTimers();
});
it('should create task with correct properties', async () => {
useInputBarStore.getState().setPrompt('a beautiful scene');
useInputBarStore.getState().setModel('seedance_2.0_fast');
useInputBarStore.getState().setAspectRatio('16:9');
useInputBarStore.getState().setDuration(10);
// Placeholder task is created synchronously
const promise = 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');
vi.useRealTimers();
await promise;
vi.useFakeTimers();
});
it('should snapshot references in universal mode', async () => {
useInputBarStore.getState().addReferences([
createMockFile('img1.jpg', 'image/jpeg'),
createMockFile('img2.jpg', 'image/jpeg'),
]);
const promise = useGenerationStore.getState().addTask();
const task = useGenerationStore.getState().tasks[0];
expect(task.references).toHaveLength(2);
expect(task.references[0].type).toBe('image');
vi.useRealTimers();
await promise;
vi.useFakeTimers();
});
it('should snapshot frames in keyframe mode', async () => {
useInputBarStore.getState().switchMode('keyframe');
useInputBarStore.getState().setFirstFrame(createMockFile('first.jpg', 'image/jpeg'));
useInputBarStore.getState().setLastFrame(createMockFile('last.jpg', 'image/jpeg'));
const promise = 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('尾帧');
vi.useRealTimers();
await promise;
vi.useFakeTimers();
});
it('should clear input after submit', async () => {
useInputBarStore.getState().setPrompt('test');
useInputBarStore.getState().addReferences([createMockFile('img.jpg', 'image/jpeg')]);
const promise = useGenerationStore.getState().addTask();
// Input is cleared synchronously after placeholder creation
expect(useInputBarStore.getState().prompt).toBe('');
expect(useInputBarStore.getState().references).toHaveLength(0);
vi.useRealTimers();
await promise;
vi.useFakeTimers();
});
it('should prepend new tasks (newest first)', async () => {
vi.useRealTimers();
useInputBarStore.getState().setPrompt('first');
await useGenerationStore.getState().addTask();
useInputBarStore.getState().setPrompt('second');
await useGenerationStore.getState().addTask();
const tasks = useGenerationStore.getState().tasks;
expect(tasks).toHaveLength(2);
expect(tasks[0].prompt).toBe('second');
expect(tasks[1].prompt).toBe('first');
vi.useFakeTimers();
});
it('should call videoApi.generate with correct data', async () => {
vi.useRealTimers();
useInputBarStore.getState().setPrompt('test prompt');
useInputBarStore.getState().setModel('seedance_2.0');
useInputBarStore.getState().setAspectRatio('16:9');
useInputBarStore.getState().setDuration(10);
await useGenerationStore.getState().addTask();
expect(mockGenerate).toHaveBeenCalledWith({
prompt: 'test prompt',
mode: 'universal',
model: 'seedance_2.0',
aspect_ratio: '16:9',
duration: 10,
references: [],
});
vi.useFakeTimers();
});
it('should upload files to TOS before generating', async () => {
vi.useRealTimers();
useInputBarStore.getState().addReferences([
createMockFile('img.jpg', 'image/jpeg'),
]);
await useGenerationStore.getState().addTask();
expect(mockUpload).toHaveBeenCalledTimes(1);
expect(mockGenerate).toHaveBeenCalledTimes(1);
vi.useFakeTimers();
});
});
describe('removeTask', () => {
it('should remove a task by id', async () => {
vi.useRealTimers();
useInputBarStore.getState().setPrompt('test');
const id = await useGenerationStore.getState().addTask();
expect(useGenerationStore.getState().tasks).toHaveLength(1);
useGenerationStore.getState().removeTask(id!);
expect(useGenerationStore.getState().tasks).toHaveLength(0);
vi.useFakeTimers();
});
it('should not affect other tasks', async () => {
vi.useRealTimers();
useInputBarStore.getState().setPrompt('first');
const id1 = await useGenerationStore.getState().addTask();
useInputBarStore.getState().setPrompt('second');
await useGenerationStore.getState().addTask();
useGenerationStore.getState().removeTask(id1!);
expect(useGenerationStore.getState().tasks).toHaveLength(1);
expect(useGenerationStore.getState().tasks[0].prompt).toBe('second');
vi.useFakeTimers();
});
});
describe('reEdit', () => {
it('should restore prompt from task', async () => {
vi.useRealTimers();
useInputBarStore.getState().setPrompt('original prompt');
const id = await useGenerationStore.getState().addTask();
// Input is cleared after submit
expect(useInputBarStore.getState().prompt).toBe('');
useGenerationStore.getState().reEdit(id!);
expect(useInputBarStore.getState().prompt).toBe('original prompt');
vi.useFakeTimers();
});
it('should restore settings from task', async () => {
vi.useRealTimers();
useInputBarStore.getState().setPrompt('test');
useInputBarStore.getState().setAspectRatio('16:9');
useInputBarStore.getState().setDuration(10);
const id = await 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);
vi.useFakeTimers();
});
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', async () => {
vi.useRealTimers();
useInputBarStore.getState().setPrompt('test');
const originalId = await useGenerationStore.getState().addTask();
useGenerationStore.getState().regenerate(originalId!);
// Allow time for the async regenerate to complete
await new Promise(resolve => setTimeout(resolve, 100));
const tasks = useGenerationStore.getState().tasks;
expect(tasks.length).toBeGreaterThanOrEqual(2);
const newTask = tasks[0]; // newest first
expect(newTask.id).not.toBe(originalId);
expect(newTask.prompt).toBe('test');
expect(newTask.status).toBe('generating');
vi.useFakeTimers();
});
it('should do nothing for non-existent task', () => {
useGenerationStore.getState().regenerate('non_existent');
expect(useGenerationStore.getState().tasks).toHaveLength(0);
});
});
describe('loadTasks', () => {
it('should fetch tasks from backend on load', async () => {
vi.useRealTimers();
mockGetTasks.mockResolvedValueOnce({
data: {
results: [
{
id: 1,
task_id: 'uuid-from-db',
ark_task_id: 'ark-123',
prompt: 'loaded prompt',
mode: 'universal',
model: 'seedance_2.0',
aspect_ratio: '16:9',
duration: 10,
seconds_consumed: 10,
status: 'completed',
result_url: 'https://example.com/video.mp4',
error_message: '',
reference_urls: [],
created_at: '2026-03-13T00:00:00Z',
},
],
},
});
await useGenerationStore.getState().loadTasks();
const tasks = useGenerationStore.getState().tasks;
expect(tasks).toHaveLength(1);
expect(tasks[0].prompt).toBe('loaded prompt');
expect(tasks[0].status).toBe('completed');
expect(tasks[0].resultUrl).toBe('https://example.com/video.mp4');
vi.useFakeTimers();
});
it('should set isLoading state', async () => {
vi.useRealTimers();
await useGenerationStore.getState().loadTasks();
expect(useGenerationStore.getState().isLoading).toBe(false);
vi.useFakeTimers();
});
});
});