import * as React from 'react' import { createElement } 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 { AI_EDIT_BUTTON_CLASS } from '@/components/ui/ai-edit-style' const locationImageListMock = vi.hoisted(() => vi.fn((props: { overlayActions?: React.ReactNode }) => createElement('div', null, props.overlayActions ?? null))) const uploadMutationMock = vi.hoisted(() => ({ isPending: false, mutate: vi.fn(), })) vi.mock('@/lib/query/mutations', () => ({ useUploadProjectLocationImage: () => uploadMutationMock, })) vi.mock('@/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/assets/location-card/LocationCardHeader', () => ({ default: () => createElement('div', null), })) vi.mock('@/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/assets/location-card/LocationCardActions', () => ({ default: () => createElement('div', null), })) vi.mock('@/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/assets/location-card/LocationImageList', () => ({ default: locationImageListMock, })) vi.mock('@/components/ui/icons', () => ({ AppIcon: () => createElement('span', null), })) vi.mock('@/components/ui/icons/AISparklesIcon', () => ({ default: (props: { className?: string }) => createElement('svg', { className: props.className, 'data-icon': 'ai-sparkles' }), })) vi.mock('@/components/task/TaskStatusInline', () => ({ default: () => createElement('span', null), })) vi.mock('@/components/image-generation/ImageGenerationInlineCountButton', () => ({ default: () => createElement('button', null), })) vi.mock('@/lib/image-generation/use-image-generation-count', () => ({ useImageGenerationCount: () => ({ count: 1, setCount: vi.fn(), }), })) vi.mock('@/lib/image-generation/count', () => ({ getImageGenerationCountOptions: () => [{ value: 1, label: '1' }], })) vi.mock('@/lib/task/presentation', () => ({ resolveTaskPresentationState: () => null, })) const messages = { assets: { image: { upload: '上传图片', uploadReplace: '上传替换图片', edit: '编辑图片', undo: '撤回', regenerateStuck: '重新生成', }, location: { regenerateImage: '重新生成场景', edit: '编辑场景', delete: '删除场景', }, prop: { regenerateImage: '重新生成道具', edit: '编辑道具', delete: '删除道具', }, }, } as const const TestIntlProvider = NextIntlClientProvider as React.ComponentType<{ locale: string messages: AbstractIntlMessages timeZone: string children?: React.ReactNode }> describe('LocationCard AI edit button', () => { it('uses the shared AI edit button style in single-image mode', async () => { locationImageListMock.mockClear() Reflect.set(globalThis, 'React', React) const { default: LocationCard } = await import('@/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/assets/LocationCard') const html = renderToStaticMarkup( createElement( TestIntlProvider, { locale: 'zh', messages: messages as unknown as AbstractIntlMessages, timeZone: 'Asia/Shanghai', }, createElement(LocationCard, { location: { id: 'prop-1', name: '银质餐具', summary: '银质西式餐具套装', selectedImageId: 'prop-image-1', images: [ { id: 'prop-image-1', imageIndex: 0, description: '银质餐具套装,包含刀叉与汤匙,金属光泽冷白', imageUrl: 'https://example.com/prop.png', previousImageUrl: null, previousDescription: null, isSelected: true, }, ], }, assetType: 'prop', onEdit: () => undefined, onDelete: () => undefined, onRegenerate: () => undefined, onGenerate: () => undefined, onImageClick: () => undefined, onImageEdit: () => undefined, projectId: 'project-1', }), ), ) expect(html).toContain('data-icon=\"ai-sparkles\"') for (const token of AI_EDIT_BUTTON_CLASS.split(' ')) { expect(html).toContain(token) } const firstCall = locationImageListMock.mock.calls[0]?.[0] as { aspectClassName?: string } | undefined expect(firstCall?.aspectClassName).toBe('aspect-[3/2]') }) it('passes a square image slot to project location cards', async () => { locationImageListMock.mockClear() Reflect.set(globalThis, 'React', React) const { default: LocationCard } = await import('@/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/assets/LocationCard') renderToStaticMarkup( createElement( TestIntlProvider, { locale: 'zh', messages: messages as unknown as AbstractIntlMessages, timeZone: 'Asia/Shanghai', }, createElement(LocationCard, { location: { id: 'location-1', name: '餐厅', summary: '极简餐厅', selectedImageId: 'location-image-1', images: [ { id: 'location-image-1', imageIndex: 0, description: '极简餐厅室内空间', imageUrl: 'https://example.com/location.png', previousImageUrl: null, previousDescription: null, isSelected: true, }, ], }, assetType: 'location', onEdit: () => undefined, onDelete: () => undefined, onRegenerate: () => undefined, onGenerate: () => undefined, onImageClick: () => undefined, onImageEdit: () => undefined, projectId: 'project-1', }), ), ) const firstCall = locationImageListMock.mock.calls[0]?.[0] as { aspectClassName?: string } | undefined expect(firstCall?.aspectClassName).toBe('aspect-square') }) })