/* @vitest-environment jsdom */ import { render, screen, waitFor } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { type BarkBattleImageGenerationBatchResult, generateAllBarkBattleImageAssets, updateBarkBattleDraftConfig, } from '../../services/bark-battle-creation'; import { BarkBattleGeneratingView } from './BarkBattleGeneratingView'; vi.mock('../../services/bark-battle-creation', () => ({ generateAllBarkBattleImageAssets: vi.fn(), updateBarkBattleDraftConfig: vi.fn(), })); vi.mock('./BarkBattlePreviewCard', () => ({ BarkBattlePreviewCard: () =>
汪汪声浪预览
, })); const draft = { draftId: 'bark-battle-draft-1', workId: 'BB-12345678', title: '汪汪冠军杯', description: '', themeDescription: '霓虹公园擂台', playerImageDescription: '红围巾柴犬', opponentImageDescription: '蓝头带哈士奇', difficultyPreset: 'normal' as const, configVersion: 2, rulesetVersion: 'bark-battle-ruleset-v1', updatedAt: '2026-05-14T10:00:00.000Z', }; describe('BarkBattleGeneratingView', () => { it('renders all generation slots while parallel generation is still running', async () => { const onComplete = vi.fn(); let resolveGeneration: ( result: BarkBattleImageGenerationBatchResult, ) => void = () => {}; vi.mocked(generateAllBarkBattleImageAssets).mockReturnValue( new Promise((resolve) => { resolveGeneration = resolve; }), ); vi.mocked(updateBarkBattleDraftConfig).mockResolvedValue({ ...draft, configVersion: 3, updatedAt: '2026-05-14T10:01:00.000Z', }); const { container } = render( {}} onComplete={onComplete} onError={() => {}} />, ); expect(container.firstChild).toBeTruthy(); expect((container.firstChild as HTMLElement).className).toContain('z-[1]'); expect(screen.getByText('总进度')).toBeTruthy(); expect(screen.getByText('总进度').className).toContain('text-[9px]'); const pageVideo = screen.getByTestId( 'generation-page-background-video', ) as HTMLVideoElement; expect(pageVideo.parentElement?.className).toContain('z-0'); expect(pageVideo.parentElement?.className).toContain('bg-transparent'); expect(pageVideo.parentElement?.className).not.toContain('bg-[#fff4ea]'); expect((container.firstChild as HTMLElement).contains(pageVideo)).toBe( true, ); expect(pageVideo.autoplay).toBe(true); expect(pageVideo.loop).toBe(true); expect(pageVideo.muted).toBe(true); expect(pageVideo.playsInline).toBe(true); expect(pageVideo.getAttribute('preload')).toBe('auto'); expect( document.querySelector( 'video[data-testid="generation-page-background-video"] source[type="video/mp4"]', ), ).toBeTruthy(); expect(screen.getByRole('button', { name: '返回编辑' }).className).toContain( 'text-xs', ); expect(screen.getByText('生成中').className).toContain('text-[11px]'); expect(screen.getByText('当前步骤')).toBeTruthy(); expect(screen.getByText('当前步骤').className).toContain('text-[10px]'); expect(screen.getByTestId('generation-hero-wait-card').className).toContain( 'text-center', ); expect(screen.getByTestId('generation-hero-elapsed-card').className).toContain( 'text-center', ); expect(screen.getByTestId('generation-hero-wait-card').className).toContain( 'bg-white/58', ); expect(screen.getByTestId('generation-hero-elapsed-card').className).toContain( 'bg-white/58', ); expect(screen.getByText('预计等待').className).toContain('text-[9px]'); expect(screen.getByText('已耗时').className).toContain('text-[9px]'); expect(screen.getByText('预计等待').parentElement?.className).toContain( 'justify-center', ); expect(screen.getByText('已耗时').parentElement?.className).toContain( 'justify-center', ); expect(screen.getByText('3 分钟')).toBeTruthy(); expect(screen.getByText('1 秒')).toBeTruthy(); expect(screen.queryByText('预计还需 3 分钟')).toBeNull(); expect(screen.queryByText('已耗时 1 秒')).toBeNull(); expect(screen.getByTestId('generation-hero-progress-content').className).toContain( 'justify-start', ); expect(screen.getByTestId('generation-hero-progress-content').className).toContain( 'pt-[4%]', ); expect(screen.getByText('玩家形象')).toBeTruthy(); expect(screen.getByText('进行中 36%')).toBeTruthy(); expect(screen.getByText('进行中 36%').className).toContain('text-[11px]'); expect(screen.getByText('总进度').className).toContain('text-[9px]'); expect(screen.getByText('0%').className).toContain('text-[1.15rem]'); expect( screen .getByRole('progressbar', { name: '汪汪声浪素材生成进度' }) .className, ).toContain('w-[min(35rem,94vw)]'); expect( screen .getByRole('progressbar', { name: '汪汪声浪素材生成进度' }) .className, ).toContain('sm:w-[52rem]'); expect( screen .getByRole('progressbar', { name: '汪汪声浪素材生成进度' }) .getAttribute('aria-valuenow'), ).toBe('0'); expect( screen .getByRole('progressbar', { name: '汪汪声浪素材生成进度' }) .getAttribute('data-ring-start-degrees'), ).toBe('225'); expect( screen .getByRole('progressbar', { name: '汪汪声浪素材生成进度' }) .getAttribute('data-ring-sweep-degrees'), ).toBe('270'); expect( screen .getByRole('progressbar', { name: '汪汪声浪素材生成进度' }) .getAttribute('data-ring-gap-degrees'), ).toBe('90'); expect( screen .getByRole('progressbar', { name: '汪汪声浪素材生成进度' }) .getAttribute('data-ring-fill-degrees'), ).toBe('0'); expect(screen.getByTestId('generation-hero-progress-ring').tagName).toBe( 'svg', ); expect( screen .getByTestId('generation-hero-progress-ring') .getAttribute('viewBox'), ).toBe('0 0 400 400'); expect( screen .getByTestId('generation-hero-progress-ring-track') .getAttribute('r'), ).toBe('166'); expect( screen .getByTestId('generation-hero-progress-ring-track') .getAttribute('stroke-width'), ).toBe('18'); expect( screen .getByTestId('generation-hero-progress-ring-fill') .getAttribute('stroke-dasharray'), ).toMatch(/^0\.00 1043\.\d{2}$/u); expect( screen.getByRole('progressbar', { name: '玩家形象 进度' }), ).toBeTruthy(); expect( screen .getByRole('progressbar', { name: '玩家形象 进度' }) .getAttribute('aria-valuenow'), ).toBe('36'); expect( screen.getByTestId('generation-current-step-card').className, ).toContain('bg-white/58'); expect(screen.getByText('预览信息').className).toContain('text-[13px]'); expect(screen.queryByText('对手形象')).toBeNull(); expect(screen.queryByText('竞技背景')).toBeNull(); expect(onComplete).not.toHaveBeenCalled(); resolveGeneration({ assets: { 'player-character': { imageSrc: '/generated-bark-battle/player.png', assetId: 'asset-player', model: 'gpt-image-2-all', size: '1024*1024', taskId: 'task-player', prompt: 'player', }, 'opponent-character': { imageSrc: '/generated-bark-battle/opponent.png', assetId: 'asset-opponent', model: 'gpt-image-2-all', size: '1024*1024', taskId: 'task-opponent', prompt: 'opponent', }, 'ui-background': { imageSrc: '/generated-bark-battle/background.png', assetId: 'asset-background', model: 'gpt-image-2-all', size: '1024*1792', taskId: 'task-background', prompt: 'background', }, }, failures: {}, }); await waitFor(() => expect(onComplete).toHaveBeenCalled()); }); it('persists generated image assets before entering result view', async () => { const onComplete = vi.fn(); const onError = vi.fn(); vi.mocked(generateAllBarkBattleImageAssets).mockResolvedValue({ assets: { 'player-character': { imageSrc: '/generated-bark-battle/player.png', assetId: 'asset-player', model: 'gpt-image-2-all', size: '1024*1024', taskId: 'task-player', prompt: 'player', }, 'opponent-character': { imageSrc: '/generated-bark-battle/opponent.png', assetId: 'asset-opponent', model: 'gpt-image-2-all', size: '1024*1024', taskId: 'task-opponent', prompt: 'opponent', }, 'ui-background': { imageSrc: '/generated-bark-battle/background.png', assetId: 'asset-background', model: 'gpt-image-2-all', size: '1024*1792', taskId: 'task-background', prompt: 'background', }, }, failures: {}, }); vi.mocked(updateBarkBattleDraftConfig).mockResolvedValue({ ...draft, configVersion: 3, updatedAt: '2026-05-14T10:01:00.000Z', }); render( {}} onComplete={onComplete} onError={onError} />, ); await waitFor(() => { expect(updateBarkBattleDraftConfig).toHaveBeenCalledWith( expect.objectContaining({ draftId: 'bark-battle-draft-1', workId: 'BB-12345678', playerCharacterImageSrc: '/generated-bark-battle/player.png', opponentCharacterImageSrc: '/generated-bark-battle/opponent.png', uiBackgroundImageSrc: '/generated-bark-battle/background.png', }), ); }); expect(onComplete).toHaveBeenCalledWith( expect.objectContaining({ configVersion: 3, playerCharacterImageSrc: '/generated-bark-battle/player.png', opponentCharacterImageSrc: '/generated-bark-battle/opponent.png', uiBackgroundImageSrc: '/generated-bark-battle/background.png', }), false, ); expect(onError).toHaveBeenCalledWith(null); }); it('enters result view with partial failure when only part of the images are generated', async () => { const onComplete = vi.fn(); vi.mocked(generateAllBarkBattleImageAssets).mockResolvedValue({ assets: { 'player-character': { imageSrc: '/generated-bark-battle/player.png', assetId: 'asset-player', model: 'gpt-image-2-all', size: '1024*1024', taskId: 'task-player', prompt: 'player', }, }, failures: { 'opponent-character': '泥点不足,本次需要 1 泥点,当前 0 泥点。', 'ui-background': '场景图片生成失败:上游超时', }, }); vi.mocked(updateBarkBattleDraftConfig).mockResolvedValue({ ...draft, playerCharacterImageSrc: '/generated-bark-battle/player.png', configVersion: 3, }); render( {}} onComplete={onComplete} onError={() => {}} />, ); await waitFor(() => { expect(onComplete).toHaveBeenCalledWith( expect.objectContaining({ playerCharacterImageSrc: '/generated-bark-battle/player.png', }), true, ); }); }); it('still enters result view when generated assets cannot be persisted', async () => { const onComplete = vi.fn(); const onError = vi.fn(); vi.mocked(generateAllBarkBattleImageAssets).mockResolvedValue({ assets: { 'player-character': { imageSrc: '/generated-bark-battle/player.png', assetId: 'asset-player', model: 'gpt-image-2-all', size: '1024*1024', taskId: 'task-player', prompt: 'player', }, 'opponent-character': { imageSrc: '/generated-bark-battle/opponent.png', assetId: 'asset-opponent', model: 'gpt-image-2-all', size: '1024*1024', taskId: 'task-opponent', prompt: 'opponent', }, 'ui-background': { imageSrc: '/generated-bark-battle/background.png', assetId: 'asset-background', model: 'gpt-image-2-all', size: '1024*1792', taskId: 'task-background', prompt: 'background', }, }, failures: {}, }); vi.mocked(updateBarkBattleDraftConfig).mockRejectedValue( new Error('保存超时'), ); render( {}} onComplete={onComplete} onError={onError} />, ); await waitFor(() => { expect(onComplete).toHaveBeenCalledWith( expect.objectContaining({ playerCharacterImageSrc: '/generated-bark-battle/player.png', opponentCharacterImageSrc: '/generated-bark-battle/opponent.png', uiBackgroundImageSrc: '/generated-bark-battle/background.png', }), true, ); }); expect(onError).toHaveBeenCalledWith('保存超时'); }); it('shows generation failures and enters result view when no image asset is generated', async () => { const onComplete = vi.fn(); const onError = vi.fn(); vi.mocked(generateAllBarkBattleImageAssets).mockResolvedValue({ assets: {}, failures: { 'player-character': '泥点不足,本次需要 1 泥点,当前 0 泥点。', 'opponent-character': '泥点不足,本次需要 1 泥点,当前 0 泥点。', 'ui-background': '场景图片生成失败:上游超时', }, }); vi.mocked(updateBarkBattleDraftConfig).mockResolvedValue(draft); render( {}} onComplete={onComplete} onError={onError} />, ); await waitFor(() => { expect(onError).toHaveBeenCalledWith( '泥点不足,本次需要 1 泥点,当前 0 泥点。', ); expect(onComplete).toHaveBeenCalledWith( expect.objectContaining({ draftId: draft.draftId, workId: draft.workId, title: draft.title, }), true, ); }); }); });