Files
waooplus/tests/unit/components/character-section-actions.test.ts

144 lines
4.6 KiB
TypeScript

import * as React from 'react'
import { createElement } from 'react'
import type { ComponentProps, ReactElement } from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
import { NextIntlClientProvider } from 'next-intl'
import type { AbstractIntlMessages } from 'next-intl'
import CharacterSection from '@/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/assets/CharacterSection'
const useProjectAssetsMock = vi.hoisted(() => vi.fn())
const characterCardMock = vi.hoisted(() => vi.fn((_props: unknown) => null))
vi.mock('@/lib/query/hooks/useProjectAssets', () => ({
useProjectAssets: (projectId: string | null) => useProjectAssetsMock(projectId),
}))
vi.mock('@/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/assets/CharacterCard', () => ({
__esModule: true,
default: (props: unknown) => characterCardMock(props),
}))
vi.mock('@/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/assets/CharacterProfileCard', () => ({
__esModule: true,
default: () => null,
}))
vi.mock('@/types/character-profile', () => ({
parseProfileData: () => null,
}))
vi.mock('@/components/task/TaskStatusInline', () => ({
__esModule: true,
default: () => null,
}))
vi.mock('@/lib/task/presentation', () => ({
resolveTaskPresentationState: () => null,
}))
vi.mock('@/components/ui/icons', () => ({
AppIcon: (props: { name?: string; className?: string }) =>
createElement('span', { 'data-icon': props.name, className: props.className }),
}))
const messages = {
assets: {
stage: {
characterAssets: '角色资产',
counts: '{characterCount} 个角色,{appearanceCount} 个形象',
pendingProfilesBanner: '待确认角色',
pendingProfilesHint: '确认角色设定',
confirmAll: '全部确认',
},
character: {
add: '新建角色',
assetCount: '{count} 个形象',
copyFromGlobal: '从资产中心导入',
delete: '删除角色',
},
},
} as const
function 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('CharacterSection actions', () => {
it('renders import and delete actions stacked vertically with the import icon', () => {
Reflect.set(globalThis, 'React', React)
useProjectAssetsMock.mockReturnValue({
data: {
characters: [
{
id: 'character-1',
name: '西装男',
introduction: null,
appearances: [
{
id: 'appearance-1',
appearanceIndex: 0,
changeReason: '初始形象',
imageUrl: null,
imageUrls: [],
selectedIndex: null,
},
],
},
],
},
})
const html = renderWithIntl(
createElement(CharacterSection, {
projectId: 'project-1',
activeTaskKeys: new Set<string>(),
onClearTaskKey: () => undefined,
onRegisterTransientTaskKey: () => undefined,
isAnalyzingAssets: false,
onAddCharacter: () => undefined,
onDeleteCharacter: () => undefined,
onDeleteAppearance: () => undefined,
onEditAppearance: () => undefined,
handleGenerateImage: async () => undefined,
onSelectImage: () => undefined,
onConfirmSelection: () => undefined,
onRegenerateSingle: async () => undefined,
onRegenerateGroup: async () => undefined,
onUndo: () => undefined,
onImageClick: () => undefined,
onImageEdit: () => undefined,
onVoiceChange: () => undefined,
onVoiceDesign: () => undefined,
onVoiceSelectFromHub: () => undefined,
onCopyFromGlobal: () => undefined,
getAppearances: (character) => character.appearances,
unconfirmedCharacters: [],
isConfirmingCharacter: () => false,
deletingCharacterId: null,
batchConfirming: false,
batchConfirmingState: null,
onBatchConfirm: () => undefined,
onEditProfile: () => undefined,
onConfirmProfile: () => undefined,
onUseExistingProfile: () => undefined,
onDeleteProfile: () => undefined,
}),
)
expect(html).toContain('从资产中心导入')
expect(html).toContain('删除角色')
expect(html).toContain('data-icon="arrowDownCircle"')
expect(html).toContain('flex flex-col items-end gap-1.5')
})
})