Files
Genarrative/src/components/AdventureEntityModal.test.tsx
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

172 lines
4.4 KiB
TypeScript

/* @vitest-environment jsdom */
import { render, screen } from '@testing-library/react';
import { afterEach, expect, test, vi } from 'vitest';
import {
AnimationState,
type Encounter,
type GameState,
type EquipmentLoadout,
WorldType,
} from '../types';
import { AdventureEntityModal } from './AdventureEntityModal';
vi.mock('./CharacterAnimator', () => ({
CharacterAnimator: () => <div data-testid="character-portrait" />,
}));
vi.mock('./MedievalNpcAnimator', () => ({
MedievalNpcAnimator: () => <div data-testid="medieval-npc-portrait" />,
}));
vi.mock('./HostileNpcAnimator', () => ({
HostileNpcAnimator: () => <div data-testid="hostile-npc-portrait" />,
}));
function createGameState(overrides: Partial<GameState> = {}): GameState {
return {
worldType: WorldType.WUXIA,
customWorldProfile: null,
playerCharacter: null,
runtimeStats: {
playTimeMs: 0,
lastPlayTickAt: null,
hostileNpcsDefeated: 0,
questsAccepted: 0,
itemsUsed: 0,
scenesTraveled: 0,
},
currentScene: 'test-scene',
storyHistory: [],
characterChats: {},
animationState: AnimationState.IDLE,
currentEncounter: null,
npcInteractionActive: false,
currentScenePreset: null,
sceneHostileNpcs: [],
playerX: 0,
playerOffsetY: 0,
playerFacing: 'right',
playerActionMode: 'idle',
scrollWorld: false,
inBattle: false,
playerHp: 100,
playerMaxHp: 100,
playerMana: 30,
playerMaxMana: 30,
playerSkillCooldowns: {},
activeCombatEffects: [],
playerCurrency: 0,
playerInventory: [],
playerEquipment: {} as EquipmentLoadout,
npcStates: {},
quests: [],
roster: [],
companions: [],
currentBattleNpcId: null,
currentNpcBattleMode: null,
currentNpcBattleOutcome: null,
sparReturnEncounter: null,
sparPlayerHpBefore: null,
sparPlayerMaxHpBefore: null,
sparStoryHistoryBefore: null,
...overrides,
} as GameState;
}
function createEncounter(overrides: Partial<Encounter> = {}): Encounter {
return {
id: 'runtime-npc',
kind: 'npc',
npcName: '雾中来客',
npcDescription: '带着临时生成形象的相遇者',
npcAvatar: '/avatar.png',
context: '桥边试探',
...overrides,
};
}
afterEach(() => {
vi.restoreAllMocks();
});
test('NPC 详情立绘优先展示遭遇实例形象,而不是 characterId 对应预设', () => {
const encounter = createEncounter({
characterId: 'sword-princess',
imageSrc: '/runtime-npc-preview.png',
});
render(
<AdventureEntityModal
selection={{ kind: 'npc', encounter }}
gameState={createGameState()}
onClose={() => undefined}
/>,
);
const portrait = screen.getByAltText('雾中来客');
expect(portrait.getAttribute('src')).toBe('/runtime-npc-preview.png');
expect(screen.queryByTestId('character-portrait')).toBeNull();
});
test('NPC 背包物品空 id 会被规范成稳定渲染 id', () => {
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined);
const encounter = createEncounter();
render(
<AdventureEntityModal
selection={{ kind: 'npc', encounter }}
gameState={createGameState({
npcStates: {
'runtime-npc': {
affinity: 0,
relationState: { affinity: 0, stance: 'neutral' },
helpUsed: false,
chattedCount: 0,
giftsGiven: 0,
inventory: [
{
id: '',
category: '材料',
name: '裂纹石片',
quantity: 1,
rarity: 'common',
tags: [],
},
{
id: '',
category: '材料',
name: '裂纹石片',
quantity: 2,
rarity: 'common',
tags: [],
},
],
recruited: false,
revealedFacts: [],
knownAttributeRumors: [],
firstMeaningfulContactResolved: false,
seenBackstoryChapterIds: [],
},
},
})}
onClose={() => undefined}
/>,
);
expect(screen.getAllByTitle(/裂纹石片 x/)).toHaveLength(2);
expect(
consoleErrorSpy.mock.calls.some((call) =>
call.some(
(arg) =>
typeof arg === 'string' &&
arg.includes('Encountered two children with the same key'),
),
),
).toBe(false);
});