All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m17s
372 lines
12 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|