feat: initial release v0.3.0
This commit is contained in:
116
tests/unit/components/character-creation-modal.test.ts
Normal file
116
tests/unit/components/character-creation-modal.test.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import * as React from 'react'
|
||||
import { createElement } from 'react'
|
||||
import type { ComponentProps, ReactElement } from 'react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { NextIntlClientProvider } from 'next-intl'
|
||||
import type { AbstractIntlMessages } from 'next-intl'
|
||||
import { CharacterCreationModal } from '@/components/shared/assets/CharacterCreationModal'
|
||||
|
||||
vi.mock('@/lib/query/hooks', () => ({
|
||||
useProjectAssets: vi.fn(() => ({ data: { characters: [] } })),
|
||||
}))
|
||||
|
||||
vi.mock('@/components/shared/assets/character-creation/hooks/useCharacterCreationSubmit', () => ({
|
||||
useCharacterCreationSubmit: vi.fn(() => ({
|
||||
isSubmitting: false,
|
||||
isAiDesigning: false,
|
||||
isExtracting: false,
|
||||
characterGenerationCount: 3,
|
||||
setCharacterGenerationCount: vi.fn(),
|
||||
referenceCharacterGenerationCount: 3,
|
||||
setReferenceCharacterGenerationCount: vi.fn(),
|
||||
handleExtractDescription: vi.fn(),
|
||||
handleCreateWithReference: vi.fn(),
|
||||
handleAiDesign: vi.fn(),
|
||||
handleSubmit: vi.fn(),
|
||||
handleSubmitAndGenerate: vi.fn(),
|
||||
})),
|
||||
}))
|
||||
|
||||
const messages = {
|
||||
assetModal: {
|
||||
character: {
|
||||
title: '新建角色',
|
||||
name: '角色名称',
|
||||
namePlaceholder: '请输入角色名称',
|
||||
modeReference: '参考图模式',
|
||||
modeDescription: '描述模式',
|
||||
uploadReference: '上传参考图',
|
||||
pasteHint: 'Ctrl+V 粘贴',
|
||||
generationMode: '生成方式',
|
||||
directGenerate: '直接生成',
|
||||
extractPrompt: '反推提示词',
|
||||
extractFirst: '先提取描述',
|
||||
description: '角色描述',
|
||||
descPlaceholder: '请输入角色外貌描述...',
|
||||
isSubAppearance: '这是一个子形象',
|
||||
isSubAppearanceHint: '为已有角色添加新的形象状态',
|
||||
selectMainCharacter: '选择主角色',
|
||||
selectCharacterPlaceholder: '请选择角色...',
|
||||
appearancesCount: '{count} 个形象',
|
||||
changeReason: '形象变化原因',
|
||||
changeReasonPlaceholder: '例如',
|
||||
useReferenceGeneratePrefix: '使用参考图生成',
|
||||
generateCountSuffix: '张图片',
|
||||
selectReferenceGenerateCount: '选择参考图生成数量',
|
||||
},
|
||||
artStyle: { title: '画面风格' },
|
||||
aiDesign: {
|
||||
title: 'AI 设计',
|
||||
placeholder: '描述你想要的角色特征...',
|
||||
generating: '设计中...',
|
||||
generate: '生成',
|
||||
},
|
||||
common: {
|
||||
creating: '创建中...',
|
||||
cancel: '取消',
|
||||
adding: '添加中...',
|
||||
add: '添加',
|
||||
addOnly: '仅添加角色',
|
||||
addOnlyToAssetHub: '仅添加人物到资产库',
|
||||
addAndGeneratePrefix: '添加并生成',
|
||||
generateCountSuffix: '张图片',
|
||||
selectGenerateCount: '选择生成数量',
|
||||
optional: '(可选)',
|
||||
},
|
||||
errors: {
|
||||
uploadFailed: '上传失败',
|
||||
extractDescriptionFailed: '提取描述失败',
|
||||
createFailed: '创建失败',
|
||||
aiDesignFailed: 'AI 设计失败',
|
||||
addSubAppearanceFailed: '添加子形象失败',
|
||||
insufficientBalance: '账户余额不足',
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
const renderWithIntl = (node: ReactElement) => {
|
||||
const providerProps: ComponentProps<typeof NextIntlClientProvider> = {
|
||||
locale: 'zh',
|
||||
messages: messages as unknown as AbstractIntlMessages,
|
||||
timeZone: 'Asia/Shanghai',
|
||||
children: node,
|
||||
}
|
||||
|
||||
return renderToStaticMarkup(
|
||||
createElement(NextIntlClientProvider, providerProps),
|
||||
)
|
||||
}
|
||||
|
||||
describe('CharacterCreationModal', () => {
|
||||
it('renders add-only and add-and-generate actions in the fixed footer', () => {
|
||||
Reflect.set(globalThis, 'React', React)
|
||||
const html = renderWithIntl(
|
||||
createElement(CharacterCreationModal, {
|
||||
mode: 'asset-hub',
|
||||
onClose: () => undefined,
|
||||
onSuccess: () => undefined,
|
||||
}),
|
||||
)
|
||||
|
||||
expect(html).toContain('仅添加人物到资产库')
|
||||
expect(html).toContain('添加并生成')
|
||||
expect(html).toContain('取消')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as React from 'react'
|
||||
import { createElement } from 'react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import ImageGenerationInlineCountButton from '@/components/image-generation/ImageGenerationInlineCountButton'
|
||||
|
||||
describe('ImageGenerationInlineCountButton', () => {
|
||||
it('keeps the select enabled when only the action is disabled', () => {
|
||||
Reflect.set(globalThis, 'React', React)
|
||||
|
||||
const html = renderToStaticMarkup(
|
||||
createElement(ImageGenerationInlineCountButton, {
|
||||
prefix: createElement('span', null, '生成'),
|
||||
suffix: createElement('span', null, '张图片'),
|
||||
value: 3,
|
||||
options: [1, 2, 3],
|
||||
onValueChange: () => undefined,
|
||||
onClick: () => undefined,
|
||||
actionDisabled: true,
|
||||
selectDisabled: false,
|
||||
ariaLabel: '选择生成数量',
|
||||
}),
|
||||
)
|
||||
|
||||
expect(html).toContain('aria-disabled="true"')
|
||||
expect(html).toContain('opacity-60 cursor-not-allowed')
|
||||
expect(html).not.toContain('<select disabled=""')
|
||||
})
|
||||
})
|
||||
81
tests/unit/components/location-creation-modal.test.ts
Normal file
81
tests/unit/components/location-creation-modal.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import * as React from 'react'
|
||||
import { createElement } from 'react'
|
||||
import type { ComponentProps, ReactElement } from 'react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { NextIntlClientProvider } from 'next-intl'
|
||||
import type { AbstractIntlMessages } from 'next-intl'
|
||||
import { LocationCreationModal } from '@/components/shared/assets/LocationCreationModal'
|
||||
|
||||
vi.mock('@/lib/query/hooks', () => ({
|
||||
useAiCreateProjectLocation: vi.fn(() => ({ mutateAsync: vi.fn() })),
|
||||
useAiDesignLocation: vi.fn(() => ({ mutateAsync: vi.fn() })),
|
||||
useCreateAssetHubLocation: vi.fn(() => ({ mutateAsync: vi.fn() })),
|
||||
useGenerateLocationImage: vi.fn(() => ({ mutateAsync: vi.fn() })),
|
||||
useCreateProjectLocation: vi.fn(() => ({ mutateAsync: vi.fn() })),
|
||||
useGenerateProjectLocationImage: vi.fn(() => ({ mutateAsync: vi.fn() })),
|
||||
}))
|
||||
|
||||
const messages = {
|
||||
assetModal: {
|
||||
location: {
|
||||
title: '新建场景',
|
||||
name: '场景名称',
|
||||
namePlaceholder: '请输入场景名称',
|
||||
description: '场景描述',
|
||||
descPlaceholder: '请输入场景描述...',
|
||||
},
|
||||
artStyle: { title: '画面风格' },
|
||||
aiDesign: {
|
||||
title: 'AI 设计',
|
||||
placeholderLocation: '描述场景氛围和环境...',
|
||||
generating: '设计中...',
|
||||
generate: '生成',
|
||||
tip: '输入简单描述,AI 帮你生成详细设定',
|
||||
},
|
||||
common: {
|
||||
cancel: '取消',
|
||||
addOnlyLocation: '仅添加场景',
|
||||
addOnlyToAssetHubLocation: '仅添加场景到资产库',
|
||||
addAndGeneratePrefix: '添加并生成',
|
||||
generateCountSuffix: '张图片',
|
||||
selectGenerateCount: '选择生成数量',
|
||||
optional: '(可选)',
|
||||
},
|
||||
errors: {
|
||||
createFailed: '创建失败',
|
||||
aiDesignFailed: 'AI 设计失败',
|
||||
insufficientBalance: '账户余额不足',
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
const renderWithIntl = (node: ReactElement) => {
|
||||
const providerProps: ComponentProps<typeof NextIntlClientProvider> = {
|
||||
locale: 'zh',
|
||||
messages: messages as unknown as AbstractIntlMessages,
|
||||
timeZone: 'Asia/Shanghai',
|
||||
children: node,
|
||||
}
|
||||
|
||||
return renderToStaticMarkup(
|
||||
createElement(NextIntlClientProvider, providerProps),
|
||||
)
|
||||
}
|
||||
|
||||
describe('LocationCreationModal', () => {
|
||||
it('renders add-only and add-and-generate actions in the fixed footer', () => {
|
||||
Reflect.set(globalThis, 'React', React)
|
||||
const html = renderWithIntl(
|
||||
createElement(LocationCreationModal, {
|
||||
mode: 'asset-hub',
|
||||
onClose: () => undefined,
|
||||
onSuccess: () => undefined,
|
||||
}),
|
||||
)
|
||||
|
||||
expect(html).toContain('仅添加场景到资产库')
|
||||
expect(html).toContain('添加并生成')
|
||||
expect(html).toContain('取消')
|
||||
})
|
||||
})
|
||||
68
tests/unit/components/voice-design-shared.test.ts
Normal file
68
tests/unit/components/voice-design-shared.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
DEFAULT_VOICE_SCHEME_COUNT,
|
||||
MAX_VOICE_SCHEME_COUNT,
|
||||
MIN_VOICE_SCHEME_COUNT,
|
||||
generateVoiceDesignOptions,
|
||||
normalizeVoiceSchemeCount,
|
||||
} from '@/components/voice/voice-design-shared'
|
||||
|
||||
describe('voice-design-shared', () => {
|
||||
it('clamps scheme count into the supported range', () => {
|
||||
expect(normalizeVoiceSchemeCount(undefined)).toBe(DEFAULT_VOICE_SCHEME_COUNT)
|
||||
expect(normalizeVoiceSchemeCount('not-a-number')).toBe(DEFAULT_VOICE_SCHEME_COUNT)
|
||||
expect(normalizeVoiceSchemeCount(0)).toBe(MIN_VOICE_SCHEME_COUNT)
|
||||
expect(normalizeVoiceSchemeCount(99)).toBe(MAX_VOICE_SCHEME_COUNT)
|
||||
expect(normalizeVoiceSchemeCount('5')).toBe(5)
|
||||
})
|
||||
|
||||
it('generates the requested number of voice options with default preview text fallback', async () => {
|
||||
const onDesignVoice = vi
|
||||
.fn<(_: {
|
||||
voicePrompt: string
|
||||
previewText: string
|
||||
preferredName: string
|
||||
language: 'zh'
|
||||
}) => Promise<{ voiceId: string; audioBase64: string }>>()
|
||||
.mockResolvedValueOnce({ voiceId: 'voice-1', audioBase64: 'audio-1' })
|
||||
.mockResolvedValueOnce({ voiceId: 'voice-2', audioBase64: 'audio-2' })
|
||||
.mockResolvedValueOnce({ voiceId: 'voice-3', audioBase64: 'audio-3' })
|
||||
.mockResolvedValueOnce({ voiceId: 'voice-4', audioBase64: 'audio-4' })
|
||||
|
||||
const result = await generateVoiceDesignOptions({
|
||||
count: '4',
|
||||
voicePrompt: ' 温柔女声 ',
|
||||
previewText: ' ',
|
||||
defaultPreviewText: '默认试听文案',
|
||||
onDesignVoice,
|
||||
createPreferredName: (index) => `preferred-${index + 1}`,
|
||||
})
|
||||
|
||||
expect(result).toEqual([
|
||||
{ voiceId: 'voice-1', audioBase64: 'audio-1', audioUrl: 'data:audio/wav;base64,audio-1' },
|
||||
{ voiceId: 'voice-2', audioBase64: 'audio-2', audioUrl: 'data:audio/wav;base64,audio-2' },
|
||||
{ voiceId: 'voice-3', audioBase64: 'audio-3', audioUrl: 'data:audio/wav;base64,audio-3' },
|
||||
{ voiceId: 'voice-4', audioBase64: 'audio-4', audioUrl: 'data:audio/wav;base64,audio-4' },
|
||||
])
|
||||
expect(onDesignVoice.mock.calls).toEqual([
|
||||
[{ voicePrompt: '温柔女声', previewText: '默认试听文案', preferredName: 'preferred-1', language: 'zh' }],
|
||||
[{ voicePrompt: '温柔女声', previewText: '默认试听文案', preferredName: 'preferred-2', language: 'zh' }],
|
||||
[{ voicePrompt: '温柔女声', previewText: '默认试听文案', preferredName: 'preferred-3', language: 'zh' }],
|
||||
[{ voicePrompt: '温柔女声', previewText: '默认试听文案', preferredName: 'preferred-4', language: 'zh' }],
|
||||
])
|
||||
})
|
||||
|
||||
it('fails explicitly when a designed voice is missing voiceId', async () => {
|
||||
const onDesignVoice = vi.fn(async () => ({ voiceId: '', audioBase64: 'audio-only' }))
|
||||
|
||||
await expect(
|
||||
generateVoiceDesignOptions({
|
||||
count: 1,
|
||||
voicePrompt: '旁白',
|
||||
previewText: '测试',
|
||||
defaultPreviewText: '默认试听文案',
|
||||
onDesignVoice,
|
||||
}),
|
||||
).rejects.toThrow('VOICE_DESIGN_INVALID_RESPONSE: missing voiceId')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user