/* @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(
,
);
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();
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();
const skillCardClassName = findNearestClassName(
screen.getByText('潮刃突进'),
'bg-black/25',
);
expect(skillCardClassName).toContain('border-white/5');
});
test('CharacterIdentityBadges renders role and level chips together', () => {
render(
,
);
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(
,
);
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(
,
);
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();
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(
,
);
expect(screen.getByText('Lv.6')).toBeTruthy();
expect(screen.getByText('72/120')).toBeTruthy();
});