Files
waooplus/tests/unit/components/asset-hub-card-aspect-ratio.test.ts

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]')
})
})