This commit is contained in:
2026-04-26 20:50:58 +08:00
parent a3a9bfa194
commit 67161bd6d1
142 changed files with 3349 additions and 10674 deletions

View File

@@ -1,6 +1,13 @@
/* @vitest-environment jsdom */
import { cleanup, render, screen, waitFor, within } from '@testing-library/react';
import {
cleanup,
fireEvent,
render,
screen,
waitFor,
within,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useState } from 'react';
import { afterEach, expect, test, vi } from 'vitest';
@@ -191,6 +198,7 @@ function createProfile(): CustomWorldProfile {
attributeSchema: {
id: 'schema-1',
worldId: 'world-1',
schemaName: '潮雾六维',
schemaVersion: 1,
generatedFrom: {
worldType: 'WUXIA',
@@ -199,7 +207,68 @@ function createProfile(): CustomWorldProfile {
tone: '压抑、潮湿、带着未解旧伤。',
conflictCore: '旧航道归属',
},
slots: [],
slots: [
{
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: '长线跋涉。',
},
],
},
playableNpcs: [createPlayableRole('playable-1', '沈砺')],
storyNpcs: [createStoryRole('story-1', '顾潮音')],
@@ -684,6 +753,57 @@ test('基本设定目标打开独立编辑面板', () => {
expect(screen.queryByText('编辑世界信息')).toBeNull();
});
test('世界信息面板可以编辑六个角色维度信息', async () => {
const user = userEvent.setup();
const savedProfileRef: { current: CustomWorldProfile | null } = {
current: null,
};
render(
<RpgCreationEntityEditorModal
profile={createProfile()}
target={{ kind: 'world' }}
onClose={() => {}}
onProfileChange={(profile) => {
savedProfileRef.current = profile;
}}
/>,
);
expect(screen.getByText('角色维度')).toBeTruthy();
const nameInputs = screen.getAllByLabelText('维度名称');
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]!, '正面压线与护住阵脚。');
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 () => {
const user = userEvent.setup();
const handleEditTarget = vi.fn();
@@ -821,11 +941,15 @@ test('场景图片保存后会同步更新编辑页和场景列表', async () =>
const savedSceneChapter = savedProfile.sceneChapterBlueprints?.find(
(entry) => entry.sceneId === 'landmark-1',
);
expect(
savedSceneChapter?.acts.every(
(act) => act.backgroundImageSrc === '/generated-custom-world-scenes/updated-scene.png',
),
).toBe(true);
expect(savedSceneChapter?.acts[0]?.backgroundImageSrc).toBe(
'/generated-custom-world-scenes/updated-scene.png',
);
expect(savedSceneChapter?.acts[1]?.backgroundImageSrc).not.toBe(
'/generated-custom-world-scenes/updated-scene.png',
);
expect(savedSceneChapter?.acts[2]?.backgroundImageSrc).not.toBe(
'/generated-custom-world-scenes/updated-scene.png',
);
});
test('开局场景图片保存后会同步更新编辑页和场景列表', async () => {
@@ -899,11 +1023,15 @@ test('开局场景图片保存后会同步更新编辑页和场景列表', async
const savedSceneChapter = savedProfile.sceneChapterBlueprints?.find(
(entry) => entry.sceneId === 'custom-scene-camp',
);
expect(
savedSceneChapter?.acts.every(
(act) => act.backgroundImageSrc === '/generated-custom-world-scenes/updated-camp.png',
),
).toBe(true);
expect(savedSceneChapter?.acts[0]?.backgroundImageSrc).toBe(
'/generated-custom-world-scenes/updated-camp.png',
);
expect(savedSceneChapter?.acts[1]?.backgroundImageSrc).not.toBe(
'/generated-custom-world-scenes/updated-camp.png',
);
expect(savedSceneChapter?.acts[2]?.backgroundImageSrc).not.toBe(
'/generated-custom-world-scenes/updated-camp.png',
);
});
test('开局场景在场景配置面板中与普通场景使用同级参数并可保存', async () => {
@@ -960,6 +1088,8 @@ test('开局场景在场景配置面板中与普通场景使用同级参数并
test('开局场景列表与详情幕预览复用同一套幕级图片', async () => {
const profile = createProfileWithSceneChapters();
profile.sceneChapterBlueprints![0]!.acts[1]!.backgroundPromptText =
'第二幕专属背景提示';
const user = userEvent.setup();
render(
@@ -1003,6 +1133,53 @@ test('开局场景列表与详情幕预览复用同一套幕级图片', async ()
);
});
test('开局场景幕背景智能生成复用当前幕图片和幕级提示词', async () => {
mockedRpgCreationAssetClient.generateSceneImage.mockClear();
mockedRpgCreationAssetClient.generateSceneImage.mockResolvedValue({
imageSrc: '/generated-custom-world-scenes/camp-act-2-ai.png',
assetId: 'asset-camp-act-2',
model: 'wan2.2-t2i-flash',
size: '1280*720',
taskId: 'task-camp-act-2',
prompt: '第二幕专属背景提示',
});
const profile = createProfileWithSceneChapters();
profile.sceneChapterBlueprints![0]!.acts[1]!.backgroundPromptText =
'第二幕专属背景提示';
const user = userEvent.setup();
render(
<RpgCreationEntityEditorModal
profile={profile}
target={{ kind: 'camp' }}
onClose={() => {}}
onProfileChange={() => {}}
/>,
);
await user.click(within(getSceneActCard(1)).getByRole('button', { name: '配置背景' }));
await waitFor(() => {
expect(screen.getByText('配置幕背景第2幕')).toBeTruthy();
});
await user.click(screen.getByRole('button', { name: 'AI生成' }));
await waitFor(() => {
expect(screen.getByText('智能生成:潮灯居')).toBeTruthy();
});
expect(screen.getByRole('img', { name: '潮灯居' }).getAttribute('src')).toBe(
'/generated-custom-world-scenes/camp-act-2.png',
);
await user.click(screen.getByRole('button', { name: '开始生成' }));
await waitFor(() => {
expect(mockedRpgCreationAssetClient.generateSceneImage).toHaveBeenCalledTimes(1);
});
const payload = mockedRpgCreationAssetClient.generateSceneImage.mock.calls[0]?.[0];
expect(payload?.userPrompt).toBe('第二幕专属背景提示');
});
test('普通场景世界地图会包含开局场景并高亮当前场景', async () => {
const user = userEvent.setup();