This commit is contained in:
228
src/components/rpg-entry/RpgEntryCharacterSelectView.test.tsx
Normal file
228
src/components/rpg-entry/RpgEntryCharacterSelectView.test.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { afterEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
buildCustomWorldPlayableCharacters,
|
||||
} from '../../data/characterPresets';
|
||||
import {
|
||||
type Character,
|
||||
type CustomWorldProfile,
|
||||
WorldType,
|
||||
} from '../../types';
|
||||
import { RpgEntryCharacterSelectView } from './RpgEntryCharacterSelectView';
|
||||
|
||||
vi.mock('../../data/characterPresets', () => ({
|
||||
ROLE_TEMPLATE_CHARACTERS: [],
|
||||
buildCustomWorldPlayableCharacters: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../CharacterAnimator', () => ({
|
||||
CharacterAnimator: ({ character }: { character: Character }) => (
|
||||
<div>{character.name}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../CharacterDetailModal', () => ({
|
||||
CharacterDetailModal: () => null,
|
||||
}));
|
||||
|
||||
vi.mock('../SelectionCustomizationModals', () => ({
|
||||
CharacterDraftModal: () => null,
|
||||
}));
|
||||
|
||||
function createCharacter(name: string, title: string): Character {
|
||||
return {
|
||||
id: '',
|
||||
name,
|
||||
title,
|
||||
description: `${name}的定位描述`,
|
||||
backstory: `${name}的背景故事`,
|
||||
personality: `${name} 冷静 果断`,
|
||||
gender: 'female',
|
||||
portrait: `/portraits/${name}.png`,
|
||||
attributes: {
|
||||
strength: 10,
|
||||
agility: 11,
|
||||
intelligence: 12,
|
||||
spirit: 13,
|
||||
},
|
||||
skills: [],
|
||||
} as unknown as Character;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('custom world character selection stays stable when character ids are empty', async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleConfirm = vi.fn();
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined);
|
||||
|
||||
vi.mocked(buildCustomWorldPlayableCharacters).mockReturnValue([
|
||||
createCharacter('沈砺', '潮锋斥候'),
|
||||
createCharacter('闻潮', '雾海哨兵'),
|
||||
]);
|
||||
|
||||
HTMLElement.prototype.scrollTo = function scrollTo(
|
||||
this: HTMLElement,
|
||||
options?: ScrollToOptions | number,
|
||||
) {
|
||||
if (typeof options === 'object' && options) {
|
||||
if (typeof options.left === 'number') {
|
||||
this.scrollLeft = options.left;
|
||||
}
|
||||
if (typeof options.top === 'number') {
|
||||
this.scrollTop = options.top;
|
||||
}
|
||||
}
|
||||
this.dispatchEvent(new Event('scroll'));
|
||||
};
|
||||
|
||||
vi
|
||||
.spyOn(HTMLElement.prototype, 'getBoundingClientRect')
|
||||
.mockImplementation(function mockGetBoundingClientRect(this: HTMLElement) {
|
||||
if ((this as HTMLElement).dataset.carouselCard === 'true') {
|
||||
return {
|
||||
width: 240,
|
||||
height: 360,
|
||||
top: 0,
|
||||
right: 240,
|
||||
bottom: 360,
|
||||
left: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: () => ({}),
|
||||
} as DOMRect;
|
||||
}
|
||||
|
||||
return {
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: () => ({}),
|
||||
} as DOMRect;
|
||||
});
|
||||
|
||||
render(
|
||||
<RpgEntryCharacterSelectView
|
||||
worldType={WorldType.CUSTOM}
|
||||
customWorldProfile={{
|
||||
attributeSchema: {
|
||||
id: 'schema:custom:test',
|
||||
worldId: 'custom:test',
|
||||
schemaVersion: 1,
|
||||
generatedFrom: {
|
||||
worldType: WorldType.CUSTOM,
|
||||
worldName: '潮城',
|
||||
settingSummary: '潮水与迷雾交织的港城。',
|
||||
tone: '潮湿、危险、带着试探。',
|
||||
conflictCore: '在涨落之间抢先一步。',
|
||||
},
|
||||
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: '在恶劣天气里保有余力。',
|
||||
},
|
||||
],
|
||||
},
|
||||
} as unknown as CustomWorldProfile}
|
||||
onBack={() => {}}
|
||||
onConfirm={handleConfirm}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(/潮骨:/u)).toBeTruthy();
|
||||
expect(screen.queryByText(/力量:/u)).toBeNull();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /闻潮/u }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /查看闻潮的详情/u })).toBeTruthy();
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /进入营地/u }));
|
||||
|
||||
expect(handleConfirm).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: '闻潮',
|
||||
title: '雾海哨兵',
|
||||
}),
|
||||
);
|
||||
|
||||
const duplicateKeyCalls = consoleErrorSpy.mock.calls.filter((call) =>
|
||||
call.some(
|
||||
(arg) =>
|
||||
typeof arg === 'string'
|
||||
&& arg.includes('Encountered two children with the same key'),
|
||||
),
|
||||
);
|
||||
|
||||
expect(duplicateKeyCalls).toHaveLength(0);
|
||||
});
|
||||
Reference in New Issue
Block a user