@@ -1,6 +1,6 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useState } from 'react';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
@@ -12,8 +12,8 @@ import type {
|
||||
} from '../types';
|
||||
import { CustomWorldEntityCatalog } from './CustomWorldEntityCatalog';
|
||||
import {
|
||||
CustomWorldEntityEditorModal,
|
||||
type CustomWorldEditorTarget,
|
||||
CustomWorldEntityEditorModal,
|
||||
} from './CustomWorldEntityEditorModal';
|
||||
|
||||
vi.mock('../data/characterPresets', async () => {
|
||||
@@ -24,6 +24,11 @@ vi.mock('../data/characterPresets', async () => {
|
||||
return {
|
||||
...actual,
|
||||
buildCustomWorldPlayableCharacters: vi.fn(() => []),
|
||||
buildCustomWorldRuntimeCharacters: vi.fn(() => []),
|
||||
createCharacterSkillCooldowns: vi.fn(() => ({})),
|
||||
getCharacterMaxHp: vi.fn(() => 180),
|
||||
getCharacterMaxMana: vi.fn(() => 60),
|
||||
setRuntimeCharacterOverrides: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -34,6 +39,8 @@ vi.mock('./CharacterAnimator', () => ({
|
||||
vi.mock('../services/aiService', () => ({
|
||||
generateCustomWorldSceneImage: vi.fn(),
|
||||
generateCustomWorldSceneNpc: vi.fn(),
|
||||
generateInitialStory: vi.fn(),
|
||||
generateNextStep: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./CustomWorldNpcVisualEditor', () => ({
|
||||
@@ -43,6 +50,19 @@ vi.mock('./CustomWorldNpcVisualEditor', () => ({
|
||||
CustomWorldNpcVisualEditor: () => <div>预设形象编辑器</div>,
|
||||
}));
|
||||
|
||||
vi.mock('./game-shell/GameShellRuntime', () => ({
|
||||
GameShellRuntime: ({
|
||||
session,
|
||||
}: {
|
||||
session: { gameState: { currentScenePreset?: { name?: string } | null } };
|
||||
}) => (
|
||||
<div>
|
||||
<div>幕预览运行时</div>
|
||||
<div>{session.gameState.currentScenePreset?.name ?? '未进入场景'}</div>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('./asset-studio/characterAssetWorkflowPersistence', () => ({
|
||||
fetchCharacterWorkflowCache: vi.fn().mockResolvedValue({ cache: null }),
|
||||
generateCharacterPromptBundle: vi.fn().mockResolvedValue({
|
||||
@@ -138,7 +158,19 @@ function createProfile(): CustomWorldProfile {
|
||||
templateWorldType: 'WUXIA',
|
||||
majorFactions: ['守潮盟', '沉钟会'],
|
||||
coreConflicts: ['旧航道归属', '沉钟遗产争夺'],
|
||||
attributeSchema: {},
|
||||
attributeSchema: {
|
||||
id: 'schema-1',
|
||||
worldId: 'world-1',
|
||||
schemaVersion: 1,
|
||||
generatedFrom: {
|
||||
worldType: 'WUXIA',
|
||||
worldName: '潮雾群岛',
|
||||
settingSummary: '潮雾群岛上的禁制与旧航道正在一起失衡。',
|
||||
tone: '压抑、潮湿、带着未解旧伤。',
|
||||
conflictCore: '旧航道归属',
|
||||
},
|
||||
slots: [],
|
||||
},
|
||||
playableNpcs: [createPlayableRole('playable-1', '沈砺')],
|
||||
storyNpcs: [createStoryRole('story-1', '顾潮音')],
|
||||
items: [],
|
||||
@@ -189,6 +221,9 @@ function LandmarkEditorFlowHarness() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<pre data-testid="landmark-profile-json" className="hidden">
|
||||
{JSON.stringify(profile)}
|
||||
</pre>
|
||||
<CustomWorldEntityCatalog
|
||||
profile={profile}
|
||||
previewCharacters={[]}
|
||||
@@ -209,6 +244,19 @@ function LandmarkEditorFlowHarness() {
|
||||
);
|
||||
}
|
||||
|
||||
function readLandmarkHarnessProfile() {
|
||||
const content = screen.getByTestId('landmark-profile-json').textContent;
|
||||
return JSON.parse(content || '{}') as CustomWorldProfile;
|
||||
}
|
||||
|
||||
function getSceneActCard(index: number) {
|
||||
const card = screen.getAllByTestId('scene-act-card')[index];
|
||||
if (!card) {
|
||||
throw new Error(`未找到第 ${index + 1} 个幕卡片`);
|
||||
}
|
||||
return card;
|
||||
}
|
||||
|
||||
function CampEditorFlowHarness() {
|
||||
const [profile, setProfile] = useState<CustomWorldProfile>({
|
||||
...createProfileWithLandmark(),
|
||||
@@ -548,3 +596,128 @@ test('开局场景图片保存后会同步更新编辑页和场景列表', async
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('场景编辑器会在场景内展示槽位化多幕配置并保存', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<LandmarkEditorFlowHarness />);
|
||||
|
||||
expect(screen.getByText('多幕配置')).toBeTruthy();
|
||||
expect(screen.getAllByTestId('scene-act-card')).toHaveLength(3);
|
||||
expect(screen.queryByText('幕标题')).toBeNull();
|
||||
expect(screen.queryByText('幕摘要')).toBeNull();
|
||||
expect(screen.queryByText('幕目标')).toBeNull();
|
||||
expect(screen.queryByText('过渡铺垫')).toBeNull();
|
||||
|
||||
const firstActCard = getSceneActCard(0);
|
||||
expect(within(firstActCard).getAllByTestId('scene-act-slot-button')).toHaveLength(3);
|
||||
|
||||
await user.click(within(firstActCard).getByRole('button', { name: '配置背景' }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('配置幕背景:第1幕')).toBeTruthy();
|
||||
});
|
||||
|
||||
const presetImage = screen.getByRole('img', { name: '幕背景预设 1' });
|
||||
const presetSrc = presetImage.getAttribute('src');
|
||||
const presetButton = presetImage.closest('button');
|
||||
expect(presetButton).toBeTruthy();
|
||||
if (!presetButton) {
|
||||
throw new Error('未找到幕背景预设按钮');
|
||||
}
|
||||
await user.click(presetButton);
|
||||
await user.click(screen.getByRole('button', { name: '保存背景' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('配置幕背景:第1幕')).toBeNull();
|
||||
});
|
||||
|
||||
await user.click(within(getSceneActCard(0)).getAllByTestId('scene-act-slot-button')[0]!);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('配置角色:第1幕 · 主角色槽位')).toBeTruthy();
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /闻雪汀[\s\S]*选择/u }));
|
||||
await user.click(screen.getByRole('button', { name: '保存角色' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('配置角色:第1幕 · 主角色槽位')).toBeNull();
|
||||
});
|
||||
|
||||
expect(
|
||||
within(getSceneActCard(0)).getByRole('button', {
|
||||
name: '配置第1个角色:闻雪汀',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /保存修改/u }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('编辑场景:沉钟栈桥')).toBeNull();
|
||||
});
|
||||
|
||||
const savedProfile = readLandmarkHarnessProfile();
|
||||
const savedSceneChapter = savedProfile.sceneChapterBlueprints?.find(
|
||||
(entry) => entry.sceneId === 'landmark-1',
|
||||
);
|
||||
|
||||
expect(savedSceneChapter).toBeTruthy();
|
||||
expect(savedSceneChapter?.acts[0]?.backgroundImageSrc).toBe(presetSrc);
|
||||
expect(savedSceneChapter?.acts[0]?.encounterNpcIds[0]).toBe('story-2');
|
||||
expect(savedSceneChapter?.acts[0]?.primaryNpcId).toBe('story-2');
|
||||
});
|
||||
|
||||
test('场景多幕支持新增删除和调序', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<LandmarkEditorFlowHarness />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '新增一幕' }));
|
||||
expect(screen.getAllByTestId('scene-act-card')).toHaveLength(4);
|
||||
|
||||
const secondActCard = getSceneActCard(1);
|
||||
await user.click(within(secondActCard).getAllByTestId('scene-act-slot-button')[0]!);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('配置角色:第2幕 · 主角色槽位')).toBeTruthy();
|
||||
});
|
||||
await user.click(screen.getByRole('button', { name: /谢孤灯[\s\S]*选择/u }));
|
||||
await user.click(screen.getByRole('button', { name: '保存角色' }));
|
||||
await user.click(within(secondActCard).getByRole('button', { name: '下移' }));
|
||||
|
||||
const fourthActCard = getSceneActCard(3);
|
||||
await user.click(within(fourthActCard).getByRole('button', { name: '删除' }));
|
||||
expect(screen.getAllByTestId('scene-act-card')).toHaveLength(3);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /保存修改/u }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('编辑场景:沉钟栈桥')).toBeNull();
|
||||
});
|
||||
|
||||
const savedProfile = readLandmarkHarnessProfile();
|
||||
const savedSceneChapter = savedProfile.sceneChapterBlueprints?.find(
|
||||
(entry) => entry.sceneId === 'landmark-1',
|
||||
);
|
||||
|
||||
expect(savedSceneChapter?.acts).toHaveLength(3);
|
||||
expect(savedSceneChapter?.acts[2]?.primaryNpcId).toBe('story-3');
|
||||
});
|
||||
|
||||
test('场景幕预览会打开当前幕运行时面板', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<LandmarkEditorFlowHarness />);
|
||||
|
||||
await user.click(within(getSceneActCard(0)).getByRole('button', { name: '幕预览' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('幕预览运行时')).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(screen.getAllByText('沉钟栈桥').length).toBeGreaterThan(0);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '关闭预览' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('幕预览运行时')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user