Fix prop confirmation bug, add Wan 2.7 model, refine multiple UI details, improve prop generation quality and aspect ratio, remove text overlays from Asset Center created images, and optimize prop filtering logic

This commit is contained in:
saturn
2026-04-03 22:36:41 +08:00
parent 854b932e67
commit 78b93331b4
136 changed files with 3393 additions and 875 deletions

View File

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