1ad25e30f8
新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件 迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome 补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
792 lines
26 KiB
TypeScript
792 lines
26 KiB
TypeScript
/** @vitest-environment jsdom */
|
|
|
|
import { act, render } from '@testing-library/react';
|
|
import { useEffect, useState } from 'react';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import type { CustomWorldAgentSessionSnapshot } from '../../../packages/shared/src/contracts/customWorldAgent';
|
|
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldWorkSummary';
|
|
import type { RpgCreationResultView } from '../../../packages/shared/src/contracts/rpgCreationResultView';
|
|
import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
|
|
import {
|
|
executeRpgCreationAction,
|
|
getRpgCreationOperation,
|
|
upsertRpgWorldProfile,
|
|
} from '../../services/rpg-creation';
|
|
import { deleteRpgEntryWorldProfile } from '../../services/rpg-entry/rpgEntryLibraryClient';
|
|
import { type CustomWorldProfile, WorldType } from '../../types';
|
|
import { useRpgCreationResultAutosave } from './useRpgCreationResultAutosave';
|
|
import { useRpgEntryLibraryDetail } from './useRpgEntryLibraryDetail';
|
|
|
|
vi.mock('../../services/rpg-creation', () => ({
|
|
executeRpgCreationAction: vi.fn(),
|
|
getRpgCreationOperation: vi.fn(),
|
|
upsertRpgWorldProfile: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../../services/rpg-entry/rpgEntryLibraryClient', () => ({
|
|
deleteRpgEntryWorldProfile: vi.fn(),
|
|
getRpgEntryWorldGalleryDetail: vi.fn(),
|
|
getRpgEntryWorldLibraryDetail: vi.fn(),
|
|
listRpgEntryWorldLibrary: vi.fn(),
|
|
publishRpgEntryWorldProfile: vi.fn(),
|
|
unpublishRpgEntryWorldProfile: vi.fn(),
|
|
}));
|
|
|
|
function buildProfile(name: string): CustomWorldProfile {
|
|
return {
|
|
id: `profile-${name}`,
|
|
settingText: name,
|
|
name,
|
|
subtitle: name,
|
|
summary: name,
|
|
tone: '测试',
|
|
playerGoal: '测试',
|
|
templateWorldType: WorldType.WUXIA,
|
|
compatibilityTemplateWorldType: WorldType.WUXIA,
|
|
majorFactions: [],
|
|
coreConflicts: [],
|
|
attributeSchema: {
|
|
id: `schema-${name}`,
|
|
worldId: `profile-${name}`,
|
|
schemaVersion: 1,
|
|
generatedFrom: {
|
|
worldType: WorldType.CUSTOM,
|
|
worldName: name,
|
|
settingSummary: name,
|
|
tone: '测试',
|
|
conflictCore: '测试',
|
|
},
|
|
slots: [],
|
|
},
|
|
playableNpcs: [],
|
|
storyNpcs: [],
|
|
items: [],
|
|
landmarks: [],
|
|
generationMode: 'full',
|
|
generationStatus: 'complete',
|
|
};
|
|
}
|
|
|
|
function buildLibraryEntry(
|
|
profile: CustomWorldProfile,
|
|
): CustomWorldLibraryEntry<CustomWorldProfile> {
|
|
return {
|
|
ownerUserId: 'user-1',
|
|
profileId: profile.id,
|
|
publicWorkCode: null,
|
|
authorPublicUserCode: null,
|
|
profile,
|
|
visibility: 'published' as const,
|
|
publishedAt: '2026-04-25T00:00:00.000Z',
|
|
updatedAt: '2026-04-25T00:00:00.000Z',
|
|
authorDisplayName: '测试玩家',
|
|
worldName: profile.name,
|
|
subtitle: profile.subtitle,
|
|
summaryText: profile.summary,
|
|
coverImageSrc: null,
|
|
themeMode: 'tide' as const,
|
|
playableNpcCount: profile.playableNpcs.length,
|
|
landmarkCount: profile.landmarks.length,
|
|
likeCount: 0,
|
|
};
|
|
}
|
|
|
|
function buildSession(
|
|
overrides: Partial<CustomWorldAgentSessionSnapshot> = {},
|
|
): CustomWorldAgentSessionSnapshot {
|
|
return {
|
|
sessionId: 'agent-session-1',
|
|
currentTurn: 1,
|
|
anchorContent: {
|
|
worldPromise: null,
|
|
playerFantasy: null,
|
|
themeBoundary: null,
|
|
playerEntryPoint: null,
|
|
coreConflict: null,
|
|
keyRelationships: null,
|
|
hiddenLines: null,
|
|
iconicElements: null,
|
|
},
|
|
progressPercent: 20,
|
|
lastAssistantReply: '继续补齐世界草稿。',
|
|
stage: 'clarifying',
|
|
focusCardId: null,
|
|
creatorIntent: null,
|
|
creatorIntentReadiness: {
|
|
isReady: false,
|
|
completedKeys: [],
|
|
missingKeys: [],
|
|
},
|
|
anchorPack: null,
|
|
lockState: null,
|
|
draftProfile: null,
|
|
messages: [],
|
|
draftCards: [],
|
|
pendingClarifications: [],
|
|
suggestedActions: [],
|
|
recommendedReplies: [],
|
|
qualityFindings: [],
|
|
assetCoverage: {
|
|
roleAssets: [],
|
|
sceneAssets: [],
|
|
allRoleAssetsReady: false,
|
|
allSceneAssetsReady: false,
|
|
},
|
|
resultPreview: null,
|
|
updatedAt: '2026-04-25T00:00:00.000Z',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function buildResultView(
|
|
overrides: Partial<RpgCreationResultView> = {},
|
|
): RpgCreationResultView {
|
|
const session = overrides.session ?? buildSession();
|
|
return {
|
|
session,
|
|
profile: null,
|
|
profileSource: 'none',
|
|
targetStage: 'agent-workspace',
|
|
generationViewSource: null,
|
|
resultViewSource: null,
|
|
canAutosaveLibrary: false,
|
|
canSyncResultProfile: false,
|
|
publishReady: false,
|
|
canEnterWorld: false,
|
|
blockerCount: 0,
|
|
recoveryAction: 'continue_agent',
|
|
recoveryReason: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('RPG Agent 草稿恢复', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('作品摘要已有对象数量但 session 没有 draftProfile 时恢复 Agent 页面', async () => {
|
|
const syncAgentCreationResultView = vi.fn(async () =>
|
|
buildResultView({
|
|
session: buildSession({
|
|
stage: 'clarifying',
|
|
draftProfile: null,
|
|
}),
|
|
targetStage: 'agent-workspace',
|
|
recoveryAction: 'continue_agent',
|
|
}),
|
|
);
|
|
const setSelectionStage = vi.fn();
|
|
const persistAgentUiState = vi.fn();
|
|
const setGeneratedCustomWorldProfile = vi.fn();
|
|
const setCustomWorldResultViewSource = vi.fn();
|
|
const suppressAgentDraftResultAutoOpen = vi.fn();
|
|
let openWork:
|
|
| ((work: CustomWorldWorkSummary) => Promise<void>)
|
|
| null = null;
|
|
|
|
function Harness() {
|
|
openWork = useRpgEntryLibraryDetail({
|
|
userId: 'user-1',
|
|
selectedDetailEntry: null,
|
|
setSelectedDetailEntry: vi.fn(),
|
|
savedCustomWorldEntries: [],
|
|
setSavedCustomWorldEntries: vi.fn(),
|
|
setGeneratedCustomWorldProfile,
|
|
setCustomWorldError: vi.fn(),
|
|
setCustomWorldAutoSaveError: vi.fn(),
|
|
setCustomWorldAutoSaveState: vi.fn(),
|
|
setCustomWorldGenerationViewSource: vi.fn(),
|
|
setCustomWorldResultViewSource,
|
|
setSelectionStage,
|
|
setPlatformTabToCreate: vi.fn(),
|
|
setPlatformTabToDraft: vi.fn(),
|
|
setPlatformError: vi.fn(),
|
|
appendBrowseHistoryEntry: vi.fn(async () => {}),
|
|
refreshCustomWorldWorks: vi.fn(async () => []),
|
|
refreshPublishedGallery: vi.fn(async () => []),
|
|
persistAgentUiState,
|
|
syncAgentCreationResultView,
|
|
buildDraftResultProfile: (view) =>
|
|
(view?.profile as CustomWorldProfile | null) ?? null,
|
|
suppressAgentDraftResultAutoOpen,
|
|
releaseAgentDraftResultAutoOpenSuppression: vi.fn(),
|
|
resetAutoSaveTrackingToIdle: vi.fn(),
|
|
markAutoSavedProfile: vi.fn(),
|
|
}).handleOpenCreationWork;
|
|
|
|
return null;
|
|
}
|
|
|
|
render(<Harness />);
|
|
|
|
await act(async () => {
|
|
await openWork?.({
|
|
workId: 'draft:agent-session-1',
|
|
sourceType: 'agent_session',
|
|
status: 'draft',
|
|
title: '未生成草稿作品',
|
|
subtitle: '',
|
|
summary: '',
|
|
updatedAt: '2026-04-25T00:00:00.000Z',
|
|
stage: 'clarifying',
|
|
stageLabel: '澄清中',
|
|
playableNpcCount: 2,
|
|
landmarkCount: 3,
|
|
sessionId: 'agent-session-1',
|
|
canResume: true,
|
|
canEnterWorld: false,
|
|
});
|
|
});
|
|
|
|
expect(syncAgentCreationResultView).toHaveBeenCalledWith('agent-session-1');
|
|
expect(suppressAgentDraftResultAutoOpen).toHaveBeenCalled();
|
|
expect(persistAgentUiState).toHaveBeenCalledWith('agent-session-1', null);
|
|
expect(setGeneratedCustomWorldProfile).toHaveBeenLastCalledWith(null);
|
|
expect(setCustomWorldResultViewSource).toHaveBeenLastCalledWith(null);
|
|
expect(setSelectionStage).toHaveBeenLastCalledWith('agent-workspace');
|
|
expect(setSelectionStage).not.toHaveBeenCalledWith('custom-world-result');
|
|
});
|
|
|
|
it('作品详情已加载完整编辑资产时列表摘要不能覆盖 selectedDetailEntry', async () => {
|
|
const fullProfile: CustomWorldProfile = {
|
|
...buildProfile('星砂废都'),
|
|
id: 'profile-stardust',
|
|
cover: {
|
|
sourceType: 'default',
|
|
imageSrc: null,
|
|
characterRoleIds: ['playable-shamian'],
|
|
},
|
|
playableNpcs: [
|
|
{
|
|
id: 'playable-shamian',
|
|
name: '砂眠',
|
|
title: '废都引路人',
|
|
role: '主角代理',
|
|
description: '追查旧约的人。',
|
|
backstory: '从星砂潮汐里醒来。',
|
|
personality: '冷静。',
|
|
motivation: '找到旧约。',
|
|
combatStyle: '踏砂突进。',
|
|
initialAffinity: 45,
|
|
relationshipHooks: [],
|
|
tags: [],
|
|
relations: [],
|
|
backstoryReveal: {
|
|
publicSummary: '废都引路人。',
|
|
privateChatUnlockAffinity: 60,
|
|
chapters: [],
|
|
},
|
|
skills: [
|
|
{
|
|
id: 'skill-star-step',
|
|
name: '星砂步',
|
|
summary: '踏砂突进。',
|
|
style: 'mobility',
|
|
actionPreviewConfig: {
|
|
folder: 'characters/shamian',
|
|
prefix: 'skill_',
|
|
frames: 8,
|
|
basePath: '/assets/custom-world/shamian/skill',
|
|
previewVideoPath: '/assets/custom-world/shamian/skill.mp4',
|
|
},
|
|
},
|
|
],
|
|
initialItems: [
|
|
{
|
|
id: 'item-sand-compass',
|
|
name: '星砂罗盘',
|
|
category: '专属物品',
|
|
quantity: 1,
|
|
rarity: 'rare',
|
|
description: '能指出旧约埋藏方向。',
|
|
tags: [],
|
|
iconSrc: '/assets/custom-world/items/sand-compass.png',
|
|
},
|
|
],
|
|
imageSrc: '/assets/custom-world/playable-shamian.png',
|
|
attributeProfile: {
|
|
schemaId: 'schema-星砂废都',
|
|
values: { axis_a: 8 },
|
|
topTraits: ['星砂共鸣'],
|
|
evidence: [
|
|
{ slotId: 'axis_a', reason: '能听见星砂潮汐。' },
|
|
],
|
|
},
|
|
},
|
|
],
|
|
sceneChapterBlueprints: [
|
|
{
|
|
id: 'scene-chapter-clocktower',
|
|
sceneId: 'landmark-clocktower',
|
|
title: '钟楼第一夜',
|
|
summary: '钟楼第一夜。',
|
|
sceneTaskDescription: '进入钟楼。',
|
|
linkedThreadIds: [],
|
|
linkedLandmarkIds: ['landmark-clocktower'],
|
|
acts: [
|
|
{
|
|
id: 'act-clocktower-opening',
|
|
sceneId: 'landmark-clocktower',
|
|
title: '第一幕',
|
|
summary: '砂眠带玩家进入坠星钟楼。',
|
|
stageCoverage: ['opening'],
|
|
backgroundImageSrc:
|
|
'/assets/custom-world/act-clocktower-opening.png',
|
|
backgroundAssetId: 'asset-act-clocktower-opening',
|
|
encounterNpcIds: ['playable-shamian'],
|
|
primaryNpcId: 'playable-shamian',
|
|
oppositeNpcId: 'playable-shamian',
|
|
eventDescription: '钟楼旧铃忽然自鸣。',
|
|
linkedThreadIds: ['thread-old-vow'],
|
|
advanceRule: 'after_primary_contact',
|
|
actGoal: '进入钟楼。',
|
|
transitionHook: '星砂开始倒流。',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
const summaryProfile: CustomWorldProfile = {
|
|
...fullProfile,
|
|
cover: null,
|
|
playableNpcs: [
|
|
{
|
|
...fullProfile.playableNpcs[0]!,
|
|
skills: [
|
|
{
|
|
id: 'skill-star-step',
|
|
name: '星砂步',
|
|
summary: '踏砂突进。',
|
|
style: 'mobility',
|
|
},
|
|
],
|
|
initialItems: [
|
|
{
|
|
...fullProfile.playableNpcs[0]!.initialItems[0]!,
|
|
iconSrc: undefined,
|
|
},
|
|
],
|
|
imageSrc: undefined,
|
|
attributeProfile: undefined,
|
|
},
|
|
],
|
|
sceneChapterBlueprints: [
|
|
{
|
|
...fullProfile.sceneChapterBlueprints![0]!,
|
|
acts: [
|
|
{
|
|
...fullProfile.sceneChapterBlueprints![0]!.acts[0]!,
|
|
backgroundImageSrc: undefined,
|
|
backgroundAssetId: undefined,
|
|
linkedThreadIds: [],
|
|
actGoal: '',
|
|
transitionHook: '',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
const detailEntry = buildLibraryEntry(fullProfile);
|
|
const summaryEntry = buildLibraryEntry(summaryProfile);
|
|
const selectedEntries: CustomWorldLibraryEntry<CustomWorldProfile>[] = [];
|
|
|
|
function Harness() {
|
|
const [selectedDetailEntry, setSelectedDetailEntry] = useState<
|
|
CustomWorldLibraryEntry<CustomWorldProfile> | null
|
|
>(detailEntry);
|
|
useEffect(() => {
|
|
if (selectedDetailEntry) {
|
|
selectedEntries.push(selectedDetailEntry);
|
|
}
|
|
}, [selectedDetailEntry]);
|
|
useRpgEntryLibraryDetail({
|
|
userId: 'user-1',
|
|
selectedDetailEntry,
|
|
setSelectedDetailEntry,
|
|
savedCustomWorldEntries: [summaryEntry],
|
|
setSavedCustomWorldEntries: vi.fn(),
|
|
setGeneratedCustomWorldProfile: vi.fn(),
|
|
setCustomWorldError: vi.fn(),
|
|
setCustomWorldAutoSaveError: vi.fn(),
|
|
setCustomWorldAutoSaveState: vi.fn(),
|
|
setCustomWorldGenerationViewSource: vi.fn(),
|
|
setCustomWorldResultViewSource: vi.fn(),
|
|
setSelectionStage: vi.fn(),
|
|
setPlatformTabToCreate: vi.fn(),
|
|
setPlatformTabToDraft: vi.fn(),
|
|
setPlatformError: vi.fn(),
|
|
appendBrowseHistoryEntry: vi.fn(async () => {}),
|
|
refreshCustomWorldWorks: vi.fn(async () => []),
|
|
refreshPublishedGallery: vi.fn(async () => []),
|
|
persistAgentUiState: vi.fn(),
|
|
syncAgentCreationResultView: vi.fn(),
|
|
buildDraftResultProfile: () => null,
|
|
suppressAgentDraftResultAutoOpen: vi.fn(),
|
|
releaseAgentDraftResultAutoOpenSuppression: vi.fn(),
|
|
resetAutoSaveTrackingToIdle: vi.fn(),
|
|
markAutoSavedProfile: vi.fn(),
|
|
});
|
|
return null;
|
|
}
|
|
|
|
render(<Harness />);
|
|
|
|
await act(async () => {});
|
|
const lastSelected = selectedEntries.at(-1);
|
|
expect(lastSelected?.profile.cover?.characterRoleIds).toEqual([
|
|
'playable-shamian',
|
|
]);
|
|
expect(
|
|
lastSelected?.profile.playableNpcs[0]?.skills[0]?.actionPreviewConfig
|
|
?.previewVideoPath,
|
|
).toBe('/assets/custom-world/shamian/skill.mp4');
|
|
expect(lastSelected?.profile.playableNpcs[0]?.initialItems[0]?.iconSrc).toBe(
|
|
'/assets/custom-world/items/sand-compass.png',
|
|
);
|
|
expect(lastSelected?.profile.playableNpcs[0]?.attributeProfile).toBeTruthy();
|
|
expect(
|
|
lastSelected?.profile.sceneChapterBlueprints?.[0]?.acts[0]
|
|
?.backgroundImageSrc,
|
|
).toBe('/assets/custom-world/act-clocktower-opening.png');
|
|
});
|
|
|
|
it('默认封面和角色编辑结构差异也不能被列表摘要覆盖', async () => {
|
|
const fullRole = {
|
|
id: 'playable-shamian',
|
|
name: '砂眠',
|
|
title: '废都引路人',
|
|
role: '主角代理',
|
|
description: '追查旧约的人。',
|
|
backstory: '从星砂潮汐里醒来。',
|
|
personality: '冷静。',
|
|
motivation: '找到旧约。',
|
|
combatStyle: '踏砂突进。',
|
|
initialAffinity: 45,
|
|
relationshipHooks: [],
|
|
tags: [],
|
|
relations: [],
|
|
backstoryReveal: {
|
|
publicSummary: '废都引路人。',
|
|
privateChatUnlockAffinity: 60,
|
|
chapters: [],
|
|
},
|
|
skills: [
|
|
{
|
|
id: 'skill-star-step',
|
|
name: '星砂步',
|
|
summary: '踏砂突进。',
|
|
style: 'mobility',
|
|
actionPreviewConfig: {
|
|
folder: 'characters/shamian',
|
|
prefix: 'skill_',
|
|
frames: 8,
|
|
basePath: '/assets/custom-world/shamian/skill',
|
|
previewVideoPath: '/assets/custom-world/shamian/skill.mp4',
|
|
},
|
|
},
|
|
],
|
|
initialItems: [
|
|
{
|
|
id: 'item-sand-compass',
|
|
name: '星砂罗盘',
|
|
category: '专属物品',
|
|
quantity: 1,
|
|
rarity: 'rare',
|
|
description: '能指出旧约埋藏方向。',
|
|
tags: [],
|
|
iconSrc: '/assets/custom-world/items/sand-compass.png',
|
|
},
|
|
],
|
|
attributeProfile: {
|
|
schemaId: 'schema-星砂废都',
|
|
values: { axis_a: 8 },
|
|
topTraits: ['星砂共鸣'],
|
|
evidence: [
|
|
{ slotId: 'axis_a', reason: '能听见星砂潮汐。' },
|
|
],
|
|
},
|
|
} satisfies CustomWorldProfile['playableNpcs'][number];
|
|
const fullProfile: CustomWorldProfile = {
|
|
...buildProfile('星砂废都'),
|
|
id: 'profile-stardust-structure',
|
|
cover: {
|
|
sourceType: 'default',
|
|
imageSrc: null,
|
|
characterRoleIds: ['playable-shamian'],
|
|
},
|
|
playableNpcs: [fullRole],
|
|
};
|
|
const summaryProfile: CustomWorldProfile = {
|
|
...fullProfile,
|
|
cover: null,
|
|
playableNpcs: [
|
|
{
|
|
...fullRole,
|
|
skills: [
|
|
{
|
|
id: 'skill-star-step',
|
|
name: '星砂步',
|
|
summary: '踏砂突进。',
|
|
style: 'mobility',
|
|
},
|
|
],
|
|
initialItems: [
|
|
{
|
|
...fullRole.initialItems[0]!,
|
|
iconSrc: undefined,
|
|
},
|
|
],
|
|
attributeProfile: undefined,
|
|
},
|
|
],
|
|
};
|
|
const detailEntry = buildLibraryEntry(fullProfile);
|
|
const summaryEntry = buildLibraryEntry(summaryProfile);
|
|
const selectedEntries: CustomWorldLibraryEntry<CustomWorldProfile>[] = [];
|
|
|
|
function Harness() {
|
|
const [selectedDetailEntry, setSelectedDetailEntry] = useState<
|
|
CustomWorldLibraryEntry<CustomWorldProfile> | null
|
|
>(detailEntry);
|
|
useEffect(() => {
|
|
if (selectedDetailEntry) {
|
|
selectedEntries.push(selectedDetailEntry);
|
|
}
|
|
}, [selectedDetailEntry]);
|
|
useRpgEntryLibraryDetail({
|
|
userId: 'user-1',
|
|
selectedDetailEntry,
|
|
setSelectedDetailEntry,
|
|
savedCustomWorldEntries: [summaryEntry],
|
|
setSavedCustomWorldEntries: vi.fn(),
|
|
setGeneratedCustomWorldProfile: vi.fn(),
|
|
setCustomWorldError: vi.fn(),
|
|
setCustomWorldAutoSaveError: vi.fn(),
|
|
setCustomWorldAutoSaveState: vi.fn(),
|
|
setCustomWorldGenerationViewSource: vi.fn(),
|
|
setCustomWorldResultViewSource: vi.fn(),
|
|
setSelectionStage: vi.fn(),
|
|
setPlatformTabToCreate: vi.fn(),
|
|
setPlatformTabToDraft: vi.fn(),
|
|
setPlatformError: vi.fn(),
|
|
appendBrowseHistoryEntry: vi.fn(async () => {}),
|
|
refreshCustomWorldWorks: vi.fn(async () => []),
|
|
refreshPublishedGallery: vi.fn(async () => []),
|
|
persistAgentUiState: vi.fn(),
|
|
syncAgentCreationResultView: vi.fn(),
|
|
buildDraftResultProfile: () => null,
|
|
suppressAgentDraftResultAutoOpen: vi.fn(),
|
|
releaseAgentDraftResultAutoOpenSuppression: vi.fn(),
|
|
resetAutoSaveTrackingToIdle: vi.fn(),
|
|
markAutoSavedProfile: vi.fn(),
|
|
});
|
|
return null;
|
|
}
|
|
|
|
render(<Harness />);
|
|
|
|
await act(async () => {});
|
|
const lastSelected = selectedEntries.at(-1);
|
|
expect(lastSelected?.profile.cover?.characterRoleIds).toEqual([
|
|
'playable-shamian',
|
|
]);
|
|
expect(
|
|
lastSelected?.profile.playableNpcs[0]?.skills[0]?.actionPreviewConfig
|
|
?.previewVideoPath,
|
|
).toBe('/assets/custom-world/shamian/skill.mp4');
|
|
expect(lastSelected?.profile.playableNpcs[0]?.initialItems[0]?.iconSrc).toBe(
|
|
'/assets/custom-world/items/sand-compass.png',
|
|
);
|
|
expect(lastSelected?.profile.playableNpcs[0]?.attributeProfile?.values).toEqual(
|
|
{ axis_a: 8 },
|
|
);
|
|
});
|
|
|
|
it('详情删除由上层确认弹窗触发,hook 不再调用原生 confirm', async () => {
|
|
const draftProfile = buildProfile('潮雾列岛');
|
|
const selectedEntry = {
|
|
...buildLibraryEntry(draftProfile),
|
|
visibility: 'draft' as const,
|
|
publishedAt: null,
|
|
worldName: '潮雾列岛',
|
|
};
|
|
const confirmSpy = vi
|
|
.spyOn(window, 'confirm')
|
|
.mockImplementation(() => false);
|
|
const setSelectedDetailEntry = vi.fn();
|
|
const setSavedCustomWorldEntries = vi.fn();
|
|
const refreshCustomWorldWorks = vi.fn(async () => []);
|
|
const refreshPublishedGallery = vi.fn(async () => []);
|
|
const setSelectionStage = vi.fn();
|
|
let deleteSelectedWorld: (() => Promise<void>) | null = null;
|
|
|
|
vi.mocked(deleteRpgEntryWorldProfile).mockResolvedValue([]);
|
|
|
|
function Harness() {
|
|
deleteSelectedWorld = useRpgEntryLibraryDetail({
|
|
userId: 'user-1',
|
|
selectedDetailEntry: selectedEntry,
|
|
setSelectedDetailEntry,
|
|
savedCustomWorldEntries: [selectedEntry],
|
|
setSavedCustomWorldEntries,
|
|
setGeneratedCustomWorldProfile: vi.fn(),
|
|
setCustomWorldError: vi.fn(),
|
|
setCustomWorldAutoSaveError: vi.fn(),
|
|
setCustomWorldAutoSaveState: vi.fn(),
|
|
setCustomWorldGenerationViewSource: vi.fn(),
|
|
setCustomWorldResultViewSource: vi.fn(),
|
|
setSelectionStage,
|
|
setPlatformTabToCreate: vi.fn(),
|
|
setPlatformTabToDraft: vi.fn(),
|
|
setPlatformError: vi.fn(),
|
|
appendBrowseHistoryEntry: vi.fn(async () => {}),
|
|
refreshCustomWorldWorks,
|
|
refreshPublishedGallery,
|
|
persistAgentUiState: vi.fn(),
|
|
syncAgentCreationResultView: vi.fn(),
|
|
buildDraftResultProfile: () => null,
|
|
suppressAgentDraftResultAutoOpen: vi.fn(),
|
|
releaseAgentDraftResultAutoOpenSuppression: vi.fn(),
|
|
resetAutoSaveTrackingToIdle: vi.fn(),
|
|
markAutoSavedProfile: vi.fn(),
|
|
}).handleDeleteSelectedWorld;
|
|
|
|
return null;
|
|
}
|
|
|
|
render(<Harness />);
|
|
|
|
await act(async () => {
|
|
await deleteSelectedWorld?.();
|
|
});
|
|
|
|
expect(confirmSpy).not.toHaveBeenCalled();
|
|
expect(deleteRpgEntryWorldProfile).toHaveBeenCalledWith('profile-潮雾列岛');
|
|
expect(setSelectedDetailEntry).toHaveBeenCalledWith(null);
|
|
expect(setSavedCustomWorldEntries).toHaveBeenCalledWith([]);
|
|
expect(refreshCustomWorldWorks).toHaveBeenCalled();
|
|
expect(refreshPublishedGallery).toHaveBeenCalled();
|
|
expect(setSelectionStage).toHaveBeenCalledWith('platform');
|
|
|
|
confirmSpy.mockRestore();
|
|
});
|
|
|
|
it('Agent 结果页自动保存先回写 session,再保存后端 result-view profile', async () => {
|
|
const oldProfile = buildProfile('旧前端快照');
|
|
const latestProfile = {
|
|
...buildProfile('服务端草稿快照'),
|
|
summary: '自动保存应保存这份 session 最新草稿。',
|
|
};
|
|
const latestSession = buildSession({
|
|
stage: 'object_refining',
|
|
draftProfile: latestProfile as unknown as Record<string, unknown>,
|
|
});
|
|
const syncAgentSessionSnapshot = vi.fn(async () => latestSession);
|
|
const syncAgentCreationResultView = vi.fn(async () =>
|
|
buildResultView({
|
|
session: latestSession,
|
|
profile: latestProfile,
|
|
profileSource: 'result_preview',
|
|
targetStage: 'custom-world-result',
|
|
resultViewSource: 'agent-draft',
|
|
canAutosaveLibrary: true,
|
|
canSyncResultProfile: true,
|
|
recoveryAction: 'open_result',
|
|
}),
|
|
);
|
|
vi.mocked(executeRpgCreationAction).mockResolvedValue({
|
|
operation: {
|
|
operationId: 'operation-sync-result',
|
|
type: 'sync_result_profile',
|
|
status: 'running',
|
|
phaseLabel: '结果页同步中',
|
|
phaseDetail: '正在同步结果页。',
|
|
progress: 50,
|
|
},
|
|
});
|
|
vi.mocked(getRpgCreationOperation).mockResolvedValue({
|
|
operationId: 'operation-sync-result',
|
|
type: 'sync_result_profile',
|
|
status: 'completed',
|
|
phaseLabel: '结果页已同步',
|
|
phaseDetail: '结果页已同步。',
|
|
progress: 100,
|
|
});
|
|
|
|
vi.mocked(upsertRpgWorldProfile).mockResolvedValue({
|
|
entry: {
|
|
ownerUserId: 'user-1',
|
|
profileId: latestProfile.id,
|
|
publicWorkCode: null,
|
|
authorPublicUserCode: null,
|
|
profile: latestProfile,
|
|
visibility: 'draft',
|
|
publishedAt: null,
|
|
updatedAt: '2026-04-25T00:00:00.000Z',
|
|
authorDisplayName: '测试玩家',
|
|
worldName: latestProfile.name,
|
|
subtitle: latestProfile.subtitle,
|
|
summaryText: latestProfile.summary,
|
|
coverImageSrc: null,
|
|
themeMode: 'tide',
|
|
playableNpcCount: 0,
|
|
landmarkCount: 0,
|
|
likeCount: 0,
|
|
},
|
|
entries: [],
|
|
});
|
|
|
|
function Harness() {
|
|
useRpgCreationResultAutosave({
|
|
selectionStage: 'custom-world-result',
|
|
activeAgentSessionId: 'agent-session-1',
|
|
generatedCustomWorldProfile: oldProfile,
|
|
isAgentDraftResultView: true,
|
|
userId: 'user-1',
|
|
setGeneratedCustomWorldProfile: vi.fn(),
|
|
setAgentOperation: vi.fn(),
|
|
setSavedCustomWorldEntries: vi.fn(),
|
|
setSelectedDetailEntry: vi.fn(),
|
|
refreshCustomWorldWorks: vi.fn(async () => []),
|
|
persistAgentUiState: vi.fn(),
|
|
syncAgentSessionSnapshot,
|
|
syncAgentCreationResultView,
|
|
buildDraftResultProfile: (view) =>
|
|
(view?.profile as CustomWorldProfile | null) ?? null,
|
|
});
|
|
|
|
return null;
|
|
}
|
|
|
|
vi.useFakeTimers();
|
|
render(<Harness />);
|
|
|
|
await act(async () => {
|
|
await vi.advanceTimersByTimeAsync(650);
|
|
});
|
|
vi.useRealTimers();
|
|
|
|
expect(syncAgentSessionSnapshot).toHaveBeenCalledWith('agent-session-1');
|
|
expect(syncAgentCreationResultView).toHaveBeenCalledWith('agent-session-1');
|
|
expect(executeRpgCreationAction).toHaveBeenCalledWith('agent-session-1', {
|
|
action: 'sync_result_profile',
|
|
profile: expect.objectContaining({
|
|
id: oldProfile.id,
|
|
name: oldProfile.name,
|
|
}),
|
|
});
|
|
expect(upsertRpgWorldProfile).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
id: latestProfile.id,
|
|
name: latestProfile.name,
|
|
summary: latestProfile.summary,
|
|
}),
|
|
{
|
|
sourceAgentSessionId: 'agent-session-1',
|
|
},
|
|
);
|
|
});
|
|
});
|