This commit is contained in:
2026-04-28 20:25:37 +08:00
parent f0471a4f8d
commit 0f013b6eee
45 changed files with 1117 additions and 1047 deletions

View File

@@ -1383,9 +1383,6 @@ export function AdventureEntityModal({
)}
</span>
</div>
<div className="mt-2 text-[11px] leading-5 text-zinc-500">
{attribute.definition}
</div>
</div>
))}
</div>

View File

@@ -331,7 +331,7 @@ export function CharacterAttributeGrid({
boostedCombatStats,
resourceLabels,
)
: slot.combatUseText,
: '',
};
});
@@ -364,9 +364,11 @@ export function CharacterAttributeGrid({
</div>
</div>
</div>
<div className="mt-2 text-[10px] leading-relaxed text-sky-200/85">
{effectText}
</div>
{effectText ? (
<div className="mt-2 text-[10px] leading-relaxed text-sky-200/85">
{effectText}
</div>
) : null}
</div>
),
)}

View File

@@ -561,9 +561,6 @@ export function CharacterPanel({
)}
</span>
</div>
<div className="mt-2 text-[11px] leading-5 text-zinc-500">
{attribute.definition}
</div>
</div>
))}
</div>

View File

@@ -545,16 +545,6 @@ function buildLandmarkSearchText(
].join(' ');
}
function buildAttributeSlotSummary(
slot: CustomWorldProfile['attributeSchema']['slots'][number],
) {
return compactTextList([
slot.combatUseText,
slot.socialUseText,
slot.explorationUseText,
]).join(' / ');
}
export function CustomWorldEntityCatalog({
profile,
previewCharacters,
@@ -984,11 +974,6 @@ export function CustomWorldEntityCatalog({
<div className="text-[11px] font-bold tracking-[0.18em] text-zinc-500">
</div>
{profile.attributeSchema?.schemaName ? (
<div className="text-xs leading-5 text-zinc-500">
{profile.attributeSchema.schemaName}
</div>
) : null}
</div>
<div className="mt-3 grid grid-cols-2 gap-2 sm:grid-cols-3 xl:grid-cols-6">
{attributeSlots.map((slot) => (
@@ -999,9 +984,6 @@ export function CustomWorldEntityCatalog({
<div className="text-sm font-semibold text-white">
{slot.name}
</div>
<div className="mt-1 line-clamp-2 text-[11px] leading-5 text-zinc-400">
{buildAttributeSlotSummary(slot) || slot.definition}
</div>
</div>
))}
</div>

View File

@@ -2,7 +2,6 @@
import {
cleanup,
fireEvent,
render,
screen,
waitFor,
@@ -198,7 +197,6 @@ function createProfile(): CustomWorldProfile {
attributeSchema: {
id: 'schema-1',
worldId: 'world-1',
schemaName: '潮雾六维',
schemaVersion: 1,
generatedFrom: {
worldType: 'WUXIA',
@@ -211,62 +209,26 @@ function createProfile(): CustomWorldProfile {
{
slotId: 'axis_a',
name: '骨势',
definition: '扛住压力并正面推进的底子。',
positiveSignals: ['硬顶'],
negativeSignals: ['畏缩'],
combatUseText: '正面承压与破阵。',
socialUseText: '在谈判里稳住立场。',
explorationUseText: '穿过危险地形。',
},
{
slotId: 'axis_b',
name: '身法',
definition: '抢位、转场与把握节奏的能力。',
positiveSignals: ['灵动'],
negativeSignals: ['迟滞'],
combatUseText: '移动换位。',
socialUseText: '捕捉话锋。',
explorationUseText: '快速穿行。',
},
{
slotId: 'axis_c',
name: '眼脉',
definition: '看破破绽、拆解局势的能力。',
positiveSignals: ['洞察'],
negativeSignals: ['误判'],
combatUseText: '识破招式。',
socialUseText: '辨别谎言。',
explorationUseText: '发现线索。',
},
{
slotId: 'axis_d',
name: '心焰',
definition: '决断、压迫与坚持意志的能力。',
positiveSignals: ['果断'],
negativeSignals: ['犹疑'],
combatUseText: '强行压制。',
socialUseText: '立威推进。',
explorationUseText: '面对险境不退。',
},
{
slotId: 'axis_e',
name: '尘缘',
definition: '处理人情、承诺和关系牵引的能力。',
positiveSignals: ['守信'],
negativeSignals: ['冷漠'],
combatUseText: '协作配合。',
socialUseText: '建立信任。',
explorationUseText: '借助人脉。',
},
{
slotId: 'axis_f',
name: '玄息',
definition: '调息、稳态和久战的能力。',
positiveSignals: ['沉稳'],
negativeSignals: ['浮躁'],
combatUseText: '续战恢复。',
socialUseText: '保持耐心。',
explorationUseText: '长线跋涉。',
},
],
},
@@ -753,7 +715,7 @@ test('基本设定目标打开独立编辑面板', () => {
expect(screen.queryByText('编辑世界信息')).toBeNull();
});
test('世界信息面板可以编辑六个角色维度信息', async () => {
test('基本设定面板只编辑六个角色维度名称', async () => {
const user = userEvent.setup();
const savedProfileRef: { current: CustomWorldProfile | null } = {
current: null,
@@ -762,7 +724,7 @@ test('世界信息面板可以编辑六个角色维度信息', async () => {
render(
<RpgCreationEntityEditorModal
profile={createProfile()}
target={{ kind: 'world' }}
target={{ kind: 'foundation' }}
onClose={() => {}}
onProfileChange={(profile) => {
savedProfileRef.current = profile;
@@ -775,33 +737,15 @@ test('世界信息面板可以编辑六个角色维度信息', async () => {
await user.clear(nameInputs[0]!);
await user.type(nameInputs[0]!, '潮骨');
const definitionFields = screen.getAllByLabelText('定义');
await user.clear(definitionFields[0]!);
await user.type(definitionFields[0]!, '顶住潮压并正面推进的角色底色。');
const positiveSignalFields = screen.getAllByLabelText('正向信号');
fireEvent.change(positiveSignalFields[0]!, {
target: { value: '硬顶, 护阵' },
});
const combatFields = screen.getAllByLabelText('战斗体现');
await user.clear(combatFields[0]!);
await user.type(combatFields[0]!, '正面压线与护住阵脚。');
expect(screen.queryByLabelText('定义')).toBeNull();
expect(screen.queryByLabelText('正向信号')).toBeNull();
expect(screen.queryByLabelText('战斗体现')).toBeNull();
await user.click(screen.getByRole('button', { name: //u }));
expect(savedProfileRef.current?.attributeSchema.slots[0]?.name).toBe(
'潮骨',
);
expect(savedProfileRef.current?.attributeSchema.slots[0]?.definition).toBe(
'顶住潮压并正面推进的角色底色。',
);
expect(
savedProfileRef.current?.attributeSchema.slots[0]?.positiveSignals,
).toEqual(['硬顶', '护阵']);
expect(savedProfileRef.current?.attributeSchema.slots[0]?.combatUseText).toBe(
'正面压线与护住阵脚。',
);
});
test('可扮演角色列表使用缩略卡片并点击进入编辑', async () => {

View File

@@ -55,8 +55,8 @@ test('creation hub reflects updated draft title summary and counts after rerende
expect(screen.getByText('角色 3')).toBeTruthy();
expect(screen.getByText('地点 4')).toBeTruthy();
expect(screen.getByRole('button', { name: / RPG/u })).toBeTruthy();
expect(screen.getByRole('button', { name: //u })).toBeTruthy();
expect(screen.getByRole('button', { name: //u })).toBeTruthy();
expect(screen.queryByRole('button', { name: //u })).toBeNull();
rerender(
<CustomWorldCreationHub

View File

@@ -43,8 +43,8 @@ test('creation hub draft card renders compiled work summary fields', () => {
expect(html).toContain('玩家是失职返乡的守灯人');
expect(html).toContain('守灯会与沉船商盟争夺航道解释权');
expect(html).toContain('角色扮演 RPG');
expect(html).toContain('大鱼吃小鱼');
expect(html).toContain('拼图玩法');
expect(html).not.toContain('大鱼吃小鱼');
});
test('creation hub renders puzzle works in the same unified list with puzzle tag', () => {

View File

@@ -1,7 +1,7 @@
import { ArrowRight } from 'lucide-react';
import {
PLATFORM_CREATION_TYPES,
getVisiblePlatformCreationTypes,
type PlatformCreationTypeId,
} from '../platform-entry/platformEntryCreationTypes';
@@ -16,6 +16,10 @@ export function CustomWorldCreationStartCard({
error = null,
onCreateType,
}: CustomWorldCreationStartCardProps) {
// 创作首页首屏卡带与创作类型弹层保持同一份展示口径,
// 避免某个玩法只在其中一个入口被隐藏而出现状态漂移。
const visibleCreationTypes = getVisiblePlatformCreationTypes();
return (
// 移动端限制模块高度,模板入口改为横向滚动,避免挤占作品列表首屏空间。
<div className="platform-surface platform-surface--hero relative max-h-[33svh] overflow-hidden px-3 py-3 sm:max-h-none sm:px-5 sm:py-5 xl:px-5 xl:py-4">
@@ -34,7 +38,7 @@ export function CustomWorldCreationStartCard({
</div>
<div className="-mx-1 flex snap-x gap-2 overflow-x-auto px-1 pb-1 sm:mx-0 sm:grid sm:gap-3 sm:overflow-visible sm:px-0 sm:pb-0 sm:grid-cols-2 xl:grid-cols-5 xl:gap-2.5">
{PLATFORM_CREATION_TYPES.map((item) => {
{visibleCreationTypes.map((item) => {
const disabled = item.locked || busy;
return (

View File

@@ -1,7 +1,7 @@
import { ArrowRight } from 'lucide-react';
import { UnifiedModal } from '../common/UnifiedModal';
import { PLATFORM_CREATION_TYPES } from './platformEntryCreationTypes';
import { getVisiblePlatformCreationTypes } from './platformEntryCreationTypes';
export interface PlatformEntryCreationTypeModalProps {
isOpen: boolean;
@@ -14,7 +14,7 @@ export interface PlatformEntryCreationTypeModalProps {
}
function CreationTypeCard(props: {
item: (typeof PLATFORM_CREATION_TYPES)[number];
item: ReturnType<typeof getVisiblePlatformCreationTypes>[number];
busy: boolean;
onSelect: () => void;
}) {
@@ -81,9 +81,7 @@ export function PlatformEntryCreationTypeModal({
// 平台入口只渲染当前允许展示的创作类型;
// 被隐藏的玩法仍保留既有实现与路由,不在这里删除能力本体。
const visibleCreationTypes = PLATFORM_CREATION_TYPES.filter(
(item) => !item.hidden,
);
const visibleCreationTypes = getVisiblePlatformCreationTypes();
return (
<UnifiedModal

View File

@@ -122,6 +122,7 @@ import { useRpgCreationResultAutosave } from '../rpg-entry/useRpgCreationResultA
import { useRpgCreationSessionController } from '../rpg-entry/useRpgCreationSessionController';
import { PlatformEntryCreationTypeModal } from './PlatformEntryCreationTypeModal';
import type { PlatformCreationTypeId } from './platformEntryCreationTypes';
import { isPlatformCreationTypeVisible } from './platformEntryCreationTypes';
import {
PlatformEntryHomeView,
type PlatformHomeTab,
@@ -473,6 +474,7 @@ export function PlatformEntryFlowShellImpl({
const [deletingCreationWorkId, setDeletingCreationWorkId] = useState<
string | null
>(null);
const isBigFishCreationVisible = isPlatformCreationTypeVisible('big-fish');
const hadReadableProtectedDataRef = useRef(false);
const hasInitialAgentSession = Boolean(
readCustomWorldAgentUiState().activeSessionId &&
@@ -660,7 +662,9 @@ export function PlatformEntryFlowShellImpl({
await Promise.allSettled([
platformBootstrap.refreshPublishedGallery(),
platformBootstrap.refreshCustomWorldWorks(),
refreshBigFishGallery(),
isBigFishCreationVisible
? refreshBigFishGallery()
: Promise.resolve([] as BigFishWorkSummary[]),
refreshPuzzleGallery(),
]);
return latestSession;
@@ -716,9 +720,9 @@ export function PlatformEntryFlowShellImpl({
}, [agentResultPreview]);
const featuredGalleryEntries = useMemo(() => {
const bigFishPublicEntries = bigFishGalleryEntries.map(
mapBigFishWorkToPlatformGalleryCard,
);
const bigFishPublicEntries = isBigFishCreationVisible
? bigFishGalleryEntries.map(mapBigFishWorkToPlatformGalleryCard)
: [];
const puzzlePublicEntries = puzzleGalleryEntries.map(
mapPuzzleWorkToPlatformGalleryCard,
);
@@ -727,6 +731,7 @@ export function PlatformEntryFlowShellImpl({
[...bigFishPublicEntries, ...puzzlePublicEntries],
).slice(0, 6);
}, [
isBigFishCreationVisible,
bigFishGalleryEntries,
platformBootstrap.publishedGalleryEntries,
puzzleGalleryEntries,
@@ -736,11 +741,14 @@ export function PlatformEntryFlowShellImpl({
mergePlatformPublicGalleryEntries(
platformBootstrap.publishedGalleryEntries,
[
...bigFishGalleryEntries.map(mapBigFishWorkToPlatformGalleryCard),
...(isBigFishCreationVisible
? bigFishGalleryEntries.map(mapBigFishWorkToPlatformGalleryCard)
: []),
...puzzleGalleryEntries.map(mapPuzzleWorkToPlatformGalleryCard),
],
),
[
isBigFishCreationVisible,
bigFishGalleryEntries,
platformBootstrap.publishedGalleryEntries,
puzzleGalleryEntries,
@@ -986,7 +994,6 @@ export function PlatformEntryFlowShellImpl({
const bigFishError = bigFishFlow.error;
const setBigFishError = bigFishFlow.setError;
const isBigFishBusy = bigFishFlow.isBusy;
const setIsBigFishBusy = bigFishFlow.setIsBusy;
const streamingBigFishReplyText = bigFishFlow.streamingReplyText;
const isStreamingBigFishReply = bigFishFlow.isStreamingReply;
@@ -1892,10 +1899,17 @@ export function PlatformEntryFlowShellImpl({
useEffect(() => {
if (selectionStage === 'platform') {
void refreshBigFishGallery();
if (isBigFishCreationVisible) {
void refreshBigFishGallery();
}
void refreshPuzzleGallery();
}
}, [refreshBigFishGallery, refreshPuzzleGallery, selectionStage]);
}, [
isBigFishCreationVisible,
refreshBigFishGallery,
refreshPuzzleGallery,
selectionStage,
]);
useEffect(() => {
if (
@@ -1914,6 +1928,7 @@ export function PlatformEntryFlowShellImpl({
useEffect(() => {
if (
isBigFishCreationVisible &&
(platformBootstrap.platformTab === 'create' ||
selectionStage === 'platform') &&
platformBootstrap.canReadProtectedData
@@ -1921,6 +1936,7 @@ export function PlatformEntryFlowShellImpl({
void refreshBigFishShelf();
}
}, [
isBigFishCreationVisible,
platformBootstrap.canReadProtectedData,
platformBootstrap.platformTab,
refreshBigFishShelf,
@@ -1955,7 +1971,9 @@ export function PlatformEntryFlowShellImpl({
resolveRpgCreationErrorMessage(error, '读取创作作品列表失败。'),
);
});
void refreshBigFishShelf();
if (isBigFishCreationVisible) {
void refreshBigFishShelf();
}
void refreshPuzzleShelf();
}}
createError={
@@ -1991,20 +2009,32 @@ export function PlatformEntryFlowShellImpl({
handleExperienceRpgWork(item);
}}
rpgLibraryEntries={platformBootstrap.savedCustomWorldEntries}
bigFishItems={bigFishWorks}
onOpenBigFishDetail={(item) => {
runProtectedAction(() => {
void openBigFishDraft(item);
});
}}
onExperienceBigFish={(item) => {
runProtectedAction(() => {
void startBigFishRunFromWork(item);
});
}}
onDeleteBigFish={(item) => {
handleDeleteBigFishWork(item);
}}
bigFishItems={isBigFishCreationVisible ? bigFishWorks : []}
onOpenBigFishDetail={
isBigFishCreationVisible
? (item) => {
runProtectedAction(() => {
void openBigFishDraft(item);
});
}
: undefined
}
onExperienceBigFish={
isBigFishCreationVisible
? (item) => {
runProtectedAction(() => {
void startBigFishRunFromWork(item);
});
}
: null
}
onDeleteBigFish={
isBigFishCreationVisible
? (item) => {
handleDeleteBigFishWork(item);
}
: null
}
puzzleItems={puzzleWorks}
onOpenPuzzleDetail={(item) => {
runProtectedAction(() => {

View File

@@ -14,6 +14,21 @@ export type PlatformCreationTypeCard = {
hidden?: boolean;
};
/**
* 返回当前平台入口允许展示的创作类型。
* 平台层的入口、首屏卡带与初始化请求都应基于这份结果统一判断。
*/
export function getVisiblePlatformCreationTypes() {
return PLATFORM_CREATION_TYPES.filter((item) => !item.hidden);
}
/**
* 判断某个创作类型当前是否仍暴露在平台入口中。
*/
export function isPlatformCreationTypeVisible(id: PlatformCreationTypeId) {
return PLATFORM_CREATION_TYPES.some((item) => item.id === id && !item.hidden);
}
/**
* 创作页与类型弹层共用同一份模板元数据,避免多入口文案和可用状态漂移。
* `hidden` 只控制平台入口是否展示,不影响既有玩法链路和路由能力。

View File

@@ -5007,83 +5007,19 @@ function WorldAttributeSchemaEditor({
};
return (
<SectionPanel title="角色维度" subtitle={value.schemaName || '世界能力维度'}>
<div className="space-y-3">
<SectionPanel title="角色维度">
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{value.slots.map((slot) => (
<div
key={slot.slotId}
className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3"
>
<div className="grid gap-3 sm:grid-cols-[10rem_minmax(0,1fr)]">
<Field label="维度名称">
<TextInput
value={slot.name}
onChange={(name) => updateSlot(slot.slotId, { name })}
/>
</Field>
<Field label="定义">
<TextArea
value={slot.definition}
onChange={(definition) =>
updateSlot(slot.slotId, { definition })
}
rows={2}
/>
</Field>
</div>
<div className="mt-3 grid gap-3 sm:grid-cols-2">
<Field label="正向信号">
<TextArea
value={commaText(slot.positiveSignals)}
onChange={(text) =>
updateSlot(slot.slotId, {
positiveSignals: parseCommaText(text),
})
}
rows={2}
/>
</Field>
<Field label="负向信号">
<TextArea
value={commaText(slot.negativeSignals)}
onChange={(text) =>
updateSlot(slot.slotId, {
negativeSignals: parseCommaText(text),
})
}
rows={2}
/>
</Field>
</div>
<div className="mt-3 grid gap-3 sm:grid-cols-3">
<Field label="战斗体现">
<TextArea
value={slot.combatUseText}
onChange={(combatUseText) =>
updateSlot(slot.slotId, { combatUseText })
}
rows={2}
/>
</Field>
<Field label="社交体现">
<TextArea
value={slot.socialUseText}
onChange={(socialUseText) =>
updateSlot(slot.slotId, { socialUseText })
}
rows={2}
/>
</Field>
<Field label="探索体现">
<TextArea
value={slot.explorationUseText}
onChange={(explorationUseText) =>
updateSlot(slot.slotId, { explorationUseText })
}
rows={2}
/>
</Field>
</div>
<Field label="维度名称">
<TextInput
value={slot.name}
onChange={(name) => updateSlot(slot.slotId, { name })}
/>
</Field>
</div>
))}
</div>

View File

@@ -133,62 +133,26 @@ test('custom world character selection stays stable when character ids are empty
{
slotId: 'axis_a',
name: '潮骨',
definition: '扛住潮压与正面冲击的底子。',
positiveSignals: [],
negativeSignals: [],
combatUseText: '顶住正面浪涌。',
socialUseText: '给人能扛事的可靠感。',
explorationUseText: '在风浪里稳住自己。',
},
{
slotId: 'axis_b',
name: '浪步',
definition: '顺潮借势、换位穿行的能力。',
positiveSignals: [],
negativeSignals: [],
combatUseText: '借势切线。',
socialUseText: '谈吐灵活。',
explorationUseText: '穿越复杂地形。',
},
{
slotId: 'axis_c',
name: '舟识',
definition: '辨流向、识潮眼的能力。',
positiveSignals: [],
negativeSignals: [],
combatUseText: '抓住变化时机。',
socialUseText: '看懂局势留白。',
explorationUseText: '辨认水路与遗痕。',
},
{
slotId: 'axis_d',
name: '潮魄',
definition: '在剧烈变化中仍敢推进的胆气。',
positiveSignals: [],
negativeSignals: [],
combatUseText: '顶着压力推进。',
socialUseText: '在冲突里压住场子。',
explorationUseText: '面对异变继续前探。',
},
{
slotId: 'axis_e',
name: '契汐',
definition: '与人和约定形成牵引的能力。',
positiveSignals: [],
negativeSignals: [],
combatUseText: '借协同形成连锁。',
socialUseText: '结盟、安抚与交换。',
explorationUseText: '从旧约中打开局面。',
},
{
slotId: 'axis_f',
name: '回澜',
definition: '在漫长消耗中回稳状态的能力。',
positiveSignals: [],
negativeSignals: [],
combatUseText: '久战不乱。',
socialUseText: '遇事沉静。',
explorationUseText: '在恶劣天气里保有余力。',
},
],
},

View File

@@ -14,7 +14,6 @@ import { ApiClientError } from '../../services/apiClient';
import type { AuthUser } from '../../services/authService';
import {
createBigFishCreationSession,
executeBigFishCreationAction,
getBigFishCreationSession,
} from '../../services/big-fish-creation';
import { listBigFishGallery } from '../../services/big-fish-gallery';
@@ -1175,6 +1174,21 @@ test('create hub exposes direct template entry, keeps AIRP and visual novel lock
).toBeTruthy();
});
test('platform create hub does not prefetch hidden big fish platform data', async () => {
const user = userEvent.setup();
render(<TestWrapper withAuth />);
await openCreationHub(user);
expect(
await screen.findByRole('button', { name: / RPG/u }),
).toBeTruthy();
expect(screen.queryByRole('button', { name: //u })).toBeNull();
expect(listBigFishWorks).not.toHaveBeenCalled();
expect(listBigFishGallery).not.toHaveBeenCalled();
});
test('opening RPG agent workspace does not refetch session snapshot in a render loop', async () => {
const user = userEvent.setup();
@@ -1540,14 +1554,13 @@ test('creation hub clears all private work shelves immediately after logout stat
const createPanel = getPlatformTabPanel('create');
expect(await within(createPanel).findByText('RPG 退出缓存作品')).toBeTruthy();
expect(await within(createPanel).findByText('大鱼退出缓存作品')).toBeTruthy();
expect(within(createPanel).queryByText('大鱼退出缓存作品')).toBeNull();
expect(await within(createPanel).findByText('拼图退出缓存作品')).toBeTruthy();
rerender(<TestWrapper authValue={loggedOutAuth} />);
await waitFor(() => {
expect(within(createPanel).queryByText('RPG 退出缓存作品')).toBeNull();
expect(within(createPanel).queryByText('大鱼退出缓存作品')).toBeNull();
expect(within(createPanel).queryByText('拼图退出缓存作品')).toBeNull();
});
expect(within(createPanel).getByText('还没有作品')).toBeTruthy();
@@ -1597,7 +1610,7 @@ test('published puzzle works appear on home and category public shelves', async
).toBeGreaterThan(0);
});
test('published big fish works appear on home and category public shelves', async () => {
test('published big fish works stay hidden from platform home and category shelves', async () => {
const user = userEvent.setup();
const publishedBigFishWork: BigFishWorkSummary = {
workId: 'big-fish-work-public-1',
@@ -1623,20 +1636,17 @@ test('published big fish works appear on home and category public shelves', asyn
render(<TestWrapper />);
await waitFor(() => {
expect(screen.getAllByText('机械深海 大鱼吃小鱼').length).toBeGreaterThan(
0,
);
expect(listBigFishGallery).not.toHaveBeenCalled();
});
expect(screen.queryByText('机械深海 大鱼吃小鱼')).toBeNull();
await user.click(screen.getByRole('button', { name: '分类' }));
const categoryPanel = getPlatformTabPanel('category');
expect(within(categoryPanel).queryByText('机械深海 大鱼吃小鱼')).toBeNull();
expect(
within(categoryPanel).getAllByText('机械深海 大鱼吃小鱼').length,
).toBeGreaterThan(0);
expect(
within(categoryPanel).getAllByRole('button', { name: //u }).length,
).toBeGreaterThan(0);
within(categoryPanel).queryAllByRole('button', { name: //u }).length,
).toBe(0);
});
test('published puzzle detail returns to the source platform tab', async () => {
@@ -1853,31 +1863,15 @@ test('new creation entry maps raw bearer token errors to user-facing auth copy',
expect(screen.queryByText('缺少 Authorization Bearer Token')).toBeNull();
});
test('big fish creation timeout exits busy state and shows a readable error', async () => {
test('hidden big fish creation entry does not render in platform create hub', async () => {
const user = userEvent.setup();
vi.mocked(createBigFishCreationSession).mockRejectedValueOnce(
Object.assign(new Error('请求超时15000ms'), {
name: 'TimeoutError',
}),
);
render(<TestWrapper withAuth />);
await openCreationHub(user);
const button = screen.getByRole('button', { name: //u });
await user.click(button);
await waitFor(() => {
expect(
within(getPlatformTabPanel('create')).getAllByText(
'开启大鱼吃小鱼创作工作台超时,请确认运行时后端已启动后重试。',
).length,
).toBeGreaterThan(0);
});
expect((button as HTMLButtonElement).disabled).toBe(false);
expect(screen.queryByText(//u)).toBeNull();
expect(screen.queryByRole('button', { name: //u })).toBeNull();
expect(createBigFishCreationSession).not.toHaveBeenCalled();
});
test('puzzle creation timeout exits busy state and shows a readable error', async () => {
@@ -2086,163 +2080,6 @@ test('public code search opens a published big fish work by BF code', async () =
expect(getRpgEntryWorldGalleryDetailByCode).not.toHaveBeenCalled();
});
test('big fish draft card restores the bound agent session and opens the result view', async () => {
const user = userEvent.setup();
vi.mocked(listBigFishWorks).mockResolvedValue({
items: [
{
workId: 'big-fish-work-big-fish-session-1',
sourceSessionId: 'big-fish-session-1',
ownerUserId: 'user-1',
title: '机械深海 大鱼吃小鱼',
subtitle: '机械微生物吞并进化 · 偏爽快节奏',
summary: '机械微生物吞并进化',
coverImageSrc: null,
status: 'draft',
updatedAt: '2026-04-22T12:10:00.000Z',
publishReady: false,
levelCount: 8,
levelMainImageReadyCount: 0,
levelMotionReadyCount: 0,
backgroundReady: false,
},
],
});
render(<TestWrapper withAuth />);
await openCreationHub(user);
const title = await screen.findByText('机械深海 大鱼吃小鱼');
const card = title.closest('.platform-surface');
if (!(card instanceof HTMLElement)) {
throw new Error('Missing big fish draft card');
}
await user.click(card);
await waitFor(() => {
expect(getBigFishCreationSession).toHaveBeenCalledWith(
'big-fish-session-1',
);
});
expect(await screen.findByText('大鱼吃小鱼结果页')).toBeTruthy();
expect(screen.getByText('机械深海 大鱼吃小鱼')).toBeTruthy();
await user.click(screen.getByRole('button', { name: '返回' }));
expect(
await screen.findByText('大鱼吃小鱼工作区big-fish-session-1'),
).toBeTruthy();
expect(screen.queryByText(//u)).toBeNull();
expect(screen.getByText('我想做机械深海里微生物互相吞并进化。')).toBeTruthy();
});
test('big fish result publish action refreshes creation works', async () => {
const user = userEvent.setup();
const baseBigFishSession = (
await getBigFishCreationSession('big-fish-session-1')
).session;
vi.mocked(getBigFishCreationSession).mockClear();
vi.mocked(listBigFishWorks).mockClear();
vi.mocked(listBigFishGallery).mockClear();
const publishedBigFishSession = {
...baseBigFishSession,
stage: 'published',
publishReady: true,
assetCoverage: {
levelMainImageReadyCount: 8,
levelMotionReadyCount: 16,
backgroundReady: true,
requiredLevelCount: 8,
publishReady: true,
blockers: [],
},
updatedAt: '2026-04-22T12:20:00.000Z',
};
vi.mocked(executeBigFishCreationAction).mockResolvedValue({
session: publishedBigFishSession,
});
vi.mocked(listBigFishWorks)
.mockResolvedValueOnce({
items: [
{
workId: 'big-fish-work-big-fish-session-1',
sourceSessionId: 'big-fish-session-1',
ownerUserId: 'user-1',
title: '机械深海 大鱼吃小鱼',
subtitle: '机械微生物吞并进化 · 偏爽快节奏',
summary: '机械微生物吞并进化',
coverImageSrc: null,
status: 'draft',
updatedAt: '2026-04-22T12:10:00.000Z',
publishReady: true,
levelCount: 8,
levelMainImageReadyCount: 8,
levelMotionReadyCount: 16,
backgroundReady: true,
},
],
})
.mockResolvedValue({
items: [
{
workId: 'big-fish-work-big-fish-session-1',
sourceSessionId: 'big-fish-session-1',
ownerUserId: 'user-1',
title: '机械深海 大鱼吃小鱼',
subtitle: '机械微生物吞并进化 · 偏爽快节奏',
summary: '机械微生物吞并进化',
coverImageSrc: null,
status: 'published',
updatedAt: '2026-04-22T12:20:00.000Z',
publishReady: true,
levelCount: 8,
levelMainImageReadyCount: 8,
levelMotionReadyCount: 16,
backgroundReady: true,
},
],
});
render(<TestWrapper withAuth />);
await openCreationHub(user);
const title = await screen.findByText('机械深海 大鱼吃小鱼');
const card = title.closest('.platform-surface');
if (!(card instanceof HTMLElement)) {
throw new Error('Missing big fish draft card');
}
await user.click(card);
await waitFor(() => {
expect(getBigFishCreationSession).toHaveBeenCalledWith(
'big-fish-session-1',
);
});
vi.mocked(listBigFishWorks).mockClear();
expect(await screen.findByText('大鱼吃小鱼结果页')).toBeTruthy();
await user.click(await screen.findByRole('button', { name: '发布' }));
await waitFor(() => {
expect(executeBigFishCreationAction).toHaveBeenCalledWith(
'big-fish-session-1',
{
action: 'big_fish_publish_game',
},
);
});
await waitFor(() => {
expect(listBigFishWorks).toHaveBeenCalled();
});
await waitFor(() => {
expect(listBigFishGallery).toHaveBeenCalled();
});
});
test('starting draft generation leaves the agent workspace and shows the generation progress view', async () => {
const user = userEvent.setup();