fix: polish bark battle creation flow

This commit is contained in:
kdletters
2026-05-22 05:00:07 +08:00
parent 01da85a577
commit bf82f04b64
73 changed files with 9362 additions and 2663 deletions

View File

@@ -5,6 +5,7 @@ import userEvent from '@testing-library/user-event';
import { useState } from 'react';
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { CreativeAgentSessionSnapshot } from '../../../packages/shared/src/contracts/creativeAgent';
import type {
@@ -43,7 +44,11 @@ import { ApiClientError } from '../../services/apiClient';
import type { AuthUser } from '../../services/authService';
import {
createBarkBattleDraft,
generateAllBarkBattleImageAssets,
listBarkBattleGallery,
listBarkBattleWorks,
publishBarkBattleWork,
updateBarkBattleDraftConfig,
} from '../../services/bark-battle-creation';
import {
createBigFishCreationSession,
@@ -475,8 +480,12 @@ vi.mock('../../services/big-fish-runtime', () => ({
vi.mock('../../services/bark-battle-creation', () => ({
createBarkBattleDraft: vi.fn(),
generateAllBarkBattleImageAssets: vi.fn(),
listBarkBattleGallery: vi.fn(),
listBarkBattleWorks: vi.fn(),
publishBarkBattleWork: vi.fn(),
regenerateBarkBattleImageAsset: vi.fn(),
updateBarkBattleDraftConfig: vi.fn(),
uploadBarkBattleAsset: vi.fn(),
}));
@@ -1000,11 +1009,10 @@ vi.mock('../bark-battle-creation/BarkBattleConfigEditor', () => ({
onPreview: (payload: {
title: string;
description: string;
themePreset: string;
playerDogSkinPreset: string;
opponentDogSkinPreset: string;
themeDescription: string;
playerImageDescription: string;
opponentImageDescription: string;
difficultyPreset: 'normal';
leaderboardEnabled: boolean;
}) => void;
}) => (
<div className="bark-battle-config-editor-mock">
@@ -1026,11 +1034,10 @@ vi.mock('../bark-battle-creation/BarkBattleConfigEditor', () => ({
onPreview({
title: '汪汪测试杯',
description: '',
themePreset: 'sunny-yard',
playerDogSkinPreset: 'corgi',
opponentDogSkinPreset: 'husky',
themeDescription: '阳光草坪声浪竞技场',
playerImageDescription: '戴红色围巾的柯基选手',
opponentImageDescription: '蓝色护目镜哈士奇对手',
difficultyPreset: 'normal',
leaderboardEnabled: true,
});
}}
>
@@ -1122,14 +1129,27 @@ vi.mock('../../games/bark-battle/ui/BarkBattleRuntimeShell', () => ({
BarkBattleRuntimeShell: ({
title,
workId,
runtimeMode,
publishedConfig,
onExit,
}: {
title?: string;
workId?: string;
runtimeMode?: string;
publishedConfig?: { workId?: string; playerCharacterImageSrc?: string | null } | null;
onExit?: () => void;
}) => (
<div className="bark-battle-runtime-shell-mock">
<div>{title ?? '未命名'} / {workId ?? 'missing-work'}</div>
<div data-testid="bark-battle-runtime-mode">
{runtimeMode ?? 'missing-mode'}
</div>
<div data-testid="bark-battle-runtime-work-id">
{publishedConfig?.workId ?? 'missing-config-work'}
</div>
<div data-testid="bark-battle-runtime-player-src">
{publishedConfig?.playerCharacterImageSrc ?? 'missing-player-src'}
</div>
<button type="button" onClick={onExit}>
</button>
@@ -1311,6 +1331,34 @@ function buildMockBabyObjectMatchDraft(
};
}
function buildMockBarkBattleWork(
overrides: Partial<BarkBattleWorkSummary> = {},
): BarkBattleWorkSummary {
return {
workId: 'BB-C661A45F',
draftId: 'bark-battle-draft-public-1',
ownerUserId: 'user-1',
authorDisplayName: '测试玩家',
title: '汪汪公开杯',
summary: '',
themeDescription: '霓虹城市公园里的声浪擂台',
playerImageDescription: '戴红围巾的柴犬主角',
opponentImageDescription: '戴蓝色头带的哈士奇对手',
playerCharacterImageSrc: '/generated-bark-battle/player.png',
opponentCharacterImageSrc: '/generated-bark-battle/opponent.png',
uiBackgroundImageSrc: '/generated-bark-battle/background.png',
difficultyPreset: 'normal',
status: 'published',
generationStatus: 'ready',
publishReady: true,
playCount: 0,
finishCount: 0,
updatedAt: '2026-05-14T10:00:00.000Z',
publishedAt: '2026-05-14T10:00:00.000Z',
...overrides,
};
}
function buildMockSquareHoleAgentSession(
overrides: Partial<
Parameters<typeof buildMockSquareHoleAgentSessionImpl>[0]
@@ -2837,15 +2885,61 @@ beforeEach(() => {
workId: 'bark-battle-work-1',
title: '汪汪测试杯',
description: '',
themePreset: 'sunny-yard',
playerDogSkinPreset: 'corgi',
opponentDogSkinPreset: 'husky',
themeDescription: '阳光草坪声浪竞技场',
playerImageDescription: '戴红色围巾的柯基选手',
opponentImageDescription: '蓝色护目镜哈士奇对手',
difficultyPreset: 'normal',
leaderboardEnabled: true,
configVersion: 1,
rulesetVersion: 'bark-battle-ruleset-v1',
updatedAt: '2026-05-14T10:00:00.000Z',
});
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).mockImplementation(async (payload) => ({
draftId: payload.draftId,
workId: payload.workId ?? 'bark-battle-work-1',
title: payload.title,
description: payload.description,
themeDescription: payload.themeDescription,
playerImageDescription: payload.playerImageDescription,
opponentImageDescription: payload.opponentImageDescription,
playerCharacterImageSrc: payload.playerCharacterImageSrc,
opponentCharacterImageSrc: payload.opponentCharacterImageSrc,
uiBackgroundImageSrc: payload.uiBackgroundImageSrc,
difficultyPreset: payload.difficultyPreset,
configVersion: (payload.configVersion ?? 1) + 1,
rulesetVersion: payload.rulesetVersion ?? 'bark-battle-ruleset-v1',
updatedAt: '2026-05-14T10:01:00.000Z',
}));
vi.mocked(listBarkBattleWorks).mockResolvedValue({ items: [] });
vi.mocked(listBarkBattleGallery).mockResolvedValue({ items: [] });
vi.mocked(publishBarkBattleWork).mockResolvedValue({
workId: 'bark-battle-work-1',
draftId: 'bark-battle-draft-1',
@@ -2854,11 +2948,10 @@ beforeEach(() => {
playTypeId: 'bark-battle',
title: '汪汪测试杯',
description: '',
themePreset: 'sunny-yard',
playerDogSkinPreset: 'corgi',
opponentDogSkinPreset: 'husky',
themeDescription: '阳光草坪声浪竞技场',
playerImageDescription: '戴红色围巾的柯基选手',
opponentImageDescription: '蓝色护目镜哈士奇对手',
difficultyPreset: 'normal',
leaderboardEnabled: true,
updatedAt: '2026-05-14T10:00:00.000Z',
publishedAt: '2026-05-14T10:00:00.000Z',
});
@@ -3233,6 +3326,9 @@ test('create tab shows template tabs and embeds puzzle form by default', async (
await openCreateTemplateHub(user);
expect(screen.getByRole('tablist', { name: '选择模板' })).toBeTruthy();
expect(screen.getByRole('tablist', { name: '选择模板' }).className).toContain(
'scroll-px-3',
);
expect(
screen.getByRole('tab', { name: '拼图' }).getAttribute('aria-selected'),
).toBe('true');
@@ -3309,7 +3405,7 @@ test('create tab switches bark battle into the embedded config form', async () =
expect(publishBarkBattleWork).not.toHaveBeenCalled();
});
test('bark battle draft result can test before publish and return to the embedded form', async () => {
test('bark battle draft result can test before publish and publish to work detail', async () => {
const user = userEvent.setup();
render(<TestWrapper withAuth />);
@@ -3321,11 +3417,21 @@ test('bark battle draft result can test before publish and return to the embedde
expect(createBarkBattleDraft).toHaveBeenCalledWith({
title: '汪汪测试杯',
description: '',
themePreset: 'sunny-yard',
playerDogSkinPreset: 'corgi',
opponentDogSkinPreset: 'husky',
themeDescription: '阳光草坪声浪竞技场',
playerImageDescription: '戴红色围巾的柯基选手',
opponentImageDescription: '蓝色护目镜哈士奇对手',
difficultyPreset: 'normal',
leaderboardEnabled: true,
});
await waitFor(() => {
expect(updateBarkBattleDraftConfig).toHaveBeenCalledWith(
expect.objectContaining({
draftId: 'bark-battle-draft-1',
workId: 'bark-battle-work-1',
playerCharacterImageSrc: '/generated-bark-battle/player.png',
opponentCharacterImageSrc: '/generated-bark-battle/opponent.png',
uiBackgroundImageSrc: '/generated-bark-battle/background.png',
}),
);
});
expect(await screen.findByText(/汪汪声浪结果页:汪汪测试杯/u)).toBeTruthy();
expect(await screen.findByText('作品IDbark-battle-work-1')).toBeTruthy();
@@ -3345,17 +3451,154 @@ test('bark battle draft result can test before publish and return to the embedde
workId: 'bark-battle-work-1',
publishedSnapshot: expect.objectContaining({
title: '汪汪测试杯',
leaderboardEnabled: true,
themeDescription: '阳光草坪声浪竞技场',
playerImageDescription: '戴红色围巾的柯基选手',
opponentImageDescription: '蓝色护目镜哈士奇对手',
}),
});
expect(await screen.findByText(/汪汪声浪运行态:汪汪测试杯/u)).toBeTruthy();
await waitFor(() => {
expect(window.location.pathname).toBe('/works/detail');
expect(window.location.search).toBe('?work=BB-TLEWORK1');
});
expect(await screen.findByText('分享给朋友')).toBeTruthy();
expect(screen.getByText(/作品号BB-TLEWORK1/u)).toBeTruthy();
expect(screen.queryByText(/汪汪声浪运行态:汪汪测试杯/u)).toBeNull();
});
await user.click(screen.getByRole('button', { name: '返回配置' }));
test('direct bark battle runtime public code opens published runtime', async () => {
const publicWork = buildMockBarkBattleWork();
vi.mocked(listBarkBattleGallery).mockResolvedValueOnce({
items: [publicWork],
});
window.history.replaceState(
null,
'',
'/runtime/bark-battle?work=BB-C661A45F',
);
render(<TestWrapper withAuth />);
expect(await screen.findByText(/汪汪声浪运行态:汪汪公开杯/u)).toBeTruthy();
expect(screen.getByTestId('bark-battle-runtime-mode').textContent).toBe(
'published',
);
expect(screen.getByTestId('bark-battle-runtime-work-id').textContent).toBe(
'BB-C661A45F',
);
expect(screen.getByTestId('bark-battle-runtime-player-src').textContent).toBe(
'/generated-bark-battle/player.png',
);
expect(screen.queryByText('分享给朋友')).toBeNull();
});
test('bark battle form checks mud points before creating image assets', async () => {
const user = userEvent.setup();
vi.mocked(getProfileDashboard).mockResolvedValue({
walletBalance: 2,
totalPlayTimeMs: 0,
playedWorldCount: 0,
updatedAt: '2026-05-14T10:00:00.000Z',
});
render(<TestWrapper withAuth />);
await openCreateTemplateHub(user);
await user.click(screen.getByRole('tab', { name: '汪汪声浪' }));
await user.click(await screen.findByRole('button', { name: '生成草稿' }));
expect(await screen.findByText('汪汪声浪配置表单')).toBeTruthy();
expect(
screen.getByRole('tab', { name: '汪汪声浪' }).getAttribute('aria-selected'),
).toBe('true');
await screen.findByText('泥点不足,本次需要 3 泥点,当前 2 泥点。'),
).toBeTruthy();
expect(createBarkBattleDraft).not.toHaveBeenCalled();
expect(generateAllBarkBattleImageAssets).not.toHaveBeenCalled();
});
test('bark battle draft is visible in draft shelf while image assets are generating', async () => {
const user = userEvent.setup();
vi.mocked(generateAllBarkBattleImageAssets).mockImplementation(
() =>
new Promise<Awaited<ReturnType<typeof generateAllBarkBattleImageAssets>>>(
() => undefined,
),
);
render(<TestWrapper withAuth />);
await openCreateTemplateHub(user);
await user.click(screen.getByRole('tab', { name: '汪汪声浪' }));
await user.click(await screen.findByRole('button', { name: '生成草稿' }));
expect(await screen.findByText('自动生成素材')).toBeTruthy();
await user.click(screen.getByRole('button', { name: '返回编辑' }));
await openDraftHub(user);
const panel = getPlatformTabPanel('saves');
expect(
await within(panel).findByRole('button', {
name: /继续创作《汪汪测试杯》/u,
}),
).toBeTruthy();
await expectDraftHubGeneratingBadgeCountAtLeast(1);
expect(listBarkBattleWorks).toHaveBeenCalled();
});
test('published bark battle stays visible when refresh temporarily returns only the duplicate draft', async () => {
const user = userEvent.setup();
vi.mocked(listBarkBattleWorks).mockResolvedValueOnce({
items: [
{
workId: 'bark-battle-work-1',
draftId: 'bark-battle-draft-1',
ownerUserId: 'user-1',
authorDisplayName: '测试玩家',
title: '汪汪测试杯',
summary: '',
themeDescription: '阳光草坪声浪竞技场',
playerImageDescription: '戴红色围巾的柯基选手',
opponentImageDescription: '蓝色护目镜哈士奇对手',
playerCharacterImageSrc: '/generated-bark-battle/player.png',
opponentCharacterImageSrc: '/generated-bark-battle/opponent.png',
uiBackgroundImageSrc: '/generated-bark-battle/background.png',
difficultyPreset: 'normal',
status: 'draft',
generationStatus: 'ready',
publishReady: true,
playCount: 0,
updatedAt: '2026-05-14T10:01:00.000Z',
publishedAt: null,
},
],
});
render(<TestWrapper withAuth />);
await openCreateTemplateHub(user);
await user.click(screen.getByRole('tab', { name: '汪汪声浪' }));
await user.click(await screen.findByRole('button', { name: '生成草稿' }));
await waitFor(() => {
expect(updateBarkBattleDraftConfig).toHaveBeenCalledWith(
expect.objectContaining({
draftId: 'bark-battle-draft-1',
workId: 'bark-battle-work-1',
}),
);
});
expect(await screen.findByText(/汪汪声浪结果页:汪汪测试杯/u)).toBeTruthy();
await user.click(screen.getByRole('button', { name: '发布' }));
await waitFor(() => {
expect(window.location.pathname).toBe('/works/detail');
});
await user.click(await screen.findByRole('button', { name: '返回' }));
await openDraftHub(user);
const panel = getPlatformTabPanel('saves');
await user.click(within(panel).getByRole('button', { name: /已发布/u }));
expect(await within(panel).findByText('汪汪测试杯')).toBeTruthy();
expect(within(panel).getByRole('button', { name: /查看详情《汪汪测试杯》/u })).toBeTruthy();
});
test('running match3d form generation can return to draft tab and reopen progress', async () => {
@@ -4590,7 +4833,7 @@ test('completed match3d draft notice first opens trial then reopens result', asy
).toHaveProperty('textContent', '1');
await user.click(screen.getByRole('button', { name: '返回' }));
expect(await screen.findByText('抓大鹅结果页')).toBeTruthy();
await user.click(screen.getByRole('button', { name: '返回' }));
await user.click(await screen.findByRole('button', { name: '返回' }));
await openDraftHub(user);
expect(screen.queryByLabelText('新生成完成')).toBeNull();
@@ -4638,7 +4881,7 @@ test('completed baby object match draft viewed immediately does not keep unread
expect(screen.queryByText('宝贝识物结果页')).toBeNull();
});
expect(await screen.findByLabelText('物品 A')).toBeTruthy();
await user.click(screen.getByRole('button', { name: '返回' }));
await user.click(await screen.findByRole('button', { name: '返回' }));
await openDraftHub(user);
expect(