/* @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,
);
});
});
});