test: fix config tests and add Sora/User component tests
Some checks failed
CI / test (push) Has been cancelled
CI / golangci-lint (push) Has been cancelled
Security Scan / backend-security (push) Has been cancelled
Security Scan / frontend-security (push) Has been cancelled

- 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:
User
2026-04-16 10:35:54 +08:00
parent 2d59b9ebfc
commit 7fa795e6a4
4 changed files with 931 additions and 7 deletions

View 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)
})
})
})