first commit
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
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]')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user