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; return { ...original, useAuthStore: Object.assign( (selector: (s: Record) => 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(); }); }); });