test: fix config tests and add Sora/User component tests
- Fix config_test.go viper isolation by creating empty config file in temp dir - Fix TestLoadForcedCodexInstructionsTemplate path handling for Windows - Add SoraGeneratePage.spec.ts with comprehensive tests for Sora generation - Add UserEditModal.spec.ts with tests for user edit modal - Update sora_handler_test.go with additional field tests
This commit is contained in:
382
frontend/src/components/sora/__tests__/SoraGeneratePage.spec.ts
Normal file
382
frontend/src/components/sora/__tests__/SoraGeneratePage.spec.ts
Normal file
@@ -0,0 +1,382 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import SoraGeneratePage from '../SoraGeneratePage.vue'
|
||||
import type { SoraGeneration, QuotaInfo, GenerateRequest } from '@/api/sora'
|
||||
|
||||
// 使用 vi.hoisted 确保 mock 函数在 mock 工厂函数执行前定义
|
||||
const {
|
||||
mockGenerate,
|
||||
mockGetGeneration,
|
||||
mockCancelGeneration,
|
||||
mockDeleteGeneration,
|
||||
mockSaveToStorage,
|
||||
mockGetQuota,
|
||||
mockGetModels,
|
||||
mockListGenerations
|
||||
} = vi.hoisted(() => ({
|
||||
mockGenerate: vi.fn(),
|
||||
mockGetGeneration: vi.fn(),
|
||||
mockCancelGeneration: vi.fn(),
|
||||
mockDeleteGeneration: vi.fn(),
|
||||
mockSaveToStorage: vi.fn(),
|
||||
mockGetQuota: vi.fn(),
|
||||
mockGetModels: vi.fn(),
|
||||
mockListGenerations: vi.fn()
|
||||
}))
|
||||
|
||||
// Mock SoraProgressCard component
|
||||
vi.mock('../SoraProgressCard.vue', () => ({
|
||||
default: defineComponent({
|
||||
name: 'SoraProgressCard',
|
||||
props: ['generation'],
|
||||
emits: ['cancel', 'delete', 'save', 'retry'],
|
||||
template: '<div class="sora-progress-card" :data-id="generation.id">{{ generation.status }}</div>'
|
||||
})
|
||||
}))
|
||||
|
||||
// Mock SoraPromptBar component - 必须暴露 fillPrompt 和 reset 方法
|
||||
vi.mock('../SoraPromptBar.vue', () => ({
|
||||
default: defineComponent({
|
||||
name: 'SoraPromptBar',
|
||||
props: ['generating', 'activeTaskCount', 'maxConcurrentTasks'],
|
||||
emits: ['generate'],
|
||||
setup(_, { expose }) {
|
||||
const promptText = ref('')
|
||||
const fillPrompt = (text: string) => {
|
||||
promptText.value = text
|
||||
}
|
||||
const reset = () => {
|
||||
promptText.value = ''
|
||||
}
|
||||
expose({ fillPrompt, reset })
|
||||
return { promptText, fillPrompt, reset }
|
||||
},
|
||||
template: '<div class="sora-prompt-bar"><slot /></div>'
|
||||
})
|
||||
}))
|
||||
|
||||
// Mock API
|
||||
vi.mock('@/api/sora', () => ({
|
||||
default: {
|
||||
generate: mockGenerate,
|
||||
getGeneration: mockGetGeneration,
|
||||
cancelGeneration: mockCancelGeneration,
|
||||
deleteGeneration: mockDeleteGeneration,
|
||||
saveToStorage: mockSaveToStorage,
|
||||
getQuota: mockGetQuota,
|
||||
getModels: mockGetModels,
|
||||
listGenerations: mockListGenerations,
|
||||
getStorageStatus: vi.fn().mockResolvedValue({ s3_enabled: true, s3_healthy: true })
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock vue-i18n
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Mock window.Notification
|
||||
vi.stubGlobal('Notification', {
|
||||
permission: 'default',
|
||||
requestPermission: vi.fn().mockResolvedValue('granted')
|
||||
})
|
||||
|
||||
function createMockGeneration(overrides: Partial<SoraGeneration> = {}): SoraGeneration {
|
||||
return {
|
||||
id: 1,
|
||||
user_id: 1,
|
||||
model: 'sora2',
|
||||
prompt: 'Test prompt',
|
||||
media_type: 'video',
|
||||
status: 'completed',
|
||||
storage_type: 's3',
|
||||
media_url: 'https://example.com/video.mp4',
|
||||
media_urls: [],
|
||||
s3_object_keys: [],
|
||||
file_size_bytes: 1024 * 1024,
|
||||
error_message: '',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function createMockQuota(overrides: Partial<QuotaInfo> = {}): QuotaInfo {
|
||||
return {
|
||||
quota_bytes: 10 * 1024 * 1024 * 1024,
|
||||
used_bytes: 1 * 1024 * 1024 * 1024,
|
||||
available_bytes: 9 * 1024 * 1024 * 1024,
|
||||
quota_source: 'user',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('SoraGeneratePage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.useFakeTimers()
|
||||
|
||||
mockGetQuota.mockResolvedValue(createMockQuota())
|
||||
mockGetModels.mockResolvedValue([
|
||||
{ id: 'sora2', name: 'Sora 2', type: 'video', orientations: ['landscape', 'portrait'], durations: [10, 15, 25] }
|
||||
])
|
||||
mockListGenerations.mockResolvedValue({ data: [], total: 0, page: 1 })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe('初始渲染', () => {
|
||||
it('无活跃任务时显示欢迎区域', async () => {
|
||||
const wrapper = mount(SoraGeneratePage)
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('.sora-welcome-section').exists()).toBe(true)
|
||||
expect(wrapper.find('.sora-welcome-title').text()).toBe('sora.welcomeTitle')
|
||||
})
|
||||
|
||||
it('无活跃任务时显示示例提示词', async () => {
|
||||
const wrapper = mount(SoraGeneratePage)
|
||||
|
||||
await flushPromises()
|
||||
|
||||
const examplePrompts = wrapper.findAll('.sora-example-prompt')
|
||||
expect(examplePrompts.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('有活跃任务时隐藏欢迎区域', async () => {
|
||||
mockListGenerations.mockResolvedValue({
|
||||
data: [createMockGeneration({ status: 'generating' })],
|
||||
total: 1,
|
||||
page: 1
|
||||
})
|
||||
|
||||
const wrapper = mount(SoraGeneratePage)
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('.sora-welcome-section').exists()).toBe(false)
|
||||
expect(wrapper.find('.sora-task-cards').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('生成流程', () => {
|
||||
it('点击示例提示词填充输入框', async () => {
|
||||
const wrapper = mount(SoraGeneratePage)
|
||||
|
||||
await flushPromises()
|
||||
|
||||
const firstExample = wrapper.find('.sora-example-prompt')
|
||||
// 点击不应抛出错误(fillPrompt 方法已被 mock)
|
||||
await firstExample.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('成功提交生成请求', async () => {
|
||||
mockGenerate.mockResolvedValue({ generation_id: 123 })
|
||||
mockGetGeneration.mockResolvedValue(createMockGeneration({ id: 123, status: 'pending' }))
|
||||
|
||||
const wrapper = mount(SoraGeneratePage)
|
||||
|
||||
await flushPromises()
|
||||
|
||||
// 直接通过 findComponent 获取 SoraPromptBar 并触发 generate 事件
|
||||
const promptBar = wrapper.findComponent({ name: 'SoraPromptBar' })
|
||||
const generateReq: GenerateRequest = { model: 'sora2', prompt: 'Test prompt', media_type: 'video' }
|
||||
await promptBar.vm.$emit('generate', generateReq)
|
||||
await flushPromises()
|
||||
|
||||
expect(mockGenerate).toHaveBeenCalledWith(generateReq)
|
||||
})
|
||||
|
||||
it('生成失败时显示错误提示', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
mockGenerate.mockRejectedValue(new Error('Generation failed'))
|
||||
|
||||
// Mock alert
|
||||
vi.stubGlobal('alert', vi.fn())
|
||||
|
||||
const wrapper = mount(SoraGeneratePage)
|
||||
|
||||
await flushPromises()
|
||||
|
||||
const promptBar = wrapper.findComponent({ name: 'SoraPromptBar' })
|
||||
const generateReq: GenerateRequest = { model: 'sora2', prompt: 'Test', media_type: 'video' }
|
||||
await promptBar.vm.$emit('generate', generateReq)
|
||||
await flushPromises()
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalled()
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('任务管理', () => {
|
||||
it('取消任务', async () => {
|
||||
mockCancelGeneration.mockResolvedValue({})
|
||||
mockListGenerations.mockResolvedValue({
|
||||
data: [createMockGeneration({ id: 1, status: 'generating' })],
|
||||
total: 1,
|
||||
page: 1
|
||||
})
|
||||
|
||||
const wrapper = mount(SoraGeneratePage)
|
||||
|
||||
await flushPromises()
|
||||
|
||||
const progressCard = wrapper.findComponent({ name: 'SoraProgressCard' })
|
||||
await progressCard.vm.$emit('cancel', 1)
|
||||
await flushPromises()
|
||||
|
||||
expect(mockCancelGeneration).toHaveBeenCalledWith(1)
|
||||
})
|
||||
|
||||
it('删除任务', async () => {
|
||||
mockDeleteGeneration.mockResolvedValue({})
|
||||
mockListGenerations.mockResolvedValue({
|
||||
data: [createMockGeneration({ id: 1 })],
|
||||
total: 1,
|
||||
page: 1
|
||||
})
|
||||
|
||||
const wrapper = mount(SoraGeneratePage)
|
||||
|
||||
await flushPromises()
|
||||
|
||||
const progressCard = wrapper.findComponent({ name: 'SoraProgressCard' })
|
||||
await progressCard.vm.$emit('delete', 1)
|
||||
await flushPromises()
|
||||
|
||||
expect(mockDeleteGeneration).toHaveBeenCalledWith(1)
|
||||
})
|
||||
|
||||
it('保存到存储', async () => {
|
||||
mockSaveToStorage.mockResolvedValue({ message: 'Saved' })
|
||||
mockGetGeneration.mockResolvedValue(createMockGeneration({ id: 1, storage_type: 's3' }))
|
||||
mockListGenerations.mockResolvedValue({
|
||||
data: [createMockGeneration({ id: 1 })],
|
||||
total: 1,
|
||||
page: 1
|
||||
})
|
||||
|
||||
const wrapper = mount(SoraGeneratePage)
|
||||
|
||||
await flushPromises()
|
||||
|
||||
const progressCard = wrapper.findComponent({ name: 'SoraProgressCard' })
|
||||
await progressCard.vm.$emit('save', 1)
|
||||
await flushPromises()
|
||||
|
||||
expect(mockSaveToStorage).toHaveBeenCalledWith(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('任务计数', () => {
|
||||
it('计算活跃任务数量', async () => {
|
||||
mockListGenerations.mockResolvedValue({
|
||||
data: [
|
||||
createMockGeneration({ id: 1, status: 'pending' }),
|
||||
createMockGeneration({ id: 2, status: 'generating' }),
|
||||
createMockGeneration({ id: 3, status: 'completed' })
|
||||
],
|
||||
total: 3,
|
||||
page: 1
|
||||
})
|
||||
|
||||
const wrapper = mount(SoraGeneratePage)
|
||||
|
||||
await flushPromises()
|
||||
|
||||
// activeTaskCount 应该只计算 pending 和 generating
|
||||
const promptBar = wrapper.findComponent({ name: 'SoraPromptBar' })
|
||||
expect(promptBar.props('activeTaskCount')).toBe(2)
|
||||
})
|
||||
|
||||
it('触发 task-count-change 事件', async () => {
|
||||
mockListGenerations.mockResolvedValue({
|
||||
data: [createMockGeneration({ status: 'generating' })],
|
||||
total: 1,
|
||||
page: 1
|
||||
})
|
||||
|
||||
const wrapper = mount(SoraGeneratePage)
|
||||
|
||||
await flushPromises()
|
||||
|
||||
// 事件应该在 watch 中触发
|
||||
expect(wrapper.emitted('task-count-change')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('轮询机制', () => {
|
||||
it('启动轮询检查任务状态', async () => {
|
||||
mockGenerate.mockResolvedValue({ generation_id: 123 })
|
||||
// 使用接近当前时间的时间戳,确保轮询间隔为 3 秒
|
||||
const now = new Date().toISOString()
|
||||
mockGetGeneration.mockResolvedValue(createMockGeneration({ id: 123, status: 'generating', created_at: now }))
|
||||
|
||||
const wrapper = mount(SoraGeneratePage)
|
||||
|
||||
await flushPromises()
|
||||
|
||||
const promptBar = wrapper.findComponent({ name: 'SoraPromptBar' })
|
||||
const generateReq: GenerateRequest = { model: 'sora2', prompt: 'Test', media_type: 'video' }
|
||||
await promptBar.vm.$emit('generate', generateReq)
|
||||
await flushPromises()
|
||||
|
||||
// 第一次 getGeneration 在 handleGenerate 中
|
||||
expect(mockGetGeneration).toHaveBeenCalledTimes(1)
|
||||
|
||||
// 快进轮询定时器 - 对于刚创建的任务,轮询间隔为 3 秒
|
||||
vi.advanceTimersByTime(3000)
|
||||
await flushPromises()
|
||||
|
||||
// 轮询应该再次调用 getGeneration
|
||||
expect(mockGetGeneration).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('边界条件', () => {
|
||||
it('API 错误时不会崩溃', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
mockGetQuota.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const wrapper = mount(SoraGeneratePage)
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('组件卸载时清理定时器', async () => {
|
||||
mockGenerate.mockResolvedValue({ generation_id: 123 })
|
||||
mockGetGeneration.mockResolvedValue(createMockGeneration({ id: 123, status: 'generating' }))
|
||||
|
||||
const wrapper = mount(SoraGeneratePage)
|
||||
|
||||
await flushPromises()
|
||||
|
||||
const promptBar = wrapper.findComponent({ name: 'SoraPromptBar' })
|
||||
const generateReq: GenerateRequest = { model: 'sora2', prompt: 'Test', media_type: 'video' }
|
||||
await promptBar.vm.$emit('generate', generateReq)
|
||||
await flushPromises()
|
||||
|
||||
// 卸载组件
|
||||
wrapper.unmount()
|
||||
|
||||
// 清理后不应该有内存泄漏
|
||||
vi.advanceTimersByTime(10000)
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user