收口前端平台组件库能力
新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件 迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome 补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
This commit is contained in:
172
src/components/CharacterPanel.test.tsx
Normal file
172
src/components/CharacterPanel.test.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
AnimationState,
|
||||
type Character,
|
||||
type CompanionRenderState,
|
||||
type EquipmentLoadout,
|
||||
WorldType,
|
||||
} from '../types';
|
||||
import { CharacterPanel } from './CharacterPanel';
|
||||
|
||||
vi.mock('./CharacterAnimator', () => ({
|
||||
CharacterAnimator: () => <div>角色动画</div>,
|
||||
}));
|
||||
|
||||
vi.mock('./MedievalNpcAnimator', () => ({
|
||||
MedievalNpcAnimator: () => <div>NPC 动画</div>,
|
||||
}));
|
||||
|
||||
vi.mock('./PixelCloseButton', () => ({
|
||||
PixelCloseButton: ({
|
||||
label,
|
||||
onClick,
|
||||
}: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}) => (
|
||||
<button type="button" onClick={onClick}>
|
||||
{label}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('./PixelIcon', () => ({
|
||||
PixelIcon: ({ className }: { className?: string }) => (
|
||||
<span className={className}>像素图标</span>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('./ResolvedAssetImage', () => ({
|
||||
ResolvedAssetImage: ({ alt }: { alt: string }) => <img alt={alt} />,
|
||||
}));
|
||||
|
||||
function createCharacter(id: string, name: string): Character {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
title: `${name}称号`,
|
||||
gender: 'female',
|
||||
description: `${name}描述`,
|
||||
backstory: `${name}背景故事`,
|
||||
avatar: `/${id}.png`,
|
||||
portrait: `/${id}.png`,
|
||||
assetFolder: 'roles',
|
||||
assetVariant: 'generated',
|
||||
attributes: {
|
||||
strength: 10,
|
||||
agility: 10,
|
||||
intelligence: 10,
|
||||
spirit: 10,
|
||||
},
|
||||
personality: `${name}性格`,
|
||||
skills: [],
|
||||
adventureOpenings: {},
|
||||
};
|
||||
}
|
||||
|
||||
function findSharedDarkPanelForText(text: string) {
|
||||
let current: HTMLElement | null = screen.getByText(text);
|
||||
|
||||
while (current) {
|
||||
if (
|
||||
current.className.includes('border-white/10') &&
|
||||
current.className.includes('bg-black/25')
|
||||
) {
|
||||
return current;
|
||||
}
|
||||
current = current.parentElement;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
test('角色面板详情静态信息复用暗色平台子面板和胶囊标签', async () => {
|
||||
const user = userEvent.setup();
|
||||
const playerCharacter = createCharacter('hero', '沈行');
|
||||
const companionCharacter = createCharacter('sword-princess', '闻雪');
|
||||
const companionRenderState: CompanionRenderState = {
|
||||
npcId: 'npc-companion-1',
|
||||
character: companionCharacter,
|
||||
hp: 42,
|
||||
maxHp: 60,
|
||||
mana: 18,
|
||||
maxMana: 30,
|
||||
skillCooldowns: {},
|
||||
animationState: AnimationState.IDLE,
|
||||
slot: 'upper',
|
||||
};
|
||||
|
||||
render(
|
||||
<CharacterPanel
|
||||
worldType={WorldType.WUXIA}
|
||||
playerCharacter={playerCharacter}
|
||||
playerHp={80}
|
||||
playerMaxHp={100}
|
||||
playerMana={25}
|
||||
playerMaxMana={40}
|
||||
playerEquipment={{} as EquipmentLoadout}
|
||||
companionRenderStates={[companionRenderState]}
|
||||
companionArcStates={[
|
||||
{
|
||||
characterId: companionCharacter.id,
|
||||
arcTheme: '潮声里的信任',
|
||||
currentStage: 'opening',
|
||||
activeConflictTags: [],
|
||||
pendingEventIds: [],
|
||||
resolvedEventIds: [],
|
||||
},
|
||||
]}
|
||||
companionResolutions={[
|
||||
{
|
||||
characterId: companionCharacter.id,
|
||||
resolutionType: 'bonded',
|
||||
summary: '闻雪与主角完成潮声誓约。',
|
||||
relatedThreadIds: ['thread-tide'],
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
const multiplierBadge = screen.getAllByText(/适配 x/u)[0];
|
||||
expect(multiplierBadge?.className).toContain('rounded-full');
|
||||
expect(multiplierBadge?.className).toContain('bg-emerald-500/10');
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /沈行/u }));
|
||||
|
||||
const levelProgressPanel = screen.getByTestId(
|
||||
'character-panel-level-progress',
|
||||
);
|
||||
|
||||
expect(levelProgressPanel.className).toContain('border-amber-300/18');
|
||||
expect(levelProgressPanel.className).toContain('bg-amber-500/8');
|
||||
expect(levelProgressPanel.className).toContain('rounded-xl');
|
||||
expect(findSharedDarkPanelForText('沈行背景故事')?.className).toContain(
|
||||
'bg-black/25',
|
||||
);
|
||||
expect(findSharedDarkPanelForText('沈行性格')?.className).toContain(
|
||||
'bg-black/25',
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '关闭角色详情' }));
|
||||
await user.click(screen.getByRole('button', { name: /闻雪/u }));
|
||||
|
||||
expect(findSharedDarkPanelForText('个人线阶段')?.className).toContain(
|
||||
'bg-black/25',
|
||||
);
|
||||
expect(findSharedDarkPanelForText('潮声里的信任')?.className).toContain(
|
||||
'bg-black/25',
|
||||
);
|
||||
const resolutionPanel = screen.getByTestId('character-panel-resolution');
|
||||
|
||||
expect(resolutionPanel.className).toContain('border-emerald-400/18');
|
||||
expect(resolutionPanel.className).toContain('bg-emerald-500/8');
|
||||
expect(resolutionPanel.className).toContain('rounded-xl');
|
||||
expect(findSharedDarkPanelForText('王庭剑')?.className).toContain(
|
||||
'bg-black/25',
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user