Files
Genarrative/src/components/bark-battle-creation/BarkBattleGeneratingView.test.tsx
高物 50a0d6f982 Refine creation tab UX, generation flow, and bindings
Large changes across frontend, backend and docs to align creation-tab and generation-page behavior with new product UI/UX and Spacetime bindings. Updated hermes decision-log and pitfalls with concrete rules (banner carousel, font sizing, unread-dot tokens, template-card layout, direct card->entry routing, separation of account balance vs prize pools, removal of global page card shell, generation progress milestones and unified circular progress, and background video handling). Added GenerationProgressHero component and media assets, plus generation-related UI/tests updates (CustomWorldGenerationView, BarkBattleGeneratingView, creation hub/cards, platform entry routing, index tests). Backend and contract updates include new category fields in admin API types and admin UI form/list, spacetime-client/module/migration changes and generated bindings script. Misc: many tests adjusted, new docs and plan files added, and several server-rs crate changes to support the updated creation/ generation workflows.
2026-05-25 00:41:30 +08:00

440 lines
14 KiB
TypeScript

/* @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: () => <div></div>,
}));
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<BarkBattleImageGenerationBatchResult>((resolve) => {
resolveGeneration = resolve;
}),
);
vi.mocked(updateBarkBattleDraftConfig).mockResolvedValue({
...draft,
configVersion: 3,
updatedAt: '2026-05-14T10:01:00.000Z',
});
const { container } = render(
<BarkBattleGeneratingView
draft={draft}
onBack={() => {}}
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(
<BarkBattleGeneratingView
draft={draft}
onBack={() => {}}
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(
<BarkBattleGeneratingView
draft={draft}
onBack={() => {}}
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(
<BarkBattleGeneratingView
draft={draft}
onBack={() => {}}
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(
<BarkBattleGeneratingView
draft={draft}
onBack={() => {}}
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,
);
});
});
});