244 lines
7.0 KiB
TypeScript
244 lines
7.0 KiB
TypeScript
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'
|
|
|
|
const idleMutation = {
|
|
isPending: false,
|
|
mutate: vi.fn(),
|
|
}
|
|
|
|
vi.mock('@/lib/query/mutations', () => ({
|
|
useGenerateCharacterImage: () => idleMutation,
|
|
useSelectCharacterImage: () => idleMutation,
|
|
useUndoCharacterImage: () => idleMutation,
|
|
useUploadCharacterImage: () => idleMutation,
|
|
useDeleteCharacter: () => idleMutation,
|
|
useDeleteCharacterAppearance: () => idleMutation,
|
|
useUploadCharacterVoice: () => idleMutation,
|
|
useGenerateLocationImage: () => idleMutation,
|
|
useSelectLocationImage: () => idleMutation,
|
|
useUndoLocationImage: () => idleMutation,
|
|
useUploadLocationImage: () => idleMutation,
|
|
useDeleteLocation: () => idleMutation,
|
|
}))
|
|
|
|
vi.mock('@/components/ui/icons', () => ({
|
|
AppIcon: (props: { className?: string; name?: string }) =>
|
|
createElement('span', { className: props.className, 'data-icon': props.name }),
|
|
}))
|
|
|
|
vi.mock('@/components/task/TaskStatusOverlay', () => ({
|
|
default: () => createElement('div', null, 'overlay'),
|
|
}))
|
|
|
|
vi.mock('@/components/task/TaskStatusInline', () => ({
|
|
default: () => createElement('span', null, 'inline'),
|
|
}))
|
|
|
|
vi.mock('@/components/media/MediaImageWithLoading', () => ({
|
|
MediaImageWithLoading: (props: { containerClassName?: string; className?: string }) =>
|
|
createElement('div', {
|
|
className: [props.containerClassName, props.className].filter(Boolean).join(' '),
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/components/image-generation/ImageGenerationInlineCountButton', () => ({
|
|
default: () => createElement('button', null, 'count'),
|
|
}))
|
|
|
|
vi.mock('@/lib/task/presentation', () => ({
|
|
resolveTaskPresentationState: () => 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('@/app/[locale]/workspace/asset-hub/components/VoiceSettings', () => ({
|
|
default: () => createElement('div', null, 'voice-settings'),
|
|
}))
|
|
|
|
const messages = {
|
|
assetHub: {
|
|
generateFailed: '生成失败',
|
|
selectFailed: '选择失败',
|
|
uploadFailed: '上传失败',
|
|
confirmDeleteLocation: '确认删除场景',
|
|
confirmDeleteProp: '确认删除道具',
|
|
confirmDeleteCharacter: '确认删除角色',
|
|
cancel: '取消',
|
|
delete: '删除',
|
|
propLabel: '道具',
|
|
locationLabel: '场景',
|
|
},
|
|
assets: {
|
|
image: {
|
|
generateCountPrefix: '生成',
|
|
generateCountSuffix: '张',
|
|
generating: '生成中',
|
|
generatingPlaceholder: '正在生成',
|
|
regenerateStuck: '重新生成',
|
|
regenCountPrefix: '重生成',
|
|
undo: '撤回',
|
|
upload: '上传',
|
|
uploadReplace: '替换',
|
|
edit: '编辑',
|
|
selectCount: '选择数量',
|
|
confirmOption: '确认选择',
|
|
optionNumber: '方案 {number}',
|
|
},
|
|
common: {
|
|
generateFailed: '生成失败',
|
|
},
|
|
location: {
|
|
regenerateImage: '重生成场景',
|
|
edit: '编辑场景',
|
|
delete: '删除场景',
|
|
},
|
|
prop: {
|
|
regenerateImage: '重生成道具',
|
|
edit: '编辑道具',
|
|
delete: '删除道具',
|
|
},
|
|
character: {
|
|
deleteWhole: '删除整个角色',
|
|
primary: '主形象',
|
|
secondary: '子形象',
|
|
delete: '删除角色',
|
|
deleteOptions: '删除选项',
|
|
},
|
|
video: {
|
|
panelCard: {
|
|
editPrompt: '编辑',
|
|
},
|
|
},
|
|
},
|
|
} as const
|
|
|
|
const TestIntlProvider = NextIntlClientProvider as React.ComponentType<{
|
|
locale: string
|
|
messages: AbstractIntlMessages
|
|
timeZone: string
|
|
children?: React.ReactNode
|
|
}>
|
|
|
|
function renderWithIntl(node: React.ReactElement) {
|
|
return renderToStaticMarkup(
|
|
createElement(
|
|
TestIntlProvider,
|
|
{
|
|
locale: 'zh',
|
|
messages: messages as unknown as AbstractIntlMessages,
|
|
timeZone: 'Asia/Shanghai',
|
|
},
|
|
node,
|
|
),
|
|
)
|
|
}
|
|
|
|
describe('asset hub card aspect ratio', () => {
|
|
it('keeps prop cards at the same 3:2 ratio as character assets while generation is running', async () => {
|
|
Reflect.set(globalThis, 'React', React)
|
|
const { default: LocationCard } = await import('@/app/[locale]/workspace/asset-hub/components/LocationCard')
|
|
const html = renderWithIntl(
|
|
createElement(LocationCard, {
|
|
location: {
|
|
id: 'prop-1',
|
|
name: '鼠标',
|
|
summary: '电脑鼠标',
|
|
folderId: null,
|
|
images: [
|
|
{
|
|
id: 'prop-image-1',
|
|
imageIndex: 0,
|
|
description: null,
|
|
imageUrl: null,
|
|
previousImageUrl: null,
|
|
isSelected: false,
|
|
imageTaskRunning: true,
|
|
},
|
|
],
|
|
},
|
|
assetType: 'prop',
|
|
}),
|
|
)
|
|
|
|
expect(html).toContain('aspect-[3/2]')
|
|
expect(html).toContain('data-icon="image"')
|
|
expect(html).not.toContain('min-h-[100px]')
|
|
})
|
|
|
|
it('keeps location cards square while generation is running', async () => {
|
|
Reflect.set(globalThis, 'React', React)
|
|
const { default: LocationCard } = await import('@/app/[locale]/workspace/asset-hub/components/LocationCard')
|
|
const html = renderWithIntl(
|
|
createElement(LocationCard, {
|
|
location: {
|
|
id: 'location-1',
|
|
name: '餐厅',
|
|
summary: '极简餐厅',
|
|
folderId: null,
|
|
images: [
|
|
{
|
|
id: 'location-image-1',
|
|
imageIndex: 0,
|
|
description: null,
|
|
imageUrl: null,
|
|
previousImageUrl: null,
|
|
isSelected: false,
|
|
imageTaskRunning: true,
|
|
},
|
|
],
|
|
},
|
|
assetType: 'location',
|
|
}),
|
|
)
|
|
|
|
expect(html).toContain('aspect-square')
|
|
expect(html).toContain('data-icon="image"')
|
|
expect(html).not.toContain('min-h-[100px]')
|
|
})
|
|
|
|
it('keeps character cards at the fixed 3:2 ratio while generation is running', async () => {
|
|
Reflect.set(globalThis, 'React', React)
|
|
const { CharacterCard } = await import('@/app/[locale]/workspace/asset-hub/components/CharacterCard')
|
|
const html = renderWithIntl(
|
|
createElement(CharacterCard, {
|
|
character: {
|
|
id: 'character-1',
|
|
name: '沈烬',
|
|
folderId: null,
|
|
customVoiceUrl: null,
|
|
appearances: [
|
|
{
|
|
id: 'appearance-1',
|
|
appearanceIndex: 0,
|
|
changeReason: '默认形象',
|
|
description: null,
|
|
imageUrl: null,
|
|
imageUrls: [],
|
|
selectedIndex: null,
|
|
previousImageUrl: null,
|
|
previousImageUrls: [],
|
|
imageTaskRunning: true,
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
)
|
|
|
|
expect(html).toContain('aspect-[3/2]')
|
|
expect(html).not.toContain('min-h-[100px]')
|
|
})
|
|
})
|