1192 lines
38 KiB
TypeScript
1192 lines
38 KiB
TypeScript
/* @vitest-environment jsdom */
|
||
|
||
import { render, screen, waitFor } from '@testing-library/react';
|
||
import userEvent from '@testing-library/user-event';
|
||
import { useState } from 'react';
|
||
import { beforeEach, expect, test, vi } from 'vitest';
|
||
|
||
import type { CustomWorldAgentSessionSnapshot } from '../../../packages/shared/src/contracts/customWorldAgent';
|
||
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
||
import {
|
||
createCustomWorldAgentSession,
|
||
executeCustomWorldAgentAction,
|
||
getCustomWorldAgentOperation,
|
||
getCustomWorldAgentSession,
|
||
listCustomWorldWorks,
|
||
streamCustomWorldAgentMessage,
|
||
} from '../../services/aiService';
|
||
import type { AuthUser } from '../../services/authService';
|
||
import {
|
||
clearProfileBrowseHistory,
|
||
deleteCustomWorldProfile,
|
||
getCustomWorldGalleryDetail,
|
||
getProfileDashboard,
|
||
listCustomWorldGallery,
|
||
listCustomWorldLibrary,
|
||
listProfileBrowseHistory,
|
||
listProfileSaveArchives,
|
||
resumeProfileSaveArchive,
|
||
upsertCustomWorldProfile,
|
||
upsertProfileBrowseHistory,
|
||
} from '../../services/storageService';
|
||
import type { GameState } from '../../types';
|
||
import {
|
||
AuthUiContext,
|
||
type PlatformSettingsSection,
|
||
} from '../auth/AuthUiContext';
|
||
import {
|
||
PreGameSelectionFlow,
|
||
type SelectionStage,
|
||
} from './PreGameSelectionFlow';
|
||
|
||
async function clickFirstButtonByName(
|
||
user: ReturnType<typeof userEvent.setup>,
|
||
name: string | RegExp,
|
||
) {
|
||
const buttons = screen.getAllByRole('button', { name });
|
||
await user.click(buttons[0]!);
|
||
}
|
||
|
||
async function clickFirstAsyncButtonByName(
|
||
user: ReturnType<typeof userEvent.setup>,
|
||
name: string | RegExp,
|
||
) {
|
||
const buttons = await screen.findAllByRole('button', { name });
|
||
await user.click(buttons[0]!);
|
||
}
|
||
|
||
async function openCreationHub(user: ReturnType<typeof userEvent.setup>) {
|
||
await clickFirstButtonByName(user, '创作');
|
||
expect(await screen.findByText('创作中心')).toBeTruthy();
|
||
}
|
||
|
||
async function openNewRpgCreation(
|
||
user: ReturnType<typeof userEvent.setup>,
|
||
) {
|
||
await openCreationHub(user);
|
||
const createButtons = await screen.findAllByRole('button', {
|
||
name: /新建作品/u,
|
||
});
|
||
await user.click(createButtons.at(-1)!);
|
||
expect(screen.getByText('选择创作类型')).toBeTruthy();
|
||
await user.click(screen.getByRole('button', { name: /角色扮演 RPG/u }));
|
||
}
|
||
|
||
vi.mock('../../services/aiService', () => ({
|
||
createCustomWorldAgentSession: vi.fn(),
|
||
executeCustomWorldAgentAction: vi.fn(),
|
||
generateCustomWorldProfile: vi.fn(),
|
||
getCustomWorldAgentOperation: vi.fn(),
|
||
getCustomWorldAgentSession: vi.fn(),
|
||
listCustomWorldWorks: vi.fn(),
|
||
streamCustomWorldAgentMessage: vi.fn(),
|
||
}));
|
||
|
||
vi.mock('../../services/storageService', () => ({
|
||
clearProfileBrowseHistory: vi.fn(),
|
||
deleteCustomWorldProfile: vi.fn(),
|
||
getCustomWorldGalleryDetail: vi.fn(),
|
||
getProfileDashboard: vi.fn(),
|
||
listCustomWorldGallery: vi.fn(),
|
||
listCustomWorldLibrary: vi.fn(),
|
||
listProfileBrowseHistory: vi.fn(),
|
||
listProfileSaveArchives: vi.fn(),
|
||
publishCustomWorldProfile: vi.fn(),
|
||
resumeProfileSaveArchive: vi.fn(),
|
||
syncProfileBrowseHistory: vi.fn(),
|
||
unpublishCustomWorldProfile: vi.fn(),
|
||
upsertProfileBrowseHistory: vi.fn(),
|
||
upsertCustomWorldProfile: vi.fn(),
|
||
}));
|
||
|
||
vi.mock('../custom-world-agent/CustomWorldAgentWorkspace', () => ({
|
||
CustomWorldAgentWorkspace: ({
|
||
session,
|
||
onExecuteAction,
|
||
}: {
|
||
session: CustomWorldAgentSessionSnapshot | null;
|
||
onExecuteAction: (payload: { action: string }) => void;
|
||
}) => (
|
||
<div className="agent-workspace-mock">
|
||
Agent工作区:{session?.sessionId ?? 'missing-session'}
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
onExecuteAction({
|
||
action: 'draft_foundation',
|
||
});
|
||
}}
|
||
>
|
||
开始生成草稿
|
||
</button>
|
||
</div>
|
||
),
|
||
}));
|
||
|
||
const mockSession: CustomWorldAgentSessionSnapshot = {
|
||
sessionId: 'custom-world-agent-session-1',
|
||
currentTurn: 0,
|
||
anchorContent: {
|
||
worldPromise: {
|
||
hook: '被海雾吞没的旧航路群岛。',
|
||
differentiator: '灯塔与禁航令共同决定谁能穿过死潮。',
|
||
desiredExperience: '压抑、潮湿、悬疑',
|
||
},
|
||
playerFantasy: {
|
||
playerRole: '玩家是被迫返乡的守灯人继承者。',
|
||
corePursuit: '查清沉船夜与假航灯的关系。',
|
||
fearOfLoss: '失去家族最后一条可信航线。',
|
||
},
|
||
themeBoundary: {
|
||
toneKeywords: ['压抑', '悬疑'],
|
||
aestheticDirectives: ['潮湿群岛', '冷雾港口'],
|
||
forbiddenDirectives: ['轻喜冒险'],
|
||
},
|
||
playerEntryPoint: {
|
||
openingIdentity: '返乡守灯人继承者',
|
||
openingProblem: '回港首夜撞见禁航区假航灯重亮',
|
||
entryMotivation: '阻止更多船只误入死潮',
|
||
},
|
||
coreConflict: {
|
||
surfaceConflicts: ['守灯会与航运公会争夺航路解释权'],
|
||
hiddenCrisis: '有人在借假航灯持续清洗旧案证据',
|
||
firstTouchedConflict: '玩家返乡当夜就被卷进封航冲突',
|
||
},
|
||
keyRelationships: [
|
||
{
|
||
pairs: '玩家 vs 沈砺',
|
||
relationshipType: '旧友互疑',
|
||
secretOrCost: '他知道沉船夜的另一半真相',
|
||
},
|
||
],
|
||
hiddenLines: {
|
||
hiddenTruths: ['沉船夜与假航灯骗局属于同一操盘链条'],
|
||
misdirectionHints: ['表面像海雾自然失控'],
|
||
revealPacing: '先见异常,再见旧案,再见操盘者',
|
||
},
|
||
iconicElements: {
|
||
iconicMotifs: ['假航灯', '沉钟回响'],
|
||
institutionsOrArtifacts: ['旧灯塔', '禁航碑'],
|
||
hardRules: ['错误航灯会把船引进必死水域'],
|
||
},
|
||
},
|
||
progressPercent: 0,
|
||
lastAssistantReply: '先告诉我你想做一个怎样的 RPG 世界。',
|
||
stage: 'clarifying',
|
||
focusCardId: null,
|
||
creatorIntent: {},
|
||
creatorIntentReadiness: {
|
||
isReady: false,
|
||
completedKeys: ['world_hook'],
|
||
missingKeys: [
|
||
'player_premise',
|
||
'theme_and_tone',
|
||
'core_conflict',
|
||
'relationship_seed',
|
||
'iconic_element',
|
||
],
|
||
},
|
||
anchorPack: {},
|
||
lockState: {},
|
||
draftProfile: null,
|
||
messages: [
|
||
{
|
||
id: 'message-1',
|
||
role: 'assistant',
|
||
kind: 'summary',
|
||
text: '先告诉我你想做一个怎样的 RPG 世界。',
|
||
createdAt: '2026-04-14T12:00:00.000Z',
|
||
relatedOperationId: null,
|
||
},
|
||
],
|
||
draftCards: [],
|
||
pendingClarifications: [],
|
||
suggestedActions: [],
|
||
recommendedReplies: [],
|
||
qualityFindings: [],
|
||
assetCoverage: {
|
||
roleAssets: [],
|
||
sceneAssets: [],
|
||
allRoleAssetsReady: false,
|
||
allSceneAssetsReady: false,
|
||
},
|
||
updatedAt: '2026-04-14T12:00:00.000Z',
|
||
};
|
||
|
||
const mockAuthUser: AuthUser = {
|
||
id: 'user-1',
|
||
username: 'tester',
|
||
displayName: '测试玩家',
|
||
phoneNumberMasked: null,
|
||
loginMethod: 'password',
|
||
bindingStatus: 'active',
|
||
wechatBound: false,
|
||
};
|
||
|
||
type TestAuthValue = {
|
||
user: AuthUser | null;
|
||
openLoginModal: (postLoginAction?: (() => void) | null) => void;
|
||
requireAuth: (action: () => void) => void;
|
||
openSettingsModal: (section?: PlatformSettingsSection) => void;
|
||
openAccountModal: () => void;
|
||
logout: () => Promise<void>;
|
||
musicVolume: number;
|
||
setMusicVolume: (value: number) => void;
|
||
platformTheme: 'light' | 'dark';
|
||
setPlatformTheme: (theme: 'light' | 'dark') => void;
|
||
isHydratingSettings: boolean;
|
||
isPersistingSettings: boolean;
|
||
settingsError: string | null;
|
||
};
|
||
|
||
function createAuthValue(overrides: Partial<TestAuthValue> = {}): TestAuthValue {
|
||
return {
|
||
user: mockAuthUser,
|
||
openLoginModal: () => {},
|
||
requireAuth: (action) => action(),
|
||
openSettingsModal: () => {},
|
||
openAccountModal: () => {},
|
||
logout: async () => {},
|
||
musicVolume: 0.42,
|
||
setMusicVolume: () => {},
|
||
platformTheme: 'light',
|
||
setPlatformTheme: () => {},
|
||
isHydratingSettings: false,
|
||
isPersistingSettings: false,
|
||
settingsError: null,
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
function TestWrapper({
|
||
withAuth = false,
|
||
authValue,
|
||
onContinueGame,
|
||
}: {
|
||
withAuth?: boolean;
|
||
authValue?: TestAuthValue;
|
||
onContinueGame?: (snapshot?: HydratedSavedGameSnapshot | null) => void;
|
||
} = {}) {
|
||
const [selectionStage, setSelectionStage] =
|
||
useState<SelectionStage>('platform');
|
||
|
||
const content = (
|
||
<PreGameSelectionFlow
|
||
selectionStage={selectionStage}
|
||
setSelectionStage={setSelectionStage}
|
||
gameState={{} as GameState}
|
||
hasSavedGame={false}
|
||
savedSnapshot={null}
|
||
handleContinueGame={onContinueGame ?? (() => {})}
|
||
handleStartNewGame={() => {}}
|
||
handleCustomWorldSelect={() => {}}
|
||
/>
|
||
);
|
||
|
||
if (!withAuth && !authValue) {
|
||
return content;
|
||
}
|
||
|
||
return (
|
||
<AuthUiContext.Provider
|
||
value={authValue ?? createAuthValue()}
|
||
>
|
||
{content}
|
||
</AuthUiContext.Provider>
|
||
);
|
||
}
|
||
|
||
beforeEach(() => {
|
||
vi.clearAllMocks();
|
||
window.history.replaceState(null, '', '/');
|
||
window.sessionStorage.clear();
|
||
window.localStorage.clear();
|
||
vi.mocked(getProfileDashboard).mockResolvedValue({
|
||
walletBalance: 0,
|
||
totalPlayTimeMs: 0,
|
||
playedWorldCount: 0,
|
||
updatedAt: '2026-04-16T12:00:00.000Z',
|
||
});
|
||
vi.mocked(listCustomWorldLibrary).mockResolvedValue([]);
|
||
vi.mocked(listCustomWorldGallery).mockResolvedValue([]);
|
||
vi.mocked(listProfileBrowseHistory).mockResolvedValue([]);
|
||
vi.mocked(listProfileSaveArchives).mockResolvedValue([]);
|
||
vi.mocked(resumeProfileSaveArchive).mockResolvedValue({
|
||
entry: {
|
||
worldKey: 'custom:world-archive-1',
|
||
ownerUserId: null,
|
||
profileId: 'world-archive-1',
|
||
worldType: 'CUSTOM',
|
||
worldName: '潮雾列岛',
|
||
subtitle: '旧灯塔与失控航路',
|
||
summaryText: '回到旧灯塔继续推进调查。',
|
||
coverImageSrc: null,
|
||
lastPlayedAt: '2026-04-19T12:00:00.000Z',
|
||
},
|
||
snapshot: {
|
||
version: 2,
|
||
savedAt: '2026-04-19T12:00:00.000Z',
|
||
bottomTab: 'adventure',
|
||
currentStory: null,
|
||
gameState: {} as GameState,
|
||
} as HydratedSavedGameSnapshot,
|
||
});
|
||
vi.mocked(upsertProfileBrowseHistory).mockResolvedValue([]);
|
||
vi.mocked(clearProfileBrowseHistory).mockResolvedValue([]);
|
||
vi.mocked(deleteCustomWorldProfile).mockResolvedValue([]);
|
||
vi.mocked(upsertCustomWorldProfile).mockResolvedValue({
|
||
entry: {
|
||
ownerUserId: 'user-1',
|
||
profileId: 'agent-draft-custom-world-agent-session-1',
|
||
profile: {
|
||
id: 'agent-draft-custom-world-agent-session-1',
|
||
name: '潮雾列岛',
|
||
} as never,
|
||
visibility: 'draft',
|
||
publishedAt: null,
|
||
updatedAt: '2026-04-14T12:00:00.000Z',
|
||
authorDisplayName: '玩家',
|
||
worldName: '潮雾列岛',
|
||
subtitle: '旧灯塔与失控航路',
|
||
summaryText: '第一版世界底稿已经整理完成。',
|
||
coverImageSrc: null,
|
||
themeMode: 'tide',
|
||
playableNpcCount: 1,
|
||
landmarkCount: 1,
|
||
},
|
||
entries: [],
|
||
});
|
||
vi.mocked(createCustomWorldAgentSession).mockResolvedValue({
|
||
session: mockSession,
|
||
});
|
||
vi.mocked(listCustomWorldWorks).mockResolvedValue([]);
|
||
vi.mocked(executeCustomWorldAgentAction).mockResolvedValue({
|
||
operation: {
|
||
operationId: 'operation-draft-foundation-1',
|
||
type: 'draft_foundation',
|
||
status: 'queued',
|
||
phaseLabel: '已接收请求',
|
||
phaseDetail: '正在准备生成世界底稿。',
|
||
progress: 10,
|
||
error: null,
|
||
},
|
||
});
|
||
vi.mocked(getCustomWorldAgentOperation).mockResolvedValue({
|
||
operationId: 'operation-draft-foundation-1',
|
||
type: 'draft_foundation',
|
||
status: 'running',
|
||
phaseLabel: '生成世界底稿',
|
||
phaseDetail: '正在根据已确认锚点编译第一版世界结构。',
|
||
progress: 38,
|
||
error: null,
|
||
});
|
||
vi.mocked(getCustomWorldAgentSession).mockResolvedValue(mockSession);
|
||
vi.mocked(streamCustomWorldAgentMessage).mockResolvedValue(mockSession);
|
||
});
|
||
|
||
test('create tab opens game type modal, keeps AIRP and visual novel locked, and enters agent workspace for RPG', async () => {
|
||
const user = userEvent.setup();
|
||
|
||
render(<TestWrapper />);
|
||
|
||
await openCreationHub(user);
|
||
const createButtons = await screen.findAllByRole('button', {
|
||
name: /新建作品/u,
|
||
});
|
||
await user.click(createButtons.at(-1)!);
|
||
|
||
expect(screen.getByText('选择创作类型')).toBeTruthy();
|
||
|
||
const airpButton = screen.getByRole('button', { name: /AIRP/u });
|
||
const visualNovelButton = screen.getByRole('button', {
|
||
name: /视觉小说/u,
|
||
});
|
||
|
||
expect((airpButton as HTMLButtonElement).disabled).toBe(true);
|
||
expect((visualNovelButton as HTMLButtonElement).disabled).toBe(true);
|
||
|
||
await user.click(screen.getByRole('button', { name: /角色扮演 RPG/u }));
|
||
|
||
await waitFor(() => {
|
||
expect(createCustomWorldAgentSession).toHaveBeenCalledTimes(1);
|
||
});
|
||
|
||
expect(
|
||
await screen.findByText('Agent工作区:custom-world-agent-session-1'),
|
||
).toBeTruthy();
|
||
});
|
||
|
||
test('create tab uses unified creation hub and can resume an agent draft', async () => {
|
||
const user = userEvent.setup();
|
||
|
||
vi.mocked(listCustomWorldWorks).mockResolvedValue([
|
||
{
|
||
workId: 'draft:custom-world-agent-session-1',
|
||
sourceType: 'agent_session',
|
||
status: 'draft',
|
||
title: '潮雾列岛',
|
||
subtitle: '精修对象',
|
||
summary: '玩家是失职返乡的守灯人。',
|
||
coverImageSrc: null,
|
||
coverRenderMode: 'image',
|
||
coverCharacterImageSrcs: [],
|
||
updatedAt: '2026-04-20T10:00:00.000Z',
|
||
publishedAt: null,
|
||
stage: 'object_refining',
|
||
stageLabel: '精修对象',
|
||
playableNpcCount: 3,
|
||
landmarkCount: 4,
|
||
roleVisualReadyCount: 1,
|
||
roleAnimationReadyCount: 0,
|
||
roleAssetSummaryLabel: '沈砺 · 主图已生成',
|
||
sessionId: 'custom-world-agent-session-1',
|
||
profileId: null,
|
||
canResume: true,
|
||
canEnterWorld: false,
|
||
},
|
||
]);
|
||
|
||
render(<TestWrapper withAuth />);
|
||
|
||
await openCreationHub(user);
|
||
|
||
expect(
|
||
screen.getByRole('button', { name: /继续精修/u }),
|
||
).toBeTruthy();
|
||
expect(screen.getByRole('button', { name: /继续精修/u })).toBeTruthy();
|
||
|
||
await user.click(screen.getByRole('button', { name: /继续精修/u }));
|
||
|
||
expect(
|
||
await screen.findByText('Agent工作区:custom-world-agent-session-1'),
|
||
).toBeTruthy();
|
||
});
|
||
|
||
test('clicking a public work while logged out routes through requireAuth', async () => {
|
||
const user = userEvent.setup();
|
||
const requireAuth = vi.fn();
|
||
|
||
vi.mocked(listCustomWorldGallery).mockResolvedValue([
|
||
{
|
||
ownerUserId: 'author-1',
|
||
profileId: 'world-public-1',
|
||
visibility: 'published',
|
||
publishedAt: '2026-04-16T12:00:00.000Z',
|
||
updatedAt: '2026-04-16T12:00:00.000Z',
|
||
worldName: '潮雾列岛',
|
||
subtitle: '旧灯塔与失控航路',
|
||
summaryText: '最近公开发布的世界。',
|
||
coverImageSrc: null,
|
||
themeMode: 'tide',
|
||
authorDisplayName: '潮汐作者',
|
||
playableNpcCount: 3,
|
||
landmarkCount: 4,
|
||
},
|
||
]);
|
||
|
||
render(
|
||
<TestWrapper
|
||
authValue={createAuthValue({
|
||
user: null,
|
||
openLoginModal: () => {},
|
||
requireAuth,
|
||
})}
|
||
/>,
|
||
);
|
||
|
||
const workCards = await screen.findAllByRole('button', {
|
||
name: /潮雾列岛/u,
|
||
});
|
||
await user.click(workCards[0]!);
|
||
|
||
expect(requireAuth).toHaveBeenCalledTimes(1);
|
||
expect(getCustomWorldGalleryDetail).not.toHaveBeenCalled();
|
||
});
|
||
|
||
test('selecting RPG creation while logged out routes through requireAuth', async () => {
|
||
const user = userEvent.setup();
|
||
const requireAuth = vi.fn();
|
||
|
||
render(
|
||
<TestWrapper
|
||
authValue={createAuthValue({
|
||
user: null,
|
||
openLoginModal: () => {},
|
||
requireAuth,
|
||
})}
|
||
/>,
|
||
);
|
||
|
||
await openNewRpgCreation(user);
|
||
expect(requireAuth).toHaveBeenCalledTimes(1);
|
||
expect(createCustomWorldAgentSession).not.toHaveBeenCalled();
|
||
});
|
||
|
||
test('starting draft generation leaves the agent workspace and shows the generation progress view', async () => {
|
||
const user = userEvent.setup();
|
||
|
||
render(<TestWrapper />);
|
||
|
||
await openNewRpgCreation(user);
|
||
|
||
expect(
|
||
await screen.findByText('Agent工作区:custom-world-agent-session-1'),
|
||
).toBeTruthy();
|
||
|
||
await user.click(screen.getByRole('button', { name: '开始生成草稿' }));
|
||
|
||
await waitFor(() => {
|
||
expect(executeCustomWorldAgentAction).toHaveBeenCalledWith(
|
||
'custom-world-agent-session-1',
|
||
{
|
||
action: 'draft_foundation',
|
||
},
|
||
);
|
||
});
|
||
|
||
expect(await screen.findByText('世界草稿生成进度')).toBeTruthy();
|
||
expect(screen.queryByText(/Agent工作区/u)).toBeNull();
|
||
expect(screen.getAllByText('生成世界底稿').length).toBeGreaterThan(0);
|
||
expect(screen.getByText('当前世界信息')).toBeTruthy();
|
||
expect(screen.queryByText('回到工作区')).toBeNull();
|
||
expect(screen.getByText('世界承诺')).toBeTruthy();
|
||
expect(screen.getByText(/灯塔与禁航令共同决定谁能穿过死潮/u)).toBeTruthy();
|
||
expect(screen.queryByText('先告诉我你想做一个怎样的 RPG 世界。')).toBeNull();
|
||
});
|
||
|
||
test('existing draft sessions enter the agent preview layout without opening legacy editor', async () => {
|
||
const user = userEvent.setup();
|
||
|
||
vi.mocked(getCustomWorldAgentOperation).mockResolvedValue({
|
||
operationId: 'operation-draft-foundation-1',
|
||
type: 'draft_foundation',
|
||
status: 'completed',
|
||
phaseLabel: '世界底稿已生成',
|
||
phaseDetail: '第一版世界底稿和 4 张草稿卡已经整理完成。',
|
||
progress: 100,
|
||
error: null,
|
||
});
|
||
vi.mocked(getCustomWorldAgentSession).mockResolvedValue({
|
||
...mockSession,
|
||
stage: 'object_refining',
|
||
creatorIntent: {
|
||
sourceMode: 'card',
|
||
worldHook: '被海雾吞没的旧航路群岛',
|
||
playerPremise: '玩家回到群岛调查沉船真相。',
|
||
themeKeywords: ['海雾', '旧航路'],
|
||
toneDirectives: ['压抑', '悬疑'],
|
||
openingSituation: '首夜就有陌生船只闯入禁航区。',
|
||
coreConflicts: ['航运公会与守灯会争夺航路控制权'],
|
||
keyFactions: [],
|
||
keyCharacters: [],
|
||
keyLandmarks: [],
|
||
iconicElements: ['会移动的海雾'],
|
||
forbiddenDirectives: [],
|
||
rawSettingText: '',
|
||
},
|
||
draftProfile: {
|
||
name: '潮雾列岛',
|
||
subtitle: '旧灯塔与失控航路',
|
||
summary: '第一版世界底稿已经整理完成。',
|
||
tone: '压抑、潮湿、悬疑',
|
||
playerGoal: '查清沉船与禁航区异动的真相。',
|
||
majorFactions: ['守灯会', '航运公会'],
|
||
coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
|
||
playableNpcs: [
|
||
{
|
||
id: 'playable-1',
|
||
name: '沈砺',
|
||
title: '旧航路引路人',
|
||
role: '关键同行者',
|
||
publicIdentity: '最熟悉旧航路的人。',
|
||
publicMask: '看上去像可靠旧友。',
|
||
currentPressure: '他必须在两股势力间站队。',
|
||
hiddenHook: '暗中替沉船商盟引路。',
|
||
relationToPlayer: '旧友兼潜在背叛者',
|
||
threadIds: ['thread-1'],
|
||
summary: '他像旧友,但也像一把始终没收回鞘的刀。',
|
||
},
|
||
],
|
||
storyNpcs: [
|
||
{
|
||
id: 'story-1',
|
||
name: '顾潮音',
|
||
title: '守灯会值夜人',
|
||
role: '场景关键角色',
|
||
publicIdentity: '负责夜间巡灯与封锁。',
|
||
publicMask: '对外一直冷静克制。',
|
||
currentPressure: '她知道更多禁航区真相。',
|
||
hiddenHook: '曾亲眼见过失控海雾吞船。',
|
||
relationToPlayer: '最早愿意交换线索的人',
|
||
threadIds: ['thread-1'],
|
||
summary: '她总像比所有人更早知道海雾会往哪一侧压下来。',
|
||
},
|
||
],
|
||
landmarks: [
|
||
{
|
||
id: 'landmark-1',
|
||
name: '回潮旧灯塔',
|
||
purpose: '观察雾潮与往来船只',
|
||
mood: '潮湿、压抑、风声不止',
|
||
importance: '开局核心场景',
|
||
characterIds: ['story-1'],
|
||
threadIds: ['thread-1'],
|
||
summary: '旧灯塔是整片群岛最先看见异动的地方。',
|
||
},
|
||
],
|
||
factions: [],
|
||
threads: [],
|
||
chapters: [],
|
||
worldHook: '被海雾吞没的旧航路群岛',
|
||
playerPremise: '玩家回到群岛调查沉船真相。',
|
||
openingSituation: '首夜就有陌生船只闯入禁航区。',
|
||
iconicElements: ['会移动的海雾'],
|
||
sourceAnchorSummary: '海雾、旧灯塔、失控航路。',
|
||
},
|
||
draftCards: [
|
||
{
|
||
id: 'world-foundation',
|
||
kind: 'world',
|
||
title: '潮雾列岛',
|
||
subtitle: '旧灯塔与失控航路',
|
||
summary: '第一版世界底稿已经整理完成。',
|
||
status: 'warning',
|
||
linkedIds: ['playable-1', 'story-1', 'landmark-1'],
|
||
warningCount: 0,
|
||
},
|
||
],
|
||
});
|
||
|
||
render(<TestWrapper />);
|
||
|
||
await openNewRpgCreation(user);
|
||
|
||
await waitFor(
|
||
async () => {
|
||
expect(await screen.findByText('世界档案')).toBeTruthy();
|
||
expect(screen.getByText('已自动保存')).toBeTruthy();
|
||
expect(screen.getByRole('button', { name: /进入世界/u })).toBeTruthy();
|
||
},
|
||
{ timeout: 2500 },
|
||
);
|
||
|
||
expect(screen.queryByText(/Agent工作区/u)).toBeNull();
|
||
expect(screen.queryByRole('button', { name: /^锚点/u })).toBeNull();
|
||
expect(screen.getByText(/基本设定/u)).toBeTruthy();
|
||
expect(screen.queryByRole('button', { name: /新增场景角色/u })).toBeNull();
|
||
|
||
await user.click(screen.getByRole('button', { name: /场景角色/u }));
|
||
expect(screen.getByRole('button', { name: /顾潮音/u })).toBeTruthy();
|
||
expect(screen.queryByText(/编辑场景角色:顾潮音/u)).toBeNull();
|
||
expect(screen.queryByRole('button', { name: /AI生成/u })).toBeNull();
|
||
expect(screen.queryByText('技能')).toBeNull();
|
||
});
|
||
|
||
test('agent draft result back button returns to workspace without redundant sync when session is already latest', async () => {
|
||
const user = userEvent.setup();
|
||
|
||
vi.mocked(executeCustomWorldAgentAction).mockResolvedValue({
|
||
operation: {
|
||
operationId: 'operation-sync-result-profile-1',
|
||
type: 'sync_result_profile',
|
||
status: 'queued',
|
||
phaseLabel: '同步结果页快照',
|
||
phaseDetail: '正在把结果页里的最新世界结构写回当前草稿。',
|
||
progress: 24,
|
||
error: null,
|
||
},
|
||
});
|
||
vi.mocked(getCustomWorldAgentOperation).mockResolvedValue({
|
||
operationId: 'operation-sync-result-profile-1',
|
||
type: 'sync_result_profile',
|
||
status: 'completed',
|
||
phaseLabel: '结果页快照已同步',
|
||
phaseDetail: '当前结果页编辑内容已经并回 Agent 草稿主链。',
|
||
progress: 100,
|
||
error: null,
|
||
});
|
||
|
||
const resultSession = {
|
||
...mockSession,
|
||
stage: 'object_refining' as const,
|
||
creatorIntent: {
|
||
sourceMode: 'card',
|
||
worldHook: '被海雾吞没的旧航路群岛',
|
||
playerPremise: '玩家回到群岛调查沉船真相。',
|
||
themeKeywords: ['海雾', '旧航路'],
|
||
toneDirectives: ['压抑', '悬疑'],
|
||
openingSituation: '首夜就有陌生船只闯入禁航区。',
|
||
coreConflicts: ['航运公会与守灯会争夺航路控制权'],
|
||
keyFactions: [],
|
||
keyCharacters: [],
|
||
keyLandmarks: [],
|
||
iconicElements: ['会移动的海雾'],
|
||
forbiddenDirectives: [],
|
||
rawSettingText: '',
|
||
},
|
||
draftProfile: {
|
||
name: '潮雾列岛',
|
||
subtitle: '旧灯塔与失控航路',
|
||
summary: '第一版世界底稿已经整理完成。',
|
||
tone: '压抑、潮湿、悬疑',
|
||
playerGoal: '查清沉船与禁航区异动的真相。',
|
||
majorFactions: ['守灯会', '航运公会'],
|
||
coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
|
||
playableNpcs: [
|
||
{
|
||
id: 'playable-1',
|
||
name: '沈砺',
|
||
title: '旧航路引路人',
|
||
role: '关键同行者',
|
||
publicIdentity: '最熟悉旧航路的人。',
|
||
publicMask: '看上去像可靠旧友。',
|
||
currentPressure: '他必须在两股势力间站队。',
|
||
hiddenHook: '暗中替沉船商盟引路。',
|
||
relationToPlayer: '旧友兼潜在背叛者',
|
||
threadIds: ['thread-1'],
|
||
summary: '他像旧友,但也像一把始终没收回鞘的刀。',
|
||
},
|
||
],
|
||
storyNpcs: [
|
||
{
|
||
id: 'story-1',
|
||
name: '顾潮音',
|
||
title: '守灯会值夜人',
|
||
role: '场景关键角色',
|
||
publicIdentity: '负责夜间巡灯与封锁。',
|
||
publicMask: '对外一直冷静克制。',
|
||
currentPressure: '她知道更多禁航区真相。',
|
||
hiddenHook: '曾亲眼见过失控海雾吞船。',
|
||
relationToPlayer: '最早愿意交换线索的人',
|
||
threadIds: ['thread-1'],
|
||
summary: '她总像比所有人更早知道海雾会往哪一侧压下来。',
|
||
},
|
||
],
|
||
landmarks: [
|
||
{
|
||
id: 'landmark-1',
|
||
name: '回潮旧灯塔',
|
||
purpose: '观察雾潮与往来船只',
|
||
mood: '潮湿、压抑、风声不止',
|
||
importance: '开局核心场景',
|
||
characterIds: ['story-1'],
|
||
threadIds: ['thread-1'],
|
||
summary: '旧灯塔是整片群岛最先看见异动的地方。',
|
||
},
|
||
],
|
||
factions: [],
|
||
threads: [],
|
||
chapters: [],
|
||
worldHook: '被海雾吞没的旧航路群岛',
|
||
playerPremise: '玩家回到群岛调查沉船真相。',
|
||
openingSituation: '首夜就有陌生船只闯入禁航区。',
|
||
iconicElements: ['会移动的海雾'],
|
||
sourceAnchorSummary: '海雾、旧灯塔、失控航路。',
|
||
legacyResultProfile: {
|
||
id: 'agent-draft-custom-world-agent-session-1',
|
||
settingText: '被海雾吞没的旧航路群岛',
|
||
name: '潮雾列岛·同步后',
|
||
subtitle: '旧灯塔与失控航路',
|
||
summary: '同步后的结果页快照已经回写到 session。',
|
||
tone: '压抑、潮湿、悬疑',
|
||
playerGoal: '查清沉船与禁航区异动的真相。',
|
||
templateWorldType: 'WUXIA',
|
||
majorFactions: ['守灯会', '航运公会'],
|
||
coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
|
||
playableNpcs: [],
|
||
storyNpcs: [],
|
||
items: [],
|
||
landmarks: [],
|
||
generationMode: 'full',
|
||
generationStatus: 'complete',
|
||
},
|
||
},
|
||
draftCards: [
|
||
{
|
||
id: 'world-foundation',
|
||
kind: 'world',
|
||
title: '潮雾列岛',
|
||
subtitle: '旧灯塔与失控航路',
|
||
summary: '第一版世界底稿已经整理完成。',
|
||
status: 'warning',
|
||
linkedIds: ['playable-1', 'story-1', 'landmark-1'],
|
||
warningCount: 0,
|
||
},
|
||
],
|
||
} satisfies CustomWorldAgentSessionSnapshot;
|
||
vi.mocked(getCustomWorldAgentSession).mockResolvedValue(resultSession);
|
||
|
||
render(<TestWrapper />);
|
||
|
||
await openNewRpgCreation(user);
|
||
|
||
await waitFor(
|
||
async () => {
|
||
expect(await screen.findByText('世界档案')).toBeTruthy();
|
||
expect(screen.getByRole('button', { name: /返回创作/u })).toBeTruthy();
|
||
},
|
||
{ timeout: 2500 },
|
||
);
|
||
|
||
await user.click(screen.getByRole('button', { name: /返回创作/u }));
|
||
|
||
await waitFor(() => {
|
||
expect(
|
||
screen.getByText('Agent工作区:custom-world-agent-session-1'),
|
||
).toBeTruthy();
|
||
});
|
||
|
||
expect(
|
||
vi.mocked(executeCustomWorldAgentAction).mock.calls.some(
|
||
([sessionId, payload]) =>
|
||
sessionId === 'custom-world-agent-session-1' &&
|
||
payload?.action === 'sync_result_profile',
|
||
),
|
||
).toBe(false);
|
||
expect(screen.queryByText('世界档案')).toBeNull();
|
||
});
|
||
|
||
test('agent draft result auto-save persists the latest profile rebuilt from synced session', async () => {
|
||
const user = userEvent.setup();
|
||
|
||
vi.mocked(executeCustomWorldAgentAction).mockResolvedValue({
|
||
operation: {
|
||
operationId: 'operation-sync-result-profile-2',
|
||
type: 'sync_result_profile',
|
||
status: 'queued',
|
||
phaseLabel: '同步结果页快照',
|
||
phaseDetail: '正在把结果页里的最新世界结构写回当前草稿。',
|
||
progress: 24,
|
||
error: null,
|
||
},
|
||
});
|
||
vi.mocked(getCustomWorldAgentOperation).mockResolvedValue({
|
||
operationId: 'operation-sync-result-profile-2',
|
||
type: 'sync_result_profile',
|
||
status: 'completed',
|
||
phaseLabel: '结果页快照已同步',
|
||
phaseDetail: '当前结果页编辑内容已经并回 Agent 草稿主链。',
|
||
progress: 100,
|
||
error: null,
|
||
});
|
||
|
||
const syncedSession = {
|
||
...mockSession,
|
||
stage: 'object_refining' as const,
|
||
creatorIntent: {
|
||
sourceMode: 'card',
|
||
worldHook: '被海雾吞没的旧航路群岛',
|
||
playerPremise: '玩家回到群岛调查沉船真相。',
|
||
themeKeywords: ['海雾', '旧航路'],
|
||
toneDirectives: ['压抑', '悬疑'],
|
||
openingSituation: '首夜就有陌生船只闯入禁航区。',
|
||
coreConflicts: ['航运公会与守灯会争夺航路控制权'],
|
||
keyFactions: [],
|
||
keyCharacters: [],
|
||
keyLandmarks: [],
|
||
iconicElements: ['会移动的海雾'],
|
||
forbiddenDirectives: [],
|
||
rawSettingText: '',
|
||
},
|
||
draftProfile: {
|
||
name: '潮雾列岛',
|
||
subtitle: '旧灯塔与失控航路',
|
||
summary: '第一版世界底稿已经整理完成。',
|
||
tone: '压抑、潮湿、悬疑',
|
||
playerGoal: '查清沉船与禁航区异动的真相。',
|
||
majorFactions: ['守灯会', '航运公会'],
|
||
coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
|
||
playableNpcs: [],
|
||
storyNpcs: [],
|
||
landmarks: [],
|
||
factions: [],
|
||
threads: [],
|
||
chapters: [],
|
||
worldHook: '被海雾吞没的旧航路群岛',
|
||
playerPremise: '玩家回到群岛调查沉船真相。',
|
||
openingSituation: '首夜就有陌生船只闯入禁航区。',
|
||
iconicElements: ['会移动的海雾'],
|
||
sourceAnchorSummary: '海雾、旧灯塔、失控航路。',
|
||
legacyResultProfile: {
|
||
id: 'agent-draft-custom-world-agent-session-1',
|
||
settingText: '被海雾吞没的旧航路群岛',
|
||
name: '潮雾列岛·session最新版',
|
||
subtitle: '旧灯塔与失控航路',
|
||
summary: '作品库应该保存这份同步后的最新快照。',
|
||
tone: '压抑、潮湿、悬疑',
|
||
playerGoal: '查清沉船与禁航区异动的真相。',
|
||
templateWorldType: 'WUXIA',
|
||
majorFactions: ['守灯会', '航运公会'],
|
||
coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
|
||
playableNpcs: [],
|
||
storyNpcs: [],
|
||
items: [],
|
||
landmarks: [],
|
||
generationMode: 'full',
|
||
generationStatus: 'complete',
|
||
},
|
||
},
|
||
draftCards: [
|
||
{
|
||
id: 'world-foundation',
|
||
kind: 'world',
|
||
title: '潮雾列岛',
|
||
subtitle: '旧灯塔与失控航路',
|
||
summary: '第一版世界底稿已经整理完成。',
|
||
status: 'warning',
|
||
linkedIds: [],
|
||
warningCount: 0,
|
||
},
|
||
],
|
||
} satisfies CustomWorldAgentSessionSnapshot;
|
||
vi.mocked(getCustomWorldAgentSession).mockResolvedValue(syncedSession);
|
||
|
||
render(<TestWrapper />);
|
||
|
||
await openNewRpgCreation(user);
|
||
|
||
await waitFor(
|
||
async () => {
|
||
expect(await screen.findByText('世界档案')).toBeTruthy();
|
||
expect(screen.getByText('已自动保存')).toBeTruthy();
|
||
},
|
||
{ timeout: 2500 },
|
||
);
|
||
|
||
await waitFor(() => {
|
||
expect(upsertCustomWorldProfile).toHaveBeenCalled();
|
||
});
|
||
|
||
const latestSavedProfile = vi.mocked(upsertCustomWorldProfile).mock.calls.at(-1)?.[0];
|
||
expect(latestSavedProfile?.name).toBe('潮雾列岛·session最新版');
|
||
expect(latestSavedProfile?.summary).toBe(
|
||
'作品库应该保存这份同步后的最新快照。',
|
||
);
|
||
});
|
||
|
||
test('authenticated users with save archives default into the saves tab', async () => {
|
||
vi.mocked(listProfileSaveArchives).mockResolvedValue([
|
||
{
|
||
worldKey: 'custom:world-1',
|
||
ownerUserId: null,
|
||
profileId: 'world-1',
|
||
worldType: 'CUSTOM',
|
||
worldName: '潮雾列岛',
|
||
subtitle: '旧灯塔与失控航路',
|
||
summaryText: '回到旧灯塔继续推进调查。',
|
||
coverImageSrc: null,
|
||
lastPlayedAt: '2026-04-19T12:00:00.000Z',
|
||
},
|
||
]);
|
||
|
||
render(<TestWrapper withAuth />);
|
||
|
||
expect((await screen.findAllByText('全部存档')).length).toBeGreaterThan(0);
|
||
expect((await screen.findAllByText('潮雾列岛')).length).toBeGreaterThan(0);
|
||
expect(screen.getAllByText('最近更新时间排序').length).toBeGreaterThan(0);
|
||
expect(screen.queryByText('SAVE ARCHIVE')).toBeNull();
|
||
});
|
||
|
||
test('save tab can resume a selected archive directly into the game', async () => {
|
||
const user = userEvent.setup();
|
||
const handleContinueGame = vi.fn();
|
||
|
||
vi.mocked(listProfileSaveArchives).mockResolvedValue([
|
||
{
|
||
worldKey: 'custom:world-1',
|
||
ownerUserId: null,
|
||
profileId: 'world-1',
|
||
worldType: 'CUSTOM',
|
||
worldName: '潮雾列岛',
|
||
subtitle: '旧灯塔与失控航路',
|
||
summaryText: '回到旧灯塔继续推进调查。',
|
||
coverImageSrc: null,
|
||
lastPlayedAt: '2026-04-19T12:00:00.000Z',
|
||
},
|
||
]);
|
||
vi.mocked(resumeProfileSaveArchive).mockResolvedValue({
|
||
entry: {
|
||
worldKey: 'custom:world-1',
|
||
ownerUserId: null,
|
||
profileId: 'world-1',
|
||
worldType: 'CUSTOM',
|
||
worldName: '潮雾列岛',
|
||
subtitle: '旧灯塔与失控航路',
|
||
summaryText: '回到旧灯塔继续推进调查。',
|
||
coverImageSrc: null,
|
||
lastPlayedAt: '2026-04-19T12:00:00.000Z',
|
||
},
|
||
snapshot: {
|
||
version: 2,
|
||
savedAt: '2026-04-19T12:00:00.000Z',
|
||
bottomTab: 'adventure',
|
||
currentStory: null,
|
||
gameState: {
|
||
worldType: 'CUSTOM',
|
||
} as GameState,
|
||
} as HydratedSavedGameSnapshot,
|
||
});
|
||
|
||
render(<TestWrapper withAuth onContinueGame={handleContinueGame} />);
|
||
|
||
await clickFirstAsyncButtonByName(user, /潮雾列岛/u);
|
||
|
||
await waitFor(() => {
|
||
expect(resumeProfileSaveArchive).toHaveBeenCalledWith('custom:world-1');
|
||
expect(handleContinueGame).toHaveBeenCalledTimes(1);
|
||
});
|
||
});
|
||
|
||
test('owned world detail can delete a work and return to the create tab list', async () => {
|
||
const user = userEvent.setup();
|
||
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
||
|
||
const publishedWork = {
|
||
workId: 'published:world-delete-1',
|
||
sourceType: 'published_profile' as const,
|
||
status: 'published' as const,
|
||
title: '潮雾列岛',
|
||
subtitle: '旧灯塔与失控航路',
|
||
summary: '用于测试删除流程的作品。',
|
||
coverImageSrc: null,
|
||
coverRenderMode: 'image' as const,
|
||
coverCharacterImageSrcs: [],
|
||
updatedAt: '2026-04-16T12:00:00.000Z',
|
||
publishedAt: '2026-04-16T12:00:00.000Z',
|
||
stage: null,
|
||
stageLabel: '已发布',
|
||
playableNpcCount: 0,
|
||
landmarkCount: 0,
|
||
roleVisualReadyCount: 0,
|
||
roleAnimationReadyCount: 0,
|
||
roleAssetSummaryLabel: null,
|
||
sessionId: null,
|
||
profileId: 'world-delete-1',
|
||
canResume: false,
|
||
canEnterWorld: true,
|
||
};
|
||
const publishedLibraryEntry = {
|
||
ownerUserId: 'user-1',
|
||
profileId: 'world-delete-1',
|
||
profile: {
|
||
id: 'world-delete-1',
|
||
name: '潮雾列岛',
|
||
subtitle: '旧灯塔与失控航路',
|
||
summary: '用于测试删除流程的作品。',
|
||
tone: '压抑、潮湿、悬疑',
|
||
playerGoal: '查清旧案。',
|
||
majorFactions: ['守灯会'],
|
||
coreConflicts: ['雾潮正在逼近港口'],
|
||
playableNpcs: [],
|
||
storyNpcs: [],
|
||
landmarks: [],
|
||
} as never,
|
||
visibility: 'published' as const,
|
||
publishedAt: '2026-04-16T12:00:00.000Z',
|
||
updatedAt: '2026-04-16T12:00:00.000Z',
|
||
authorDisplayName: '测试玩家',
|
||
worldName: '潮雾列岛',
|
||
subtitle: '旧灯塔与失控航路',
|
||
summaryText: '用于测试删除流程的作品。',
|
||
coverImageSrc: null,
|
||
themeMode: 'tide' as const,
|
||
playableNpcCount: 0,
|
||
landmarkCount: 0,
|
||
};
|
||
|
||
vi.mocked(listCustomWorldWorks)
|
||
.mockResolvedValueOnce([publishedWork])
|
||
.mockResolvedValue([]);
|
||
vi.mocked(listCustomWorldLibrary)
|
||
.mockResolvedValueOnce([publishedLibraryEntry])
|
||
.mockResolvedValue([]);
|
||
vi.mocked(deleteCustomWorldProfile).mockResolvedValue([]);
|
||
|
||
render(<TestWrapper withAuth />);
|
||
|
||
await openCreationHub(user);
|
||
await user.click(screen.getByRole('button', { name: /进入世界/u }));
|
||
await user.click(await screen.findByRole('button', { name: '删除作品' }));
|
||
|
||
await waitFor(() => {
|
||
expect(deleteCustomWorldProfile).toHaveBeenCalledWith('world-delete-1');
|
||
});
|
||
|
||
await waitFor(() => {
|
||
expect(screen.queryByRole('button', { name: '删除作品' })).toBeNull();
|
||
});
|
||
await waitFor(() => {
|
||
expect(screen.getByText('还没有作品')).toBeTruthy();
|
||
});
|
||
});
|
||
|
||
test('creation hub published work enters existing detail view', async () => {
|
||
const user = userEvent.setup();
|
||
|
||
vi.mocked(listCustomWorldWorks).mockResolvedValue([
|
||
{
|
||
workId: 'published:world-public-1',
|
||
sourceType: 'published_profile',
|
||
status: 'published',
|
||
title: '潮雾列岛',
|
||
subtitle: '旧灯塔与失控航路',
|
||
summary: '已经发布的群岛世界作品。',
|
||
coverImageSrc: null,
|
||
coverRenderMode: 'image',
|
||
coverCharacterImageSrcs: [],
|
||
updatedAt: '2026-04-20T10:00:00.000Z',
|
||
publishedAt: '2026-04-20T10:00:00.000Z',
|
||
stage: null,
|
||
stageLabel: '已发布',
|
||
playableNpcCount: 3,
|
||
landmarkCount: 4,
|
||
roleVisualReadyCount: 1,
|
||
roleAnimationReadyCount: 0,
|
||
roleAssetSummaryLabel: null,
|
||
sessionId: null,
|
||
profileId: 'world-public-1',
|
||
canResume: false,
|
||
canEnterWorld: true,
|
||
},
|
||
]);
|
||
vi.mocked(listCustomWorldLibrary).mockResolvedValue([
|
||
{
|
||
ownerUserId: 'user-1',
|
||
profileId: 'world-public-1',
|
||
profile: {
|
||
id: 'world-public-1',
|
||
name: '潮雾列岛',
|
||
subtitle: '旧灯塔与失控航路',
|
||
summary: '已经发布的群岛世界作品。',
|
||
tone: '压抑、潮湿、悬疑',
|
||
playerGoal: '查清群岛旧案。',
|
||
majorFactions: ['守灯会'],
|
||
coreConflicts: ['假航灯正在扰乱航线'],
|
||
playableNpcs: [],
|
||
storyNpcs: [],
|
||
landmarks: [],
|
||
} as never,
|
||
visibility: 'published',
|
||
publishedAt: '2026-04-20T10:00:00.000Z',
|
||
updatedAt: '2026-04-20T10:00:00.000Z',
|
||
authorDisplayName: '测试玩家',
|
||
worldName: '潮雾列岛',
|
||
subtitle: '旧灯塔与失控航路',
|
||
summaryText: '已经发布的群岛世界作品。',
|
||
coverImageSrc: null,
|
||
themeMode: 'tide',
|
||
playableNpcCount: 3,
|
||
landmarkCount: 4,
|
||
},
|
||
]);
|
||
|
||
render(<TestWrapper withAuth />);
|
||
|
||
await openCreationHub(user);
|
||
await user.click(screen.getByRole('button', { name: /进入世界/u }));
|
||
|
||
expect(await screen.findByText('世界信息')).toBeTruthy();
|
||
expect(screen.getByRole('button', { name: '开始游戏' })).toBeTruthy();
|
||
expect(screen.getByText('已发布')).toBeTruthy();
|
||
});
|