fix: polish bark battle creation flow
This commit is contained in:
@@ -4,6 +4,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { afterEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
|
||||
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||
@@ -226,6 +227,42 @@ const babyObjectMatchDraftItem: BabyObjectMatchDraft = {
|
||||
publishedAt: null,
|
||||
};
|
||||
|
||||
const barkBattleDraftItem: BarkBattleWorkSummary = {
|
||||
workId: 'bark-battle-work-draft-visible',
|
||||
draftId: 'bark-battle-draft-visible',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '声浪作者',
|
||||
title: '竖屏声浪草稿',
|
||||
summary: '生成完成后也必须留在我的草稿里。',
|
||||
themeDescription: '霓虹竖屏擂台',
|
||||
playerImageDescription: '红围巾选手',
|
||||
opponentImageDescription: '蓝头带对手',
|
||||
onomatopoeia: ['炸场', '破阵'],
|
||||
playerCharacterImageSrc: '/bark/player.png',
|
||||
opponentCharacterImageSrc: '/bark/opponent.png',
|
||||
uiBackgroundImageSrc: '/bark/background.png',
|
||||
difficultyPreset: 'normal',
|
||||
status: 'draft',
|
||||
generationStatus: 'ready',
|
||||
publishReady: true,
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-21T10:00:00.000Z',
|
||||
publishedAt: null,
|
||||
};
|
||||
|
||||
const barkBattlePublishedItem: BarkBattleWorkSummary = {
|
||||
...barkBattleDraftItem,
|
||||
workId: 'bark-battle-work-published-visible',
|
||||
draftId: 'bark-battle-draft-published-visible',
|
||||
title: '竖屏声浪已发布',
|
||||
summary: '发布完成后必须留在已发布作品里。',
|
||||
authorDisplayName: '发布作者',
|
||||
status: 'published',
|
||||
playCount: 9,
|
||||
updatedAt: '2026-05-21T10:10:00.000Z',
|
||||
publishedAt: '2026-05-21T10:10:00.000Z',
|
||||
};
|
||||
|
||||
test('creation hub reflects updated draft title summary and counts after rerender', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onCreateType = vi.fn();
|
||||
@@ -592,6 +629,47 @@ test('creation hub shows delete action for baby object match drafts', async () =
|
||||
expect(onOpenBabyObjectMatchDetail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('creation hub works-only tab filters bark battle draft and published works', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onOpenBarkBattleDetail = vi.fn();
|
||||
|
||||
render(
|
||||
<CustomWorldCreationHub
|
||||
mode="works-only"
|
||||
items={[]}
|
||||
barkBattleItems={[barkBattleDraftItem, barkBattlePublishedItem]}
|
||||
loading={false}
|
||||
error={null}
|
||||
onRetry={() => {}}
|
||||
onCreateType={noopCreateType}
|
||||
onOpenDraft={() => {}}
|
||||
onEnterPublished={() => {}}
|
||||
onOpenBarkBattleDetail={onOpenBarkBattleDetail}
|
||||
entryConfig={testEntryConfig}
|
||||
creationTypes={testCreationTypes}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button', { name: '全部 2' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '草稿 1' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '已发布 1' })).toBeTruthy();
|
||||
expect(screen.getByText('竖屏声浪草稿')).toBeTruthy();
|
||||
expect(screen.getByText('竖屏声浪已发布')).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '草稿 1' }));
|
||||
expect(screen.getByText('竖屏声浪草稿')).toBeTruthy();
|
||||
expect(screen.queryByText('竖屏声浪已发布')).toBeNull();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '已发布 1' }));
|
||||
expect(screen.queryByText('竖屏声浪草稿')).toBeNull();
|
||||
expect(screen.getByText('竖屏声浪已发布')).toBeTruthy();
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /查看详情《竖屏声浪已发布》/u }),
|
||||
);
|
||||
expect(onOpenBarkBattleDetail).toHaveBeenCalledWith(barkBattlePublishedItem);
|
||||
});
|
||||
|
||||
test('creation hub published work delete action is revealed without opening card', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onDeletePuzzle = vi.fn();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
|
||||
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
|
||||
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
|
||||
@@ -68,6 +69,9 @@ type CustomWorldCreationHubProps = {
|
||||
babyObjectMatchItems?: BabyObjectMatchDraft[];
|
||||
onOpenBabyObjectMatchDetail?: ((item: BabyObjectMatchDraft) => void) | null;
|
||||
onDeleteBabyObjectMatch?: ((item: BabyObjectMatchDraft) => void) | null;
|
||||
barkBattleItems?: BarkBattleWorkSummary[];
|
||||
onOpenBarkBattleDetail?: ((item: BarkBattleWorkSummary) => void) | null;
|
||||
onDeleteBarkBattle?: ((item: BarkBattleWorkSummary) => void) | null;
|
||||
visualNovelItems?: VisualNovelWorkSummary[];
|
||||
onOpenVisualNovelDetail?: ((item: VisualNovelWorkSummary) => void) | null;
|
||||
onDeleteVisualNovel?: ((item: VisualNovelWorkSummary) => void) | null;
|
||||
@@ -173,6 +177,9 @@ export function CustomWorldCreationHub({
|
||||
babyObjectMatchItems = [],
|
||||
onOpenBabyObjectMatchDetail = null,
|
||||
onDeleteBabyObjectMatch = null,
|
||||
barkBattleItems = [],
|
||||
onOpenBarkBattleDetail = null,
|
||||
onDeleteBarkBattle = null,
|
||||
visualNovelItems = [],
|
||||
onOpenVisualNovelDetail = null,
|
||||
onDeleteVisualNovel = null,
|
||||
@@ -196,6 +203,7 @@ export function CustomWorldCreationHub({
|
||||
squareHoleItems: isSquareHoleCreationVisible ? squareHoleItems : [],
|
||||
puzzleItems,
|
||||
babyObjectMatchItems,
|
||||
barkBattleItems,
|
||||
visualNovelItems,
|
||||
canDeleteRpg: Boolean(onDeletePublished),
|
||||
canDeleteBigFish: Boolean(onDeleteBigFish),
|
||||
@@ -204,6 +212,7 @@ export function CustomWorldCreationHub({
|
||||
isSquareHoleCreationVisible && Boolean(onDeleteSquareHole),
|
||||
canDeletePuzzle: Boolean(onDeletePuzzle),
|
||||
canDeleteBabyObjectMatch: Boolean(onDeleteBabyObjectMatch),
|
||||
canDeleteBarkBattle: Boolean(onDeleteBarkBattle),
|
||||
canDeleteVisualNovel: Boolean(onDeleteVisualNovel),
|
||||
onOpenRpgDraft: onOpenDraft,
|
||||
onEnterRpgPublished: onEnterPublished,
|
||||
@@ -219,6 +228,8 @@ export function CustomWorldCreationHub({
|
||||
onClaimPuzzlePointIncentive: onClaimPuzzlePointIncentive ?? undefined,
|
||||
onOpenBabyObjectMatchDetail: onOpenBabyObjectMatchDetail ?? undefined,
|
||||
onDeleteBabyObjectMatch: onDeleteBabyObjectMatch ?? undefined,
|
||||
onOpenBarkBattleDetail: onOpenBarkBattleDetail ?? undefined,
|
||||
onDeleteBarkBattle: onDeleteBarkBattle ?? undefined,
|
||||
onOpenVisualNovelDetail: onOpenVisualNovelDetail ?? undefined,
|
||||
onDeleteVisualNovel: onDeleteVisualNovel ?? undefined,
|
||||
getItemState: getWorkState,
|
||||
@@ -227,6 +238,7 @@ export function CustomWorldCreationHub({
|
||||
bigFishItems,
|
||||
isSquareHoleCreationVisible,
|
||||
babyObjectMatchItems,
|
||||
barkBattleItems,
|
||||
items,
|
||||
match3dItems,
|
||||
onDeleteBigFish,
|
||||
@@ -235,12 +247,14 @@ export function CustomWorldCreationHub({
|
||||
onDeletePublished,
|
||||
onDeletePuzzle,
|
||||
onDeleteBabyObjectMatch,
|
||||
onDeleteBarkBattle,
|
||||
onDeleteVisualNovel,
|
||||
onClaimPuzzlePointIncentive,
|
||||
onOpenBigFishDetail,
|
||||
onOpenDraft,
|
||||
onOpenMatch3DDetail,
|
||||
onOpenBabyObjectMatchDetail,
|
||||
onOpenBarkBattleDetail,
|
||||
onOpenPuzzleDetail,
|
||||
onOpenSquareHoleDetail,
|
||||
onOpenVisualNovelDetail,
|
||||
@@ -284,6 +298,9 @@ export function CustomWorldCreationHub({
|
||||
case 'visual-novel':
|
||||
onOpenVisualNovelDetail?.(item.source.item);
|
||||
return;
|
||||
case 'bark-battle':
|
||||
onOpenBarkBattleDetail?.(item.source.item);
|
||||
return;
|
||||
case 'big-fish':
|
||||
onOpenBigFishDetail?.(item.source.item);
|
||||
return;
|
||||
|
||||
@@ -61,6 +61,7 @@ const CREATION_WORK_KIND_FALLBACK_COVER: Record<CreationWorkShelfKind, string> =
|
||||
'square-hole': '/creation-type-references/square-hole.webp',
|
||||
puzzle: '/creation-type-references/puzzle.webp',
|
||||
'baby-object-match': '/creation-type-references/creative-agent.webp',
|
||||
'bark-battle': '/creation-type-references/bark-battle.webp',
|
||||
'visual-novel': '/creation-type-references/visual-novel.webp',
|
||||
};
|
||||
|
||||
@@ -727,6 +728,8 @@ export function CustomWorldWorkCard({
|
||||
{item.summary}
|
||||
</div>
|
||||
|
||||
<div className="creation-work-card__author">作者:{item.authorDisplayName}</div>
|
||||
|
||||
{isPublished ? (
|
||||
<div className="creation-work-card__published-info">
|
||||
{item.pointIncentive ? (
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { createElement } from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
import {
|
||||
buildCreationWorkShelfItems,
|
||||
getCreationWorkShelfItemTime,
|
||||
hasBarkBattleRequiredImages,
|
||||
isPersistedBarkBattleDraftGenerating,
|
||||
type CreationWorkShelfItem,
|
||||
} from './creationWorkShelf';
|
||||
import { CustomWorldWorkCard } from './CustomWorldWorkCard';
|
||||
|
||||
test('buildCreationWorkShelfItems maps visual novel items with VN public code', () => {
|
||||
const items = buildCreationWorkShelfItems({
|
||||
@@ -50,6 +56,253 @@ test('buildCreationWorkShelfItems maps visual novel items with VN public code',
|
||||
expect(items[1]?.publicWorkCode).toBeNull();
|
||||
});
|
||||
|
||||
test('buildCreationWorkShelfItems keeps published bark battle over duplicate draft', () => {
|
||||
const items = buildCreationWorkShelfItems({
|
||||
rpgItems: [],
|
||||
bigFishItems: [],
|
||||
puzzleItems: [],
|
||||
barkBattleItems: [
|
||||
{
|
||||
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,
|
||||
},
|
||||
{
|
||||
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: 'published',
|
||||
generationStatus: 'ready',
|
||||
publishReady: true,
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-14T10:02:00.000Z',
|
||||
publishedAt: '2026-05-14T10:02:00.000Z',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(items).toHaveLength(1);
|
||||
expect(items[0]?.kind).toBe('bark-battle');
|
||||
expect(items[0]?.status).toBe('published');
|
||||
expect(items[0]?.publicWorkCode).toBe('BB-TLEWORK1');
|
||||
expect(items[0]?.authorDisplayName).toBe('测试玩家');
|
||||
});
|
||||
|
||||
test('buildCreationWorkShelfItems keeps separate bark battle draft and published works visible', () => {
|
||||
const items = buildCreationWorkShelfItems({
|
||||
rpgItems: [],
|
||||
bigFishItems: [],
|
||||
puzzleItems: [],
|
||||
barkBattleItems: [
|
||||
{
|
||||
workId: 'BB-DRAFT001',
|
||||
draftId: 'bark-battle-draft-visible',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '草稿作者',
|
||||
title: '草稿声浪赛',
|
||||
summary: '',
|
||||
themeDescription: '草地声浪挑战',
|
||||
playerImageDescription: '柯基选手',
|
||||
opponentImageDescription: '哈士奇对手',
|
||||
playerCharacterImageSrc: '/draft-player.png',
|
||||
opponentCharacterImageSrc: '/draft-opponent.png',
|
||||
uiBackgroundImageSrc: '/draft-background.png',
|
||||
difficultyPreset: 'easy',
|
||||
status: 'draft',
|
||||
generationStatus: 'ready',
|
||||
publishReady: false,
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-20T00:00:00.000Z',
|
||||
publishedAt: null,
|
||||
},
|
||||
{
|
||||
workId: 'BB-PUB00001',
|
||||
draftId: 'bark-battle-draft-published',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '发布作者',
|
||||
title: '已发布声浪赛',
|
||||
summary: '',
|
||||
themeDescription: '霓虹声浪挑战',
|
||||
playerImageDescription: '柴犬选手',
|
||||
opponentImageDescription: '机器人对手',
|
||||
playerCharacterImageSrc: '/published-player.png',
|
||||
opponentCharacterImageSrc: '/published-opponent.png',
|
||||
uiBackgroundImageSrc: '/published-background.png',
|
||||
difficultyPreset: 'normal',
|
||||
status: 'published',
|
||||
generationStatus: 'ready',
|
||||
publishReady: true,
|
||||
playCount: 3,
|
||||
updatedAt: '2026-05-21T00:00:00.000Z',
|
||||
publishedAt: '2026-05-21T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(items).toHaveLength(2);
|
||||
expect(items.find((item) => item.status === 'draft')?.id).toBe('BB-DRAFT001');
|
||||
expect(items.find((item) => item.status === 'published')?.id).toBe(
|
||||
'BB-PUB00001',
|
||||
);
|
||||
expect(items.find((item) => item.status === 'published')?.publicWorkCode).toBe(
|
||||
'BB-PUB00001',
|
||||
);
|
||||
});
|
||||
|
||||
test('buildCreationWorkShelfItems gives bark battle draft cover from character or reference fallback', () => {
|
||||
const items = buildCreationWorkShelfItems({
|
||||
rpgItems: [],
|
||||
bigFishItems: [],
|
||||
puzzleItems: [],
|
||||
barkBattleItems: [
|
||||
{
|
||||
workId: 'BB-COVER001',
|
||||
draftId: 'bark-battle-draft-cover',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '草稿作者',
|
||||
title: '角色封面声浪赛',
|
||||
summary: '',
|
||||
themeDescription: '草地声浪挑战',
|
||||
playerImageDescription: '柯基选手',
|
||||
opponentImageDescription: '哈士奇对手',
|
||||
playerCharacterImageSrc: '/draft-player-cover.png',
|
||||
opponentCharacterImageSrc: '/draft-opponent-cover.png',
|
||||
uiBackgroundImageSrc: null,
|
||||
difficultyPreset: 'easy',
|
||||
status: 'draft',
|
||||
generationStatus: 'partial_failed',
|
||||
publishReady: false,
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-20T00:00:00.000Z',
|
||||
publishedAt: null,
|
||||
},
|
||||
{
|
||||
workId: 'BB-COVER002',
|
||||
draftId: 'bark-battle-draft-cover-fallback',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '草稿作者',
|
||||
title: '默认封面声浪赛',
|
||||
summary: '',
|
||||
themeDescription: '夜市声浪挑战',
|
||||
playerImageDescription: '柴犬选手',
|
||||
opponentImageDescription: '机器人对手',
|
||||
playerCharacterImageSrc: null,
|
||||
opponentCharacterImageSrc: null,
|
||||
uiBackgroundImageSrc: null,
|
||||
difficultyPreset: 'normal',
|
||||
status: 'draft',
|
||||
generationStatus: 'pending_assets',
|
||||
publishReady: false,
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-19T00:00:00.000Z',
|
||||
publishedAt: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(items.find((item) => item.id === 'BB-COVER001')?.coverImageSrc).toBe(
|
||||
'/draft-player-cover.png',
|
||||
);
|
||||
expect(items.find((item) => item.id === 'BB-COVER001')?.coverCharacterImageSrcs).toEqual([
|
||||
'/draft-player-cover.png',
|
||||
'/draft-opponent-cover.png',
|
||||
]);
|
||||
expect(items.find((item) => item.id === 'BB-COVER002')?.coverImageSrc).toBe(
|
||||
'/creation-type-references/bark-battle.webp',
|
||||
);
|
||||
});
|
||||
|
||||
test('buildCreationWorkShelfItems keeps bark battle draft author display name', () => {
|
||||
const items = buildCreationWorkShelfItems({
|
||||
rpgItems: [],
|
||||
bigFishItems: [],
|
||||
puzzleItems: [],
|
||||
barkBattleItems: [
|
||||
{
|
||||
workId: 'bark-battle-work-draft-author',
|
||||
draftId: 'bark-battle-draft-author',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '草稿作者',
|
||||
title: '草稿声浪赛',
|
||||
summary: '',
|
||||
themeDescription: '草地声浪挑战',
|
||||
playerImageDescription: '柯基选手',
|
||||
opponentImageDescription: '哈士奇对手',
|
||||
playerCharacterImageSrc: '/player.png',
|
||||
opponentCharacterImageSrc: '/opponent.png',
|
||||
uiBackgroundImageSrc: '/background.png',
|
||||
difficultyPreset: 'easy',
|
||||
status: 'draft',
|
||||
generationStatus: 'ready',
|
||||
publishReady: false,
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-20T00:00:00.000Z',
|
||||
publishedAt: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(items[0]?.kind).toBe('bark-battle');
|
||||
expect(items[0]?.status).toBe('draft');
|
||||
expect(items[0]?.authorDisplayName).toBe('草稿作者');
|
||||
});
|
||||
|
||||
test('buildCreationWorkShelfItems falls back unknown authors to player label', () => {
|
||||
const items = buildCreationWorkShelfItems({
|
||||
rpgItems: [],
|
||||
bigFishItems: [],
|
||||
puzzleItems: [],
|
||||
match3dItems: [
|
||||
{
|
||||
workId: 'match3d-work-author-fallback',
|
||||
profileId: 'match3d-profile-author-fallback',
|
||||
ownerUserId: 'user-1',
|
||||
gameName: '水果抓大鹅',
|
||||
themeText: '水果',
|
||||
summary: '把水果从透明罐里抓出来。',
|
||||
tags: [],
|
||||
coverImageSrc: null,
|
||||
clearCount: 0,
|
||||
difficulty: 1,
|
||||
publicationStatus: 'published',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-20T00:00:00.000Z',
|
||||
publishedAt: '2026-05-20T00:00:00.000Z',
|
||||
publishReady: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(items[0]?.kind).toBe('match3d');
|
||||
expect(items[0]?.authorDisplayName).toBe('玩家');
|
||||
});
|
||||
|
||||
test('buildCreationWorkShelfItems attaches open and delete actions through shelf adapters', () => {
|
||||
const onOpenPuzzleDetail = vi.fn();
|
||||
const onDeletePuzzle = vi.fn();
|
||||
@@ -672,6 +925,159 @@ test('buildCreationWorkShelfItems uses match3d transparent container reference a
|
||||
);
|
||||
});
|
||||
|
||||
test('buildCreationWorkShelfItems maps bark battle works with scene role cover and BB code', () => {
|
||||
const onOpenBarkBattleDetail = vi.fn();
|
||||
const items = buildCreationWorkShelfItems({
|
||||
rpgItems: [],
|
||||
bigFishItems: [],
|
||||
puzzleItems: [],
|
||||
barkBattleItems: [
|
||||
{
|
||||
workId: 'bark-battle-work-12345678',
|
||||
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: 'published',
|
||||
generationStatus: 'ready',
|
||||
publishReady: true,
|
||||
playCount: 6,
|
||||
updatedAt: '2026-05-20T00:00:00.000Z',
|
||||
publishedAt: '2026-05-20T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
onOpenBarkBattleDetail,
|
||||
});
|
||||
|
||||
const item = items[0];
|
||||
item?.actions.open();
|
||||
|
||||
expect(item?.kind).toBe('bark-battle');
|
||||
expect(item?.publicWorkCode).toBe('BB-12345678');
|
||||
expect(item?.sharePath).toContain('/works/detail?work=BB-12345678');
|
||||
expect(item?.coverImageSrc).toBe('/generated-bark-battle/background.png');
|
||||
expect(item?.coverRenderMode).toBe('scene_with_roles');
|
||||
expect(item?.coverCharacterImageSrcs).toEqual([
|
||||
'/generated-bark-battle/player.png',
|
||||
'/generated-bark-battle/opponent.png',
|
||||
]);
|
||||
expect(onOpenBarkBattleDetail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ workId: 'bark-battle-work-12345678' }),
|
||||
);
|
||||
});
|
||||
|
||||
test('bark battle draft generating state follows pending assets or missing three images', () => {
|
||||
const draft = {
|
||||
workId: 'bark-battle-work-draft',
|
||||
draftId: 'bark-battle-draft-1',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '玩家',
|
||||
title: '草稿声浪赛',
|
||||
summary: '',
|
||||
themeDescription: '草地',
|
||||
playerImageDescription: '柯基',
|
||||
opponentImageDescription: '哈士奇',
|
||||
playerCharacterImageSrc: '/player.png',
|
||||
opponentCharacterImageSrc: null,
|
||||
uiBackgroundImageSrc: '/background.png',
|
||||
difficultyPreset: 'easy' as const,
|
||||
status: 'draft' as const,
|
||||
generationStatus: 'pending_assets',
|
||||
publishReady: false,
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-20T00:00:00.000Z',
|
||||
publishedAt: null,
|
||||
};
|
||||
|
||||
expect(hasBarkBattleRequiredImages(draft)).toBe(false);
|
||||
expect(isPersistedBarkBattleDraftGenerating(draft)).toBe(true);
|
||||
expect(
|
||||
isPersistedBarkBattleDraftGenerating({
|
||||
...draft,
|
||||
opponentCharacterImageSrc: '/opponent.png',
|
||||
generationStatus: 'ready',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
test('CustomWorldWorkCard renders author for draft and published works', () => {
|
||||
const buildItem = (
|
||||
status: CreationWorkShelfItem['status'],
|
||||
authorDisplayName: string,
|
||||
): CreationWorkShelfItem => ({
|
||||
id: `card-${status}`,
|
||||
kind: 'bark-battle',
|
||||
status,
|
||||
authorDisplayName,
|
||||
title: status === 'draft' ? '草稿声浪赛' : '发布声浪赛',
|
||||
summary: '一场轻快的汪汪声浪对决。',
|
||||
updatedAt: '2026-05-20T00:00:00.000Z',
|
||||
coverImageSrc: null,
|
||||
coverRenderMode: 'image',
|
||||
coverCharacterImageSrcs: [],
|
||||
publicWorkCode: null,
|
||||
sharePath: null,
|
||||
openActionLabel: status === 'draft' ? '继续创作' : '查看详情',
|
||||
canDelete: false,
|
||||
canShare: false,
|
||||
badges: [
|
||||
{ id: 'status', label: status === 'draft' ? '草稿' : '已发布', tone: 'neutral' },
|
||||
{ id: 'type', label: '汪汪', tone: 'neutral' },
|
||||
],
|
||||
metrics: [],
|
||||
actions: { open: () => {} },
|
||||
source: {
|
||||
kind: 'bark-battle',
|
||||
item: {
|
||||
workId: `bark-battle-${status}`,
|
||||
draftId: `draft-${status}`,
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName,
|
||||
title: status === 'draft' ? '草稿声浪赛' : '发布声浪赛',
|
||||
summary: '一场轻快的汪汪声浪对决。',
|
||||
themeDescription: '公园舞台',
|
||||
playerImageDescription: '柯基选手',
|
||||
opponentImageDescription: '哈士奇对手',
|
||||
playerCharacterImageSrc: null,
|
||||
opponentCharacterImageSrc: null,
|
||||
uiBackgroundImageSrc: null,
|
||||
difficultyPreset: 'normal',
|
||||
status,
|
||||
generationStatus: 'ready',
|
||||
publishReady: status === 'published',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-20T00:00:00.000Z',
|
||||
publishedAt: status === 'published' ? '2026-05-20T00:00:00.000Z' : null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const draftHtml = renderToStaticMarkup(
|
||||
createElement(CustomWorldWorkCard, {
|
||||
item: buildItem('draft', '草稿作者'),
|
||||
onOpen: () => {},
|
||||
}),
|
||||
);
|
||||
const publishedHtml = renderToStaticMarkup(
|
||||
createElement(CustomWorldWorkCard, {
|
||||
item: buildItem('published', '发布作者'),
|
||||
onOpen: () => {},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(draftHtml).toContain('作者:草稿作者');
|
||||
expect(publishedHtml).toContain('作者:发布作者');
|
||||
});
|
||||
|
||||
test('getCreationWorkShelfItemTime parses backend seconds.microsZ values', () => {
|
||||
expect(getCreationWorkShelfItemTime('1778457601.234567Z')).toBe(
|
||||
1778457601234.567,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
|
||||
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
|
||||
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
@@ -9,6 +10,7 @@ import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contra
|
||||
import { buildPublicWorkStagePath } from '../../routing/appPageRoutes';
|
||||
import {
|
||||
buildBabyObjectMatchPublicWorkCode,
|
||||
buildBarkBattlePublicWorkCode,
|
||||
buildBigFishPublicWorkCode,
|
||||
buildMatch3DPublicWorkCode,
|
||||
buildPuzzlePublicWorkCode,
|
||||
@@ -19,6 +21,9 @@ import type { CustomWorldProfile } from '../../types';
|
||||
|
||||
const MATCH3D_CONTAINER_REFERENCE_COVER_SRC =
|
||||
'/match3d-background-references/pot-fused-reference.png';
|
||||
const BARK_BATTLE_REFERENCE_COVER_SRC =
|
||||
'/creation-type-references/bark-battle.webp';
|
||||
const DEFAULT_CREATION_WORK_AUTHOR = '玩家';
|
||||
|
||||
export type CreationWorkShelfKind =
|
||||
| 'rpg'
|
||||
@@ -27,6 +32,7 @@ export type CreationWorkShelfKind =
|
||||
| 'square-hole'
|
||||
| 'puzzle'
|
||||
| 'baby-object-match'
|
||||
| 'bark-battle'
|
||||
| 'visual-novel';
|
||||
export type CreationWorkShelfStatus = 'draft' | 'published';
|
||||
|
||||
@@ -84,6 +90,10 @@ export type CreationWorkShelfSource =
|
||||
kind: 'visual-novel';
|
||||
item: VisualNovelWorkSummary;
|
||||
}
|
||||
| {
|
||||
kind: 'bark-battle';
|
||||
item: BarkBattleWorkSummary;
|
||||
}
|
||||
| {
|
||||
kind: 'baby-object-match';
|
||||
item: BabyObjectMatchDraft;
|
||||
@@ -103,6 +113,7 @@ export type CreationWorkShelfItem = {
|
||||
hasUnreadUpdate?: boolean;
|
||||
title: string;
|
||||
summary: string;
|
||||
authorDisplayName: string;
|
||||
updatedAt: string;
|
||||
coverImageSrc: string | null;
|
||||
coverRenderMode: 'image' | 'scene_with_roles';
|
||||
@@ -127,6 +138,7 @@ export function buildCreationWorkShelfItems(params: {
|
||||
squareHoleItems?: SquareHoleWorkSummary[];
|
||||
puzzleItems: PuzzleWorkSummary[];
|
||||
babyObjectMatchItems?: BabyObjectMatchDraft[];
|
||||
barkBattleItems?: BarkBattleWorkSummary[];
|
||||
visualNovelItems?: VisualNovelWorkSummary[];
|
||||
canDeleteRpg?: boolean;
|
||||
canDeleteBigFish?: boolean;
|
||||
@@ -134,6 +146,7 @@ export function buildCreationWorkShelfItems(params: {
|
||||
canDeleteSquareHole?: boolean;
|
||||
canDeletePuzzle?: boolean;
|
||||
canDeleteBabyObjectMatch?: boolean;
|
||||
canDeleteBarkBattle?: boolean;
|
||||
canDeleteVisualNovel?: boolean;
|
||||
onOpenRpgDraft?: (item: CustomWorldWorkSummary) => void;
|
||||
onEnterRpgPublished?: (profileId: string) => void;
|
||||
@@ -149,6 +162,8 @@ export function buildCreationWorkShelfItems(params: {
|
||||
onClaimPuzzlePointIncentive?: (item: PuzzleWorkSummary) => void;
|
||||
onOpenBabyObjectMatchDetail?: (item: BabyObjectMatchDraft) => void;
|
||||
onDeleteBabyObjectMatch?: (item: BabyObjectMatchDraft) => void;
|
||||
onOpenBarkBattleDetail?: (item: BarkBattleWorkSummary) => void;
|
||||
onDeleteBarkBattle?: (item: BarkBattleWorkSummary) => void;
|
||||
onOpenVisualNovelDetail?: (item: VisualNovelWorkSummary) => void;
|
||||
onDeleteVisualNovel?: (item: VisualNovelWorkSummary) => void;
|
||||
getItemState?: (
|
||||
@@ -163,6 +178,7 @@ export function buildCreationWorkShelfItems(params: {
|
||||
squareHoleItems = [],
|
||||
puzzleItems,
|
||||
babyObjectMatchItems = [],
|
||||
barkBattleItems = [],
|
||||
visualNovelItems = [],
|
||||
canDeleteRpg = false,
|
||||
canDeleteBigFish = false,
|
||||
@@ -170,6 +186,7 @@ export function buildCreationWorkShelfItems(params: {
|
||||
canDeleteSquareHole = false,
|
||||
canDeletePuzzle = false,
|
||||
canDeleteBabyObjectMatch = false,
|
||||
canDeleteBarkBattle = false,
|
||||
canDeleteVisualNovel = false,
|
||||
onOpenRpgDraft,
|
||||
onEnterRpgPublished,
|
||||
@@ -185,6 +202,8 @@ export function buildCreationWorkShelfItems(params: {
|
||||
onClaimPuzzlePointIncentive,
|
||||
onOpenBabyObjectMatchDetail,
|
||||
onDeleteBabyObjectMatch,
|
||||
onOpenBarkBattleDetail,
|
||||
onDeleteBarkBattle,
|
||||
onOpenVisualNovelDetail,
|
||||
onDeleteVisualNovel,
|
||||
getItemState,
|
||||
@@ -229,6 +248,12 @@ export function buildCreationWorkShelfItems(params: {
|
||||
onDelete: onDeleteBabyObjectMatch,
|
||||
}),
|
||||
),
|
||||
...mergeBarkBattleShelfSourceItems(barkBattleItems).map((item) =>
|
||||
mapBarkBattleWorkToShelfItem(item, canDeleteBarkBattle, {
|
||||
onOpen: onOpenBarkBattleDetail,
|
||||
onDelete: onDeleteBarkBattle,
|
||||
}),
|
||||
),
|
||||
...visualNovelItems.map((item) =>
|
||||
mapVisualNovelWorkToShelfItem(item, canDeleteVisualNovel, {
|
||||
onOpen: onOpenVisualNovelDetail,
|
||||
@@ -259,6 +284,28 @@ export function buildCreationWorkShelfItems(params: {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function mergeBarkBattleShelfSourceItems(
|
||||
items: readonly BarkBattleWorkSummary[],
|
||||
): BarkBattleWorkSummary[] {
|
||||
const byWorkId = new Map<string, BarkBattleWorkSummary>();
|
||||
for (const item of items) {
|
||||
const current = byWorkId.get(item.workId);
|
||||
if (!current) {
|
||||
byWorkId.set(item.workId, item);
|
||||
continue;
|
||||
}
|
||||
if (current.status !== 'published' && item.status === 'published') {
|
||||
byWorkId.set(item.workId, { ...current, ...item });
|
||||
continue;
|
||||
}
|
||||
if (current.status === item.status) {
|
||||
byWorkId.set(item.workId, { ...current, ...item });
|
||||
}
|
||||
}
|
||||
return Array.from(byWorkId.values());
|
||||
}
|
||||
|
||||
type RpgWorkShelfAdapter = {
|
||||
onOpenDraft?: (item: CustomWorldWorkSummary) => void;
|
||||
onEnterPublished?: (profileId: string) => void;
|
||||
@@ -303,6 +350,7 @@ function mapRpgWorkToShelfItem(
|
||||
status: item.status,
|
||||
title: item.title,
|
||||
summary: item.summary,
|
||||
authorDisplayName: resolveAuthorDisplayName(item, libraryEntry),
|
||||
updatedAt: item.updatedAt,
|
||||
coverImageSrc: item.coverImageSrc ?? null,
|
||||
coverRenderMode: item.coverRenderMode ?? 'image',
|
||||
@@ -342,6 +390,7 @@ function mapBigFishWorkToShelfItem(
|
||||
status: item.status,
|
||||
title: item.title,
|
||||
summary: item.summary,
|
||||
authorDisplayName: resolveAuthorDisplayName(item),
|
||||
updatedAt: item.updatedAt,
|
||||
coverImageSrc: item.coverImageSrc ?? null,
|
||||
coverRenderMode: 'image',
|
||||
@@ -386,6 +435,7 @@ function mapMatch3DWorkToShelfItem(
|
||||
status,
|
||||
title: item.gameName,
|
||||
summary: item.summary,
|
||||
authorDisplayName: resolveAuthorDisplayName(item),
|
||||
updatedAt: item.updatedAt,
|
||||
coverImageSrc,
|
||||
coverRenderMode: 'image',
|
||||
@@ -434,6 +484,7 @@ function mapPuzzleWorkToShelfItem(
|
||||
item.workDescription?.trim() ||
|
||||
item.summary.trim() ||
|
||||
(status === 'draft' ? '未填写作品描述' : ''),
|
||||
authorDisplayName: resolveAuthorDisplayName(item),
|
||||
updatedAt: item.updatedAt,
|
||||
coverImageSrc,
|
||||
coverRenderMode: 'image',
|
||||
@@ -500,6 +551,7 @@ function mapBabyObjectMatchDraftToShelfItem(
|
||||
summary:
|
||||
item.workDescription.trim() ||
|
||||
`${item.itemNames[0]}和${item.itemNames[1]}识物分类`,
|
||||
authorDisplayName: resolveAuthorDisplayName(item),
|
||||
updatedAt: item.updatedAt,
|
||||
coverImageSrc,
|
||||
coverRenderMode: 'image',
|
||||
@@ -549,6 +601,7 @@ function mapVisualNovelWorkToShelfItem(
|
||||
status,
|
||||
title,
|
||||
summary,
|
||||
authorDisplayName: resolveAuthorDisplayName(item),
|
||||
updatedAt: item.updatedAt,
|
||||
coverImageSrc: item.coverImageSrc ?? null,
|
||||
coverRenderMode: 'image',
|
||||
@@ -578,6 +631,72 @@ function mapVisualNovelWorkToShelfItem(
|
||||
};
|
||||
}
|
||||
|
||||
function mapBarkBattleWorkToShelfItem(
|
||||
item: BarkBattleWorkSummary,
|
||||
canDelete: boolean,
|
||||
adapter: WorkShelfAdapter<BarkBattleWorkSummary>,
|
||||
): CreationWorkShelfItem {
|
||||
const status = item.status;
|
||||
const publicWorkCode =
|
||||
status === 'published' ? buildBarkBattlePublicWorkCode(item.workId) : null;
|
||||
const playerCharacterImageSrc = normalizeCoverImageSrc(
|
||||
item.playerCharacterImageSrc,
|
||||
);
|
||||
const opponentCharacterImageSrc = normalizeCoverImageSrc(
|
||||
item.opponentCharacterImageSrc,
|
||||
);
|
||||
const coverImageSrc =
|
||||
normalizeCoverImageSrc(item.uiBackgroundImageSrc) ??
|
||||
playerCharacterImageSrc ??
|
||||
opponentCharacterImageSrc ??
|
||||
BARK_BATTLE_REFERENCE_COVER_SRC;
|
||||
const coverCharacterImageSrcs = [
|
||||
playerCharacterImageSrc,
|
||||
opponentCharacterImageSrc,
|
||||
].filter((imageSrc): imageSrc is string => Boolean(imageSrc));
|
||||
const canRenderSceneWithRoles =
|
||||
Boolean(normalizeCoverImageSrc(item.uiBackgroundImageSrc)) &&
|
||||
coverCharacterImageSrcs.length >= 2;
|
||||
|
||||
return {
|
||||
id: item.workId,
|
||||
kind: 'bark-battle',
|
||||
status,
|
||||
title: item.title.trim() || '汪汪声浪大作战',
|
||||
summary:
|
||||
item.summary.trim() ||
|
||||
item.themeDescription.trim() ||
|
||||
(status === 'draft' ? '未填写作品描述' : ''),
|
||||
authorDisplayName: resolveAuthorDisplayName(item),
|
||||
updatedAt: item.updatedAt,
|
||||
coverImageSrc,
|
||||
coverRenderMode: canRenderSceneWithRoles ? 'scene_with_roles' : 'image',
|
||||
coverCharacterImageSrcs,
|
||||
publicWorkCode,
|
||||
sharePath:
|
||||
publicWorkCode && status === 'published'
|
||||
? buildPublicWorkStagePath('work-detail', publicWorkCode)
|
||||
: null,
|
||||
openActionLabel: status === 'published' ? '查看详情' : '继续创作',
|
||||
canDelete,
|
||||
canShare: status === 'published' && Boolean(publicWorkCode),
|
||||
badges: [
|
||||
buildStatusBadge(status),
|
||||
{ id: 'type', label: '汪汪', tone: 'neutral' },
|
||||
],
|
||||
metrics:
|
||||
status === 'published'
|
||||
? buildPublishedMetrics({
|
||||
playCount: item.playCount,
|
||||
remixCount: 0,
|
||||
likeCount: 0,
|
||||
})
|
||||
: [],
|
||||
actions: buildWorkShelfActions(item, adapter),
|
||||
source: { kind: 'bark-battle', item },
|
||||
};
|
||||
}
|
||||
|
||||
function mapSquareHoleWorkToShelfItem(
|
||||
item: SquareHoleWorkSummary,
|
||||
canDelete: boolean,
|
||||
@@ -596,6 +715,7 @@ function mapSquareHoleWorkToShelfItem(
|
||||
status,
|
||||
title: item.gameName,
|
||||
summary: item.summary,
|
||||
authorDisplayName: resolveAuthorDisplayName(item),
|
||||
updatedAt: item.updatedAt,
|
||||
coverImageSrc,
|
||||
coverRenderMode: 'image',
|
||||
@@ -625,6 +745,26 @@ function mapSquareHoleWorkToShelfItem(
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function resolveAuthorDisplayName(
|
||||
...sources: Array<unknown>
|
||||
) {
|
||||
for (const source of sources) {
|
||||
const authorDisplayName =
|
||||
source &&
|
||||
typeof source === 'object' &&
|
||||
'authorDisplayName' in source &&
|
||||
typeof source.authorDisplayName === 'string'
|
||||
? source.authorDisplayName.trim()
|
||||
: '';
|
||||
if (authorDisplayName) {
|
||||
return authorDisplayName;
|
||||
}
|
||||
}
|
||||
|
||||
return DEFAULT_CREATION_WORK_AUTHOR;
|
||||
}
|
||||
|
||||
function normalizeCoverImageSrc(value?: string | null) {
|
||||
return value?.trim() || null;
|
||||
}
|
||||
@@ -816,11 +956,34 @@ function isPersistedCreationWorkGenerating(item: CreationWorkShelfItem) {
|
||||
return item.source.item.generationStatus === 'generating';
|
||||
case 'puzzle':
|
||||
return isPersistedPuzzleDraftGenerating(item.source.item);
|
||||
case 'bark-battle':
|
||||
return isPersistedBarkBattleDraftGenerating(item.source.item);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isPersistedBarkBattleDraftGenerating(
|
||||
item: BarkBattleWorkSummary,
|
||||
) {
|
||||
if (item.status === 'published') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
item.generationStatus === 'pending_assets' ||
|
||||
!hasBarkBattleRequiredImages(item)
|
||||
);
|
||||
}
|
||||
|
||||
export function hasBarkBattleRequiredImages(item: BarkBattleWorkSummary) {
|
||||
return Boolean(
|
||||
normalizeCoverImageSrc(item.playerCharacterImageSrc) &&
|
||||
normalizeCoverImageSrc(item.opponentCharacterImageSrc) &&
|
||||
normalizeCoverImageSrc(item.uiBackgroundImageSrc),
|
||||
);
|
||||
}
|
||||
|
||||
export function isPersistedPuzzleDraftGenerating(item: PuzzleWorkSummary) {
|
||||
if (item.generationStatus !== 'generating') {
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user