/* @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(); });