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 = { 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(), 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') }) })