/* @vitest-environment jsdom */ import { render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { expect, test, vi } from 'vitest'; import { buildVisualNovelForbiddenCopyPattern } from '../visual-novel-runtime/visualNovelForbiddenCopy'; import { mockVisualNovelDraft } from '../visual-novel-runtime/visualNovelMockData'; import { VisualNovelResultView } from './VisualNovelResultView'; vi.mock('../../services/visual-novel-creation', () => ({ createVisualNovelBackgroundMusicTask: vi.fn(), createVisualNovelSoundEffectTask: vi.fn(), generateVisualNovelImageAsset: vi.fn(), buildVisualNovelImageGenerationPrompt: vi.fn(() => '默认图片提示词'), listVisualNovelHistoryAssets: vi.fn().mockResolvedValue([]), publishVisualNovelBackgroundMusicAsset: vi.fn(), publishVisualNovelSoundEffectAsset: vi.fn(), uploadVisualNovelAsset: vi.fn(), })); vi.mock('../../services/assetReadUrlService', () => ({ resolveAssetReadUrl: vi.fn().mockResolvedValue(''), shouldResolveAssetReadUrl: vi.fn(() => false), })); test('visual novel profile tab uses PlatformSubpanel shells', () => { const { container } = render( {}} />, ); const profilePanels = Array.from( container.querySelectorAll('section.platform-subpanel'), ).filter((panel) => panel.className.includes('rounded-[1.35rem]')); const workTitlePanel = screen.getByText('作品名称').closest('section'); expect(profilePanels).toHaveLength(2); for (const panel of profilePanels) { expect(panel.className).toContain('p-4'); } expect(workTitlePanel?.className).toContain('platform-subpanel'); expect(workTitlePanel?.className).toContain('rounded-[1.35rem]'); }); test('visual novel profile media previews use PlatformMediaFrame aspects', () => { const draftWithCover = { ...mockVisualNovelDraft, coverImageSrc: '/visual-novel/cover.png', }; const { container } = render( {}} />, ); const coverImages = Array.from( container.querySelectorAll('img[src="/visual-novel/cover.png"]'), ); const mediaFrameClassNames = coverImages.map( (image) => image.closest('div.relative')?.className ?? '', ); expect(coverImages).toHaveLength(2); expect(mediaFrameClassNames).toContainEqual( expect.stringContaining('aspect-[4/3]'), ); expect(mediaFrameClassNames).toContainEqual( expect.stringContaining('aspect-[16/9]'), ); const assetPreviewFrameClassName = mediaFrameClassNames.find((className) => className.includes('aspect-[16/9]'), ); expect(assetPreviewFrameClassName).toBeTruthy(); expect(assetPreviewFrameClassName).not.toContain( 'bg-[var(--platform-subpanel-fill)]', ); }); test('visual novel upload-only asset picker uses PlatformEmptyState chrome', async () => { const user = userEvent.setup(); render( {}} />, ); await user.click(screen.getByRole('button', { name: '封面' })); const pickerDialog = screen.getByRole('dialog', { name: '封面' }); const uploadOnlyEmptyState = within(pickerDialog).getByText('选择本地文件上传'); expect(uploadOnlyEmptyState.className).toContain('border-dashed'); expect(uploadOnlyEmptyState.className).toContain('rounded-[1.35rem]'); expect(uploadOnlyEmptyState.className).toContain('min-h-24'); expect(uploadOnlyEmptyState.className).toContain( 'text-[var(--platform-text-base)]', ); }); test('visual novel entity list items use interactive PlatformSubpanel shells', async () => { const user = userEvent.setup(); render( {}} />, ); await user.click(screen.getByRole('button', { name: '角色' })); const characterCard = screen.getByRole('button', { name: /林遥/u }); expect(characterCard.className).toContain('platform-subpanel'); expect(characterCard.className).toContain('hover:bg-white'); expect(characterCard.className).toContain('disabled:cursor-not-allowed'); expect(characterCard.className).toContain('rounded-[1.25rem]'); }); test('visual novel empty entity list uses PlatformEmptyState shell', async () => { const user = userEvent.setup(); const emptyCharacterDraft = { ...mockVisualNovelDraft, characters: [], }; render( {}} />, ); await user.click(screen.getByRole('button', { name: '角色' })); const emptyPanel = screen .getByText('暂无角色') .closest('.platform-empty-state'); expect(emptyPanel?.className).toContain('platform-empty-state'); expect(emptyPanel?.className).toContain('bg-white/74'); expect(emptyPanel?.className).toContain('rounded-[1.25rem]'); expect(emptyPanel?.className).toContain('min-h-32'); expect(emptyPanel?.className).toContain( 'text-[var(--platform-text-soft)]', ); }); test('visual novel result opens complex editors as a dialog', async () => { const user = userEvent.setup(); render( {}} />, ); await user.click(screen.getByRole('button', { name: '角色' })); await user.click(screen.getByRole('button', { name: /林遥/u })); const dialog = screen.getByRole('dialog', { name: '林遥' }); expect(within(dialog).getByDisplayValue('林遥')).toBeTruthy(); expect(screen.queryByText(buildVisualNovelForbiddenCopyPattern())).toBeNull(); }); test('visual novel result exposes test run action with current draft', async () => { const user = userEvent.setup(); const onStartTestRun = vi.fn(); render( {}} onStartTestRun={onStartTestRun} />, ); await user.click(screen.getByRole('button', { name: '试玩' })); expect(onStartTestRun).toHaveBeenCalledTimes(1); expect(onStartTestRun.mock.calls[0]?.[0].workTitle).toBe('雪线电台'); }); test('visual novel result sends edited character draft to save and test run', async () => { const user = userEvent.setup(); const onSaveDraft = vi.fn(); const onStartTestRun = vi.fn(); render( {}} onSaveDraft={onSaveDraft} onStartTestRun={onStartTestRun} />, ); await user.click(screen.getByRole('button', { name: '角色' })); await user.click(screen.getByRole('button', { name: /林遥/u })); const dialog = screen.getByRole('dialog', { name: '林遥' }); const nameInput = within(dialog).getByDisplayValue('林遥'); await user.clear(nameInput); await user.type(nameInput, '林遥改'); await user.click(within(dialog).getByRole('button', { name: '关闭' })); const saveButtons = screen.getAllByRole('button', { name: '保存草稿' }); await user.click(saveButtons[1]!); await user.click(screen.getByRole('button', { name: '试玩' })); expect(onSaveDraft.mock.calls[0]?.[0].characters[0]?.name).toBe('林遥改'); expect(onStartTestRun.mock.calls[0]?.[0].characters[0]?.name).toBe('林遥改'); }); test('visual novel result uploads scene and character assets into platform references', async () => { const user = userEvent.setup(); const onSaveDraft = vi.fn(); const uploadMock = vi.mocked( await import('../../services/visual-novel-creation'), ).uploadVisualNovelAsset; uploadMock.mockResolvedValue({ assetObjectId: 'asset-scene-1', assetKind: 'scene_image', objectKey: 'generated-custom-world-scenes/vn-profile/scene-1/background.png', imageSrc: '/generated-custom-world-scenes/vn-profile/scene-1/background.png', }); render( {}} onSaveDraft={onSaveDraft} />, ); await user.click(screen.getByRole('button', { name: '场景' })); await user.click(screen.getByRole('button', { name: /风雪站台/u })); const dialog = screen.getByRole('dialog', { name: '风雪站台' }); const backgroundButtons = within(dialog).getAllByRole('button', { name: '背景图', }); await user.click(backgroundButtons[0]!); const fileInput = within( screen.getByRole('dialog', { name: '背景图' }), ).getByLabelText('上传背景图文件') as HTMLInputElement; await user.upload( fileInput, new File(['image-bytes'], 'scene.png', { type: 'image/png' }), ); await user.click(within(dialog).getByRole('button', { name: '关闭' })); await user.click(screen.getAllByRole('button', { name: '保存草稿' })[1]!); expect(onSaveDraft).toHaveBeenCalled(); expect( onSaveDraft.mock.calls[0]?.[0].scenes[0]?.backgroundImageSrc, ).toContain('/generated-custom-world-scenes/'); }); test('visual novel result generates scene background from asset picker', async () => { const user = userEvent.setup(); const onSaveDraft = vi.fn(); const visualNovelCreation = await import( '../../services/visual-novel-creation' ); const generateImageMock = vi.mocked( visualNovelCreation.generateVisualNovelImageAsset, ); generateImageMock.mockResolvedValue({ imageSrc: '/generated-custom-world-scenes/vn-profile/scene-ai.webp', assetId: 'asset-scene-ai', model: 'test-image-model', size: '1280*720', taskId: 'task-scene-ai', prompt: '默认图片提示词', }); render( {}} onSaveDraft={onSaveDraft} />, ); await user.click(screen.getByRole('button', { name: '场景' })); await user.click(screen.getByRole('button', { name: /风雪站台/u })); const editorDialog = screen.getByRole('dialog', { name: '风雪站台' }); await user.click( within(editorDialog).getAllByRole('button', { name: '背景图' })[0]!, ); await user.click( within(screen.getByRole('dialog', { name: '背景图' })).getByRole('button', { name: 'AI生成', }), ); await user.click(within(editorDialog).getByRole('button', { name: '关闭' })); await user.click(screen.getAllByRole('button', { name: '保存草稿' })[1]!); expect(generateImageMock).toHaveBeenCalledWith( expect.objectContaining({ kind: 'scene_background', scene: expect.objectContaining({ sceneId: mockVisualNovelDraft.scenes[0]?.sceneId, }), prompt: '默认图片提示词', }), ); expect(onSaveDraft.mock.calls[0]?.[0].scenes[0]?.backgroundImageSrc).toBe( '/generated-custom-world-scenes/vn-profile/scene-ai.webp', ); });