/* @vitest-environment jsdom */ import { render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { useState } from 'react'; import { expect, test, vi } from 'vitest'; import type { CustomWorldNpc, CustomWorldPlayableNpc, CustomWorldProfile, } from '../types'; import { CustomWorldEntityCatalog } from './CustomWorldEntityCatalog'; import { type CustomWorldEditorTarget, CustomWorldEntityEditorModal, } from './CustomWorldEntityEditorModal'; import * as customWorldCoverAssetService from '../services/customWorldCoverAssetService'; vi.mock('../data/characterPresets', async () => { const actual = await vi.importActual( '../data/characterPresets', ); return { ...actual, buildCustomWorldPlayableCharacters: vi.fn(() => []), buildCustomWorldRuntimeCharacters: vi.fn(() => []), createCharacterSkillCooldowns: vi.fn(() => ({})), getCharacterMaxHp: vi.fn(() => 180), getCharacterMaxMana: vi.fn(() => 60), setRuntimeCharacterOverrides: vi.fn(), }; }); vi.mock('./CharacterAnimator', () => ({ CharacterAnimator: () =>
角色预览
, })); vi.mock('../services/aiService', () => ({ generateCustomWorldSceneImage: vi.fn(), generateCustomWorldSceneNpc: vi.fn(), generateInitialStory: vi.fn(), generateNextStep: vi.fn(), })); vi.mock('./CustomWorldNpcVisualEditor', () => ({ CustomWorldNpcPortrait: ({ npc }: { npc: { name: string } }) => (
{npc.name}
), CustomWorldNpcVisualEditor: () =>
预设形象编辑器
, })); vi.mock('./game-shell/GameShellRuntime', () => ({ GameShellRuntime: ({ session, }: { session: { gameState: { currentScenePreset?: { name?: string } | null } }; }) => (
幕预览运行时
{session.gameState.currentScenePreset?.name ?? '未进入场景'}
), })); vi.mock('./asset-studio/characterAssetWorkflowPersistence', () => ({ fetchCharacterWorkflowCache: vi.fn().mockResolvedValue({ cache: null }), saveCharacterWorkflowCache: vi.fn().mockResolvedValue(undefined), generateCharacterVisualCandidates: vi.fn(), publishCharacterVisualAsset: vi.fn(), generateCharacterAnimationDraft: vi.fn(), publishCharacterAnimationAssets: vi.fn(), })); vi.mock('../services/customWorldCoverAssetService', () => ({ generateCustomWorldCoverImage: vi.fn(), uploadCustomWorldCoverImage: vi.fn(), })); function createBackstoryReveal() { return { publicSummary: '公开背景', chapters: [ { id: 'surface', title: '表层来意', affinityRequired: 6, teaser: '表层来意', content: '表层来意内容', contextSnippet: '表层来意摘要', }, { id: 'scar', title: '旧事裂痕', affinityRequired: 12, teaser: '旧事裂痕', content: '旧事裂痕内容', contextSnippet: '旧事裂痕摘要', }, { id: 'hidden', title: '隐藏执念', affinityRequired: 18, teaser: '隐藏执念', content: '隐藏执念内容', contextSnippet: '隐藏执念摘要', }, { id: 'final', title: '最终底牌', affinityRequired: 24, teaser: '最终底牌', content: '最终底牌内容', contextSnippet: '最终底牌摘要', }, ], }; } function createPlayableRole(id: string, name: string): CustomWorldPlayableNpc { return { id, name, title: '同行者', role: '协作战力', description: `${name}的定位描述`, backstory: `${name}的背景`, personality: `${name}的性格`, motivation: `${name}的动机`, combatStyle: `${name}的战斗风格`, initialAffinity: 18, relationshipHooks: ['关系钩子'], relations: [], tags: ['测试'], backstoryReveal: createBackstoryReveal(), skills: [], initialItems: [], templateCharacterId: 'knight-female-1', }; } function createStoryRole(id: string, name: string): CustomWorldNpc { return { ...createPlayableRole(id, name), initialAffinity: 6, visual: undefined, }; } function createProfile(): CustomWorldProfile { return { id: 'world-1', settingText: '潮雾群岛上的禁制与旧航道正在一起失衡。', name: '潮雾群岛', subtitle: '旧航道与沉钟回响', summary: '一座正在被旧誓与新利益共同撕扯的群岛世界。', tone: '压抑、潮湿、带着未解旧伤。', playerGoal: '找到能让群岛重新稳定的关键节点。', templateWorldType: 'WUXIA', majorFactions: ['守潮盟', '沉钟会'], coreConflicts: ['旧航道归属', '沉钟遗产争夺'], attributeSchema: { id: 'schema-1', worldId: 'world-1', schemaVersion: 1, generatedFrom: { worldType: 'WUXIA', worldName: '潮雾群岛', settingSummary: '潮雾群岛上的禁制与旧航道正在一起失衡。', tone: '压抑、潮湿、带着未解旧伤。', conflictCore: '旧航道归属', }, slots: [], }, playableNpcs: [createPlayableRole('playable-1', '沈砺')], storyNpcs: [createStoryRole('story-1', '顾潮音')], items: [], camp: { name: '潮灯居', description: '玩家最初落脚的旧灯塔内院。', dangerLevel: 'medium', }, landmarks: [], creatorIntent: null, anchorPack: null, lockState: null, ownedSettingLayers: null, generationMode: 'full', generationStatus: 'complete', } as unknown as CustomWorldProfile; } function createProfileWithLandmark(): CustomWorldProfile { return { ...createProfile(), storyNpcs: [ createStoryRole('story-1', '顾潮音'), createStoryRole('story-2', '闻雪汀'), createStoryRole('story-3', '谢孤灯'), ], landmarks: [ { id: 'landmark-1', name: '沉钟栈桥', description: '旧钟与潮声常年相撞的码头栈桥。', dangerLevel: 'medium', imageSrc: '/generated-custom-world-scenes/original-scene.png', sceneNpcIds: ['story-1', 'story-2', 'story-3'], connections: [], }, ], } as unknown as CustomWorldProfile; } function LandmarkEditorFlowHarness() { const [profile, setProfile] = useState(createProfileWithLandmark()); const [target, setTarget] = useState({ kind: 'landmark', mode: 'edit', id: 'landmark-1', }); return ( <>
        {JSON.stringify(profile)}
      
{}} onEditTarget={setTarget} onProfileChange={setProfile} onDeleteStoryNpcs={() => {}} onDeleteLandmarks={() => {}} /> setTarget(null)} onProfileChange={setProfile} /> ); } function readLandmarkHarnessProfile() { const content = screen.getByTestId('landmark-profile-json').textContent; return JSON.parse(content || '{}') as CustomWorldProfile; } function getSceneActCard(index: number) { const card = screen.getAllByTestId('scene-act-card')[index]; if (!card) { throw new Error(`未找到第 ${index + 1} 个幕卡片`); } return card; } function CampEditorFlowHarness() { const [profile, setProfile] = useState({ ...createProfileWithLandmark(), camp: { id: 'custom-scene-camp', name: '潮灯居', description: '玩家最初落脚的旧灯塔内院。', dangerLevel: 'medium', imageSrc: '/generated-custom-world-scenes/original-camp.png', sceneNpcIds: ['story-1', 'story-2', 'story-3'], connections: [ { targetLandmarkId: 'landmark-1', relativePosition: 'north', summary: '北侧通往沉钟栈桥。', }, ], }, }); const [target, setTarget] = useState({ kind: 'camp', }); return ( <>
        {JSON.stringify(profile)}
      
{}} onEditTarget={setTarget} onProfileChange={setProfile} onDeleteStoryNpcs={() => {}} onDeleteLandmarks={() => {}} /> setTarget(null)} onProfileChange={setProfile} /> ); } function CoverEditorFlowHarness() { const [profile, setProfile] = useState({ ...createProfileWithLandmark(), cover: { sourceType: 'default', imageSrc: null, characterRoleIds: ['playable-1'], }, }); const [target, setTarget] = useState({ kind: 'cover', }); return ( <>
        {JSON.stringify(profile)}
      
setTarget(null)} onProfileChange={setProfile} /> ); } function readCoverHarnessProfile() { const content = screen.getByTestId('cover-profile-json').textContent; return JSON.parse(content || '{}') as CustomWorldProfile; } function readCampHarnessProfile() { const content = screen.getByTestId('camp-profile-json').textContent; return JSON.parse(content || '{}') as CustomWorldProfile; } test('playable角色打开AI工坊后不会自动关闭', async () => { const user = userEvent.setup(); const handleClose = vi.fn(); render( , ); await user.click(screen.getByRole('button', { name: 'AI生成' })); await waitFor(() => { expect(screen.getByText('AI角色形象生成')).toBeTruthy(); }); expect(handleClose).not.toHaveBeenCalled(); }); test('场景角色打开AI工坊后不会自动关闭', async () => { const user = userEvent.setup(); const handleClose = vi.fn(); render( , ); await user.click(screen.getByRole('button', { name: 'AI生成' })); await waitFor(() => { expect(screen.getByText('AI角色形象生成')).toBeTruthy(); }); expect(handleClose).not.toHaveBeenCalled(); }); test('可扮演角色未修改时右上角关闭不会弹确认', async () => { const user = userEvent.setup(); const handleClose = vi.fn(); render( , ); await user.click(screen.getByRole('button', { name: '关闭' })); expect(handleClose).toHaveBeenCalledTimes(1); expect(screen.queryByText('确认关闭')).toBeNull(); }); test('可扮演角色修改后右上角关闭才弹确认', async () => { const user = userEvent.setup(); const handleClose = vi.fn(); render( , ); const nameInput = screen.getByDisplayValue('沈砺'); await user.clear(nameInput); await user.type(nameInput, '沈砺·改'); await user.click(screen.getByRole('button', { name: '关闭' })); expect(handleClose).not.toHaveBeenCalled(); expect(screen.getAllByText('确认关闭').length).toBeGreaterThan(0); }); test('场景角色未修改时右上角关闭不会弹确认', async () => { const user = userEvent.setup(); const handleClose = vi.fn(); render( , ); await user.click(screen.getByRole('button', { name: '关闭' })); expect(handleClose).toHaveBeenCalledTimes(1); expect(screen.queryByText('确认关闭')).toBeNull(); }); test('场景角色修改后右上角关闭才弹确认', async () => { const user = userEvent.setup(); const handleClose = vi.fn(); render( , ); const nameInput = screen.getByDisplayValue('顾潮音'); await user.clear(nameInput); await user.type(nameInput, '顾潮音·改'); await user.click(screen.getByRole('button', { name: '关闭' })); expect(handleClose).not.toHaveBeenCalled(); expect(screen.getAllByText('确认关闭').length).toBeGreaterThan(0); }); test('可扮演角色列表使用缩略卡片并点击进入编辑', async () => { const user = userEvent.setup(); const handleEditTarget = vi.fn(); render( {}} onEditTarget={handleEditTarget} onProfileChange={vi.fn()} onDeleteStoryNpcs={() => {}} onDeleteLandmarks={() => {}} />, ); expect(screen.queryByText(/公开背景/u)).toBeNull(); await user.click(screen.getByRole('button', { name: /沈砺/u })); expect(handleEditTarget).toHaveBeenCalledWith({ kind: 'playable', mode: 'edit', id: 'playable-1', }); }); test('实体目录在空 id 列表项下不会触发重复 key 警告', () => { const consoleErrorSpy = vi .spyOn(console, 'error') .mockImplementation(() => undefined); render( {}} onEditTarget={() => {}} onProfileChange={vi.fn()} onDeleteStoryNpcs={() => {}} onDeleteLandmarks={() => {}} />, ); expect(screen.getByRole('button', { name: /沈砺/u })).toBeTruthy(); expect(screen.getByRole('button', { name: /闻潮/u })).toBeTruthy(); const duplicateKeyCalls = consoleErrorSpy.mock.calls.filter((call) => call.some( (arg) => typeof arg === 'string' && arg.includes('Encountered two children with the same key'), ), ); expect(duplicateKeyCalls).toHaveLength(0); }); test('场景图片保存后会同步更新编辑页和场景列表', async () => { const aiService = await import('../services/aiService'); vi.mocked(aiService.generateCustomWorldSceneImage).mockClear(); vi.mocked(aiService.generateCustomWorldSceneImage).mockResolvedValue({ imageSrc: '/generated-custom-world-scenes/updated-scene.png', assetId: 'asset-1', model: 'wan2.2-t2i-flash', size: '1280*720', taskId: 'task-1', prompt: '更新后的场景图', }); const user = userEvent.setup(); render(); const initialListImage = screen.getByRole('img', { name: '沉钟栈桥' }); expect(initialListImage.getAttribute('src')).toBe( '/generated-custom-world-scenes/original-scene.png', ); const firstActCard = getSceneActCard(0); await user.click(within(firstActCard).getByRole('button', { name: '配置背景' })); await waitFor(() => { expect(screen.getByText('配置幕背景:第1幕')).toBeTruthy(); }); expect(screen.queryByText('场景图片')).toBeNull(); expect(screen.queryByText('场景内 NPC')).toBeNull(); await user.click(screen.getByRole('button', { name: 'AI生成' })); await waitFor(() => { expect(screen.getByText('智能生成:沉钟栈桥')).toBeTruthy(); }); await user.click(screen.getByRole('button', { name: '开始生成' })); await waitFor(() => { expect(aiService.generateCustomWorldSceneImage).toHaveBeenCalledTimes(1); }); await user.click(screen.getByRole('button', { name: '保存' })); await waitFor(() => { expect(screen.queryByText('智能生成:沉钟栈桥')).toBeNull(); }); await waitFor(() => { expect(screen.getByRole('img', { name: '第1幕背景预览' }).getAttribute('src')).toBe( '/generated-custom-world-scenes/updated-scene.png', ); }); await user.click(screen.getByRole('button', { name: '保存背景' })); await waitFor(() => { expect(screen.queryByText('配置幕背景:第1幕')).toBeNull(); }); await user.click(screen.getByRole('button', { name: /保存修改/u })); await waitFor(() => { expect(screen.getByRole('img', { name: '沉钟栈桥' }).getAttribute('src')).toBe( '/generated-custom-world-scenes/updated-scene.png', ); }); const savedProfile = readLandmarkHarnessProfile(); expect(savedProfile.landmarks[0]?.imageSrc).toBe( '/generated-custom-world-scenes/updated-scene.png', ); }); test('开局场景图片保存后会同步更新编辑页和场景列表', async () => { const aiService = await import('../services/aiService'); vi.mocked(aiService.generateCustomWorldSceneImage).mockClear(); vi.mocked(aiService.generateCustomWorldSceneImage).mockResolvedValue({ imageSrc: '/generated-custom-world-scenes/updated-camp.png', assetId: 'asset-camp-1', model: 'wan2.2-t2i-flash', size: '1280*720', taskId: 'task-camp-1', prompt: '更新后的开局场景图', }); const user = userEvent.setup(); render(); const initialListImage = screen.getByRole('img', { name: '潮灯居' }); expect(initialListImage.getAttribute('src')).toBe( '/generated-custom-world-scenes/original-camp.png', ); const firstActCard = getSceneActCard(0); await user.click(within(firstActCard).getByRole('button', { name: '配置背景' })); await waitFor(() => { expect(screen.getByText('配置幕背景:第1幕')).toBeTruthy(); }); expect(screen.queryByText('场景图片')).toBeNull(); expect(screen.queryByText('场景内 NPC')).toBeNull(); await user.click(screen.getByRole('button', { name: 'AI生成' })); await waitFor(() => { expect(screen.getByText('智能生成:潮灯居')).toBeTruthy(); }); await user.click(screen.getByRole('button', { name: '开始生成' })); await waitFor(() => { expect(aiService.generateCustomWorldSceneImage).toHaveBeenCalledTimes(1); }); await user.click(screen.getByRole('button', { name: '保存' })); await waitFor(() => { expect(screen.queryByText('智能生成:潮灯居')).toBeNull(); }); await waitFor(() => { expect(screen.getByRole('img', { name: '第1幕背景预览' }).getAttribute('src')).toBe( '/generated-custom-world-scenes/updated-camp.png', ); }); await user.click(screen.getByRole('button', { name: '保存背景' })); await waitFor(() => { expect(screen.queryByText('配置幕背景:第1幕')).toBeNull(); }); await user.click(screen.getByRole('button', { name: /保存修改/u })); await waitFor(() => { expect(screen.getByRole('img', { name: '潮灯居' }).getAttribute('src')).toBe( '/generated-custom-world-scenes/updated-camp.png', ); }); const savedProfile = readCampHarnessProfile(); expect(savedProfile.camp?.imageSrc).toBe( '/generated-custom-world-scenes/updated-camp.png', ); }); test('开局场景在场景配置面板中与普通场景使用同级参数并可保存', async () => { const user = userEvent.setup(); render(); expect(screen.getByText('多幕配置')).toBeTruthy(); expect(screen.getByText('场景连接关系')).toBeTruthy(); expect(screen.queryByText('场景图片')).toBeNull(); expect(screen.queryByText('场景内 NPC')).toBeNull(); expect(screen.getAllByTestId('scene-act-card')).toHaveLength(3); const firstActCard = getSceneActCard(0); await user.click(within(firstActCard).getAllByTestId('scene-act-slot-button')[0]!); await waitFor(() => { expect(screen.getByText('配置角色:第1幕 · 主角色槽位')).toBeTruthy(); }); await user.click(screen.getByRole('button', { name: /闻雪汀/u })); await user.click(screen.getByRole('button', { name: '保存角色' })); await waitFor(() => { expect(screen.queryByText('配置角色:第1幕 · 主角色槽位')).toBeNull(); }); await user.click(screen.getByRole('button', { name: /保存修改/u })); await waitFor(() => { expect(screen.queryByText('编辑场景:潮灯居')).toBeNull(); }); const savedProfile = readCampHarnessProfile(); const openingSceneChapter = savedProfile.sceneChapterBlueprints?.find( (entry) => entry.sceneId === 'custom-scene-camp', ); expect(savedProfile.camp?.sceneNpcIds).toHaveLength(3); expect(savedProfile.camp?.sceneNpcIds).toEqual( expect.arrayContaining(['story-1', 'story-2', 'story-3']), ); expect(savedProfile.camp?.connections).toEqual([ { targetLandmarkId: 'landmark-1', relativePosition: 'north', summary: '北侧通往沉钟栈桥。', }, ]); expect(openingSceneChapter).toBeTruthy(); expect(openingSceneChapter?.acts[0]?.encounterNpcIds[0]).toBe('story-2'); expect(openingSceneChapter?.linkedLandmarkIds).toContain('custom-scene-camp'); }); test('场景编辑器会在场景内展示槽位化多幕配置并保存', async () => { const user = userEvent.setup(); render(); expect(screen.getByText('多幕配置')).toBeTruthy(); expect(screen.getAllByTestId('scene-act-card')).toHaveLength(3); expect(screen.queryByText('幕标题')).toBeNull(); expect(screen.queryByText('幕摘要')).toBeNull(); expect(screen.queryByText('幕目标')).toBeNull(); expect(screen.queryByText('过渡铺垫')).toBeNull(); const firstActCard = getSceneActCard(0); expect(within(firstActCard).getAllByTestId('scene-act-slot-button')).toHaveLength(3); await user.click(within(firstActCard).getByRole('button', { name: '配置背景' })); await waitFor(() => { expect(screen.getByText('配置幕背景:第1幕')).toBeTruthy(); }); const presetImage = screen.getByRole('img', { name: '幕背景预设 1' }); const presetSrc = presetImage.getAttribute('src'); const presetButton = presetImage.closest('button'); expect(presetButton).toBeTruthy(); if (!presetButton) { throw new Error('未找到幕背景预设按钮'); } await user.click(presetButton); await user.click(screen.getByRole('button', { name: '保存背景' })); await waitFor(() => { expect(screen.queryByText('配置幕背景:第1幕')).toBeNull(); }); await user.click(within(getSceneActCard(0)).getAllByTestId('scene-act-slot-button')[0]!); await waitFor(() => { expect(screen.getByText('配置角色:第1幕 · 主角色槽位')).toBeTruthy(); }); await user.click(screen.getByRole('button', { name: /闻雪汀/u })); await user.click(screen.getByRole('button', { name: '保存角色' })); await waitFor(() => { expect(screen.queryByText('配置角色:第1幕 · 主角色槽位')).toBeNull(); }); expect( within(getSceneActCard(0)).getByRole('button', { name: '配置第1个角色:闻雪汀', }), ).toBeTruthy(); await user.click(screen.getByRole('button', { name: /保存修改/u })); await waitFor(() => { expect(screen.queryByText('编辑场景:沉钟栈桥')).toBeNull(); }); const savedProfile = readLandmarkHarnessProfile(); const savedSceneChapter = savedProfile.sceneChapterBlueprints?.find( (entry) => entry.sceneId === 'landmark-1', ); expect(savedSceneChapter).toBeTruthy(); expect(savedSceneChapter?.acts[0]?.backgroundImageSrc).toBe(presetSrc); expect(savedSceneChapter?.acts[0]?.encounterNpcIds[0]).toBe('story-2'); expect(savedSceneChapter?.acts[0]?.primaryNpcId).toBe('story-2'); }); test('场景多幕支持新增删除和调序', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByRole('button', { name: '新增一幕' })); expect(screen.getAllByTestId('scene-act-card')).toHaveLength(4); const secondActCard = getSceneActCard(1); await user.click(within(secondActCard).getAllByTestId('scene-act-slot-button')[0]!); await waitFor(() => { expect(screen.getByText('配置角色:第2幕 · 主角色槽位')).toBeTruthy(); }); await user.click(screen.getByRole('button', { name: /谢孤灯/u })); await user.click(screen.getByRole('button', { name: '保存角色' })); await user.click(within(secondActCard).getByRole('button', { name: '下移' })); const fourthActCard = getSceneActCard(3); await user.click(within(fourthActCard).getByRole('button', { name: '删除' })); expect(screen.getAllByTestId('scene-act-card')).toHaveLength(3); await user.click(screen.getByRole('button', { name: /保存修改/u })); await waitFor(() => { expect(screen.queryByText('编辑场景:沉钟栈桥')).toBeNull(); }); const savedProfile = readLandmarkHarnessProfile(); const savedSceneChapter = savedProfile.sceneChapterBlueprints?.find( (entry) => entry.sceneId === 'landmark-1', ); expect(savedSceneChapter?.acts).toHaveLength(3); expect(savedSceneChapter?.acts[2]?.primaryNpcId).toBe('story-3'); }); test('场景幕预览会打开当前幕运行时面板', async () => { const user = userEvent.setup(); render(); await user.click(within(getSceneActCard(0)).getByRole('button', { name: '幕预览' })); await waitFor(() => { expect(screen.getByText('幕预览运行时')).toBeTruthy(); }); expect(screen.getAllByText('沉钟栈桥').length).toBeGreaterThan(0); await user.click(screen.getByRole('button', { name: '关闭预览' })); await waitFor(() => { expect(screen.queryByText('幕预览运行时')).toBeNull(); }); }); test('作品封面上传会先进入 16:9 裁剪面板再提交到后端', async () => { const uploadMock = vi .mocked(customWorldCoverAssetService.uploadCustomWorldCoverImage) .mockResolvedValue({ imageSrc: '/generated-custom-world-covers/world-1/uploaded/cover.webp', assetId: 'custom-cover-upload-1', sourceType: 'uploaded', }); class MockFileReader { result: string | null = null; error: Error | null = null; onload: null | (() => void) = null; onerror: null | (() => void) = null; readAsDataURL() { this.result = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO7+7aQAAAAASUVORK5CYII='; this.onload?.(); } } class MockImage { onload: null | (() => void) = null; onerror: null | (() => void) = null; naturalWidth = 1920; naturalHeight = 1080; set src(_value: string) { this.onload?.(); } } vi.stubGlobal('FileReader', MockFileReader as unknown as typeof FileReader); vi.stubGlobal('Image', MockImage as unknown as typeof Image); const user = userEvent.setup(); render(); const input = document.querySelector('input[type="file"]') as HTMLInputElement | null; expect(input).toBeTruthy(); if (!input) { throw new Error('未找到封面上传输入框'); } const file = new File(['cover'], 'cover.png', { type: 'image/png' }); await user.upload(input, file); await waitFor(() => { expect(screen.getByText('裁剪上传封面')).toBeTruthy(); }); await user.click(screen.getByRole('button', { name: '确认裁剪并上传' })); await waitFor(() => { expect(uploadMock).toHaveBeenCalledTimes(1); }); const uploadPayload = uploadMock.mock.calls[0]?.[0]; expect(uploadPayload?.worldName).toBe('潮雾群岛'); expect(uploadPayload?.cropRect.width).toBeGreaterThan(0); expect(uploadPayload?.cropRect.height).toBeGreaterThan(0); await waitFor(() => { expect(screen.queryByText('裁剪上传封面')).toBeNull(); }); await user.click(screen.getByRole('button', { name: /保存/u })); await waitFor(() => { expect(screen.queryByText('编辑作品封面')).toBeNull(); }); const savedProfile = readCoverHarnessProfile(); expect(savedProfile.cover?.sourceType).toBe('uploaded'); expect(savedProfile.cover?.imageSrc).toBe( '/generated-custom-world-covers/world-1/uploaded/cover.webp', ); });