84ded19f11
统一 NPC 弹窗底部动作按钮与交易详情空态到共享组件 统一角色构筑详情空明细到 PlatformEmptyState 补充 PlatformUiKit 收口计划、共享决策记录与对应测试护栏
226 lines
6.5 KiB
TypeScript
226 lines
6.5 KiB
TypeScript
/* @vitest-environment jsdom */
|
|
|
|
import { render, screen } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { afterEach, expect, test, vi } from 'vitest';
|
|
|
|
import type { BuildContributionRow } from '../data/buildDamage';
|
|
import { AnimationState, type Character } from '../types';
|
|
import {
|
|
BuildContributionDetailPanel,
|
|
CharacterIdentityBadges,
|
|
CharacterSkillsList,
|
|
MultiplierContributionList,
|
|
PlayerLevelProgress,
|
|
} from './CharacterInfoShared';
|
|
|
|
function createSkill(
|
|
name: string,
|
|
style: Character['skills'][number]['style'],
|
|
): Character['skills'][number] {
|
|
return {
|
|
id: '',
|
|
name,
|
|
animation: AnimationState.IDLE,
|
|
damage: 12,
|
|
manaCost: 4,
|
|
cooldownTurns: 2,
|
|
range: 1,
|
|
style,
|
|
};
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
function findNearestClassName(element: HTMLElement, className: string) {
|
|
let current: HTMLElement | null = element;
|
|
|
|
while (current) {
|
|
if (current.className.includes(className)) {
|
|
return current.className;
|
|
}
|
|
current = current.parentElement;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
test('CharacterSkillsList falls back to stable render ids when skill ids are empty', async () => {
|
|
const user = userEvent.setup();
|
|
const handleSelectSkill = vi.fn();
|
|
const consoleErrorSpy = vi
|
|
.spyOn(console, 'error')
|
|
.mockImplementation(() => undefined);
|
|
|
|
render(
|
|
<CharacterSkillsList
|
|
skills={[
|
|
createSkill('潮刃突进', 'burst'),
|
|
createSkill('雾行转位', 'mobility'),
|
|
]}
|
|
onSelectSkill={handleSelectSkill}
|
|
/>,
|
|
);
|
|
|
|
const buttons = screen.getAllByRole('button');
|
|
expect(buttons[0]?.className).toContain('bg-black/25');
|
|
expect(buttons[0]?.className).toContain('hover:border-sky-300/25');
|
|
|
|
await user.click(buttons[0]!);
|
|
await user.click(buttons[1]!);
|
|
|
|
const deliveryBadge = screen.getAllByText('近战')[0]!;
|
|
expect(deliveryBadge.className).toContain('rounded-full');
|
|
expect(deliveryBadge.className).toContain('bg-white/6');
|
|
|
|
expect(handleSelectSkill).toHaveBeenNthCalledWith(1, 'skill-潮刃突进-0');
|
|
expect(handleSelectSkill).toHaveBeenNthCalledWith(2, 'skill-雾行转位-1');
|
|
|
|
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);
|
|
});
|
|
|
|
test('CharacterSkillsList empty state reuses dark PlatformEmptyState chrome', () => {
|
|
render(<CharacterSkillsList skills={[]} emptyText="暂未掌握技能" />);
|
|
|
|
const emptyState = screen.getByText('暂未掌握技能');
|
|
|
|
expect(emptyState.className).toContain('platform-empty-state');
|
|
expect(emptyState.className).toContain('bg-black/20');
|
|
expect(emptyState.className).toContain('border-dashed');
|
|
});
|
|
|
|
test('CharacterSkillsList readonly cards reuse dark PlatformSubpanel chrome', () => {
|
|
render(<CharacterSkillsList skills={[createSkill('潮刃突进', 'burst')]} />);
|
|
|
|
const skillCardClassName = findNearestClassName(
|
|
screen.getByText('潮刃突进'),
|
|
'bg-black/25',
|
|
);
|
|
|
|
expect(skillCardClassName).toContain('border-white/5');
|
|
});
|
|
|
|
test('CharacterIdentityBadges renders role and level chips together', () => {
|
|
render(
|
|
<CharacterIdentityBadges
|
|
roleLabel="队长"
|
|
roleTone="amber"
|
|
levelText="Lv.7"
|
|
/>,
|
|
);
|
|
|
|
expect(screen.getByText('队长')).toBeTruthy();
|
|
expect(screen.getByText('Lv.7')).toBeTruthy();
|
|
expect(screen.getByText('队长').className).toContain('bg-amber-500/10');
|
|
expect(screen.getByText('Lv.7').className).toContain('bg-black/20');
|
|
});
|
|
|
|
test('MultiplierContributionList empty state reuses dark platform pill badge', () => {
|
|
render(
|
|
<MultiplierContributionList
|
|
breakdown={{
|
|
tags: [],
|
|
baseTagCount: 0,
|
|
buildDamageBonus: 0,
|
|
buildDamageMultiplier: 1,
|
|
rows: [],
|
|
}}
|
|
onSelectContribution={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
const panelClassName = findNearestClassName(
|
|
screen.getByText('状态标签'),
|
|
'bg-sky-500/8',
|
|
);
|
|
const emptyBadge = screen.getByText('当前还没有形成有效标签');
|
|
|
|
expect(panelClassName).toContain('border-sky-400/18');
|
|
expect(panelClassName).toContain('rounded-xl');
|
|
expect(emptyBadge.className).toContain('rounded-full');
|
|
expect(emptyBadge.className).toContain('bg-black/20');
|
|
});
|
|
|
|
test('BuildContributionDetailPanel reuses dark PlatformSubpanel chrome', () => {
|
|
const row: BuildContributionRow = {
|
|
label: '潮汐',
|
|
source: 'character',
|
|
fitScore: 0.72,
|
|
sourceCoefficient: 1,
|
|
bonusDelta: 0.12,
|
|
attributeSimilarities: {},
|
|
attributeWeights: {},
|
|
attributeContributions: {},
|
|
attributeModifierDeltas: { axis_a: 0.12 },
|
|
};
|
|
|
|
render(
|
|
<BuildContributionDetailPanel
|
|
row={row}
|
|
attributes={[
|
|
{
|
|
slotId: 'axis_a',
|
|
label: '武力',
|
|
similarity: 0.8,
|
|
weight: 1,
|
|
value: 0.8,
|
|
modifierDelta: 0.12,
|
|
percent: 12,
|
|
},
|
|
]}
|
|
/>,
|
|
);
|
|
|
|
const overviewPanel = screen.getByText('标签概览').closest('section');
|
|
const attributePanel = screen.getByText('属性加成').closest('section');
|
|
const attributeRow = screen.getByText('武力').closest('section');
|
|
|
|
expect(screen.getByText('潮汐')).toBeTruthy();
|
|
expect(screen.getByText('总加成 +12.0%')).toBeTruthy();
|
|
expect(screen.getByText('+12.0%')).toBeTruthy();
|
|
expect(overviewPanel?.className).toContain('bg-black/25');
|
|
expect(attributePanel?.className).toContain('bg-black/25');
|
|
expect(attributeRow?.className).toContain('bg-black/25');
|
|
});
|
|
|
|
test('BuildContributionDetailPanel empty state reuses dark PlatformEmptyState chrome', () => {
|
|
const row: BuildContributionRow = {
|
|
label: '潮汐',
|
|
source: 'character',
|
|
fitScore: 0.72,
|
|
sourceCoefficient: 1,
|
|
bonusDelta: 0.12,
|
|
attributeSimilarities: {},
|
|
attributeWeights: {},
|
|
attributeContributions: {},
|
|
attributeModifierDeltas: {},
|
|
};
|
|
|
|
render(<BuildContributionDetailPanel row={row} attributes={[]} />);
|
|
|
|
const emptyState = screen.getByText('当前标签还没有可展示的属性适配明细。');
|
|
|
|
expect(emptyState.className).toContain('platform-empty-state');
|
|
expect(emptyState.className).toContain('border-dashed');
|
|
expect(emptyState.className).toContain('bg-black/20');
|
|
});
|
|
|
|
test('PlayerLevelProgress renders xp progress details', () => {
|
|
render(
|
|
<PlayerLevelProgress level={6} currentLevelXp={72} xpToNextLevel={120} />,
|
|
);
|
|
|
|
expect(screen.getByText('Lv.6')).toBeTruthy();
|
|
expect(screen.getByText('72/120')).toBeTruthy();
|
|
});
|