收口前端平台组件库能力

新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件
迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome
补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
This commit is contained in:
2026-06-10 10:24:18 +08:00
parent a4ee6ff698
commit 1ad25e30f8
226 changed files with 23364 additions and 7825 deletions

View File

@@ -1,13 +1,15 @@
/* @vitest-environment jsdom */
import { render, screen } from '@testing-library/react';
import { fireEvent, render, screen, within } from '@testing-library/react';
import { afterEach, expect, test, vi } from 'vitest';
import {
AnimationState,
type Character,
type CompanionRenderState,
type Encounter,
type GameState,
type EquipmentLoadout,
type GameState,
WorldType,
} from '../types';
import { AdventureEntityModal } from './AdventureEntityModal';
@@ -87,6 +89,66 @@ function createEncounter(overrides: Partial<Encounter> = {}): Encounter {
};
}
function createPlayerCharacter(): Character {
return {
id: 'player-1',
name: '潮刃客',
title: '试剑者',
description: '测试主角',
backstory: '测试背景',
personality: '冷静',
avatar: '',
portrait: '',
assetFolder: '',
assetVariant: '',
attributes: {
strength: 5,
agility: 5,
intelligence: 5,
spirit: 5,
},
skills: [
{
id: 'tide-slash',
name: '潮刃突进',
animation: AnimationState.ATTACK,
damage: 16,
manaCost: 5,
cooldownTurns: 2,
range: 1,
style: 'burst',
buildBuffs: [
{
id: 'wet-mark',
sourceType: 'skill',
sourceId: 'tide-slash',
name: '潮湿',
tags: ['控制', '潮汐'],
durationTurns: 2,
},
],
},
],
adventureOpenings: {},
};
}
function createCompanionRenderState(
character: Character,
): CompanionRenderState {
return {
npcId: 'companion-1',
character,
hp: 100,
maxHp: 100,
mana: 20,
maxMana: 20,
skillCooldowns: {},
animationState: AnimationState.IDLE,
slot: 'upper',
};
}
afterEach(() => {
vi.restoreAllMocks();
});
@@ -169,3 +231,219 @@ test('NPC 背包物品空 id 会被规范成稳定渲染 id', () => {
),
).toBe(false);
});
test('物品空态复用暗色 PlatformEmptyState chrome', () => {
render(
<AdventureEntityModal
selection={{ kind: 'player' }}
gameState={createGameState({
playerCharacter: createPlayerCharacter(),
playerInventory: [],
})}
onClose={() => undefined}
/>,
);
const emptyState = screen.getByText('暂无物品');
const attributeSection = screen.getByText('属性').closest('section');
const itemSection = screen.getByText('物品').closest('section');
expect(emptyState.className).toContain('platform-empty-state');
expect(emptyState.className).toContain('border-dashed');
expect(emptyState.className).toContain('bg-black/20');
expect(attributeSection?.className).toContain('border-white/10');
expect(attributeSection?.className).toContain('bg-black/25');
expect(itemSection?.className).toContain('border-white/10');
expect(itemSection?.className).toContain('bg-black/25');
const levelPanel = screen.getByTestId('player-level-panel');
expect(levelPanel.className).toContain('border-amber-300/18');
expect(levelPanel.className).toContain('bg-amber-500/8');
expect(levelPanel.className).toContain('rounded-xl');
});
test('最近回响纯展示小卡复用暗色 PlatformSubpanel chrome', () => {
render(
<AdventureEntityModal
selection={{ kind: 'player' }}
gameState={createGameState({
playerCharacter: createPlayerCharacter(),
playerInventory: [
{
id: 'echo-shell',
category: '材料',
name: '回声贝壳',
quantity: 1,
rarity: 'rare',
tags: [],
runtimeMetadata: {
origin: 'procedural',
generationChannel: 'discovery',
seedKey: 'echo-shell-seed',
sourceReason: '测试最近回响载体',
storyFingerprint: {
visibleClue: '贝壳里仍有潮声回响',
witnessMark: '潮痕',
unresolvedQuestion: '潮声为何未散',
currentAppearanceReason: '被最近回响唤醒',
relatedThreadIds: [],
relatedScarIds: [],
reactionHooks: [],
},
},
},
],
currentScenePreset: {
narrativeResidues: [
{
id: 'residue-1',
title: '墙上残痕',
visibleClue: '刻着潮汐暗号。',
},
],
} as unknown as GameState['currentScenePreset'],
storyEngineMemory: {
chronicle: [
{
id: 'chronicle-1',
title: '潮声编年',
summary: '潮声把旧约刻回墙面。',
},
],
recentCarrierIds: ['echo-shell'],
consequenceLedger: [
{
id: 'consequence-1',
title: '旧约后果',
summary: '盟约开始反噬。',
relatedIds: ['player-1'],
},
],
} as unknown as GameState['storyEngineMemory'],
})}
onClose={() => undefined}
/>,
);
[
'recent-consequence-echo',
'recent-chronicle-echo',
'recent-carrier-echo',
'recent-scene-residue-echo',
].forEach((testId) => {
const panel = screen.getByTestId(testId);
expect(panel.className).toContain('border-white/10');
expect(panel.className).toContain('bg-black/25');
expect(panel.className).toContain('rounded-xl');
});
});
test('私聊和队友收束复用暗色 tint PlatformSubpanel chrome', () => {
const companionCharacter = createPlayerCharacter();
render(
<AdventureEntityModal
selection={{
kind: 'companion',
companion: createCompanionRenderState(companionCharacter),
}}
gameState={createGameState({
companions: [
{
npcId: 'companion-1',
characterId: companionCharacter.id,
joinedAtAffinity: 100,
hp: 100,
maxHp: 100,
mana: 20,
maxMana: 20,
skillCooldowns: {},
},
],
npcStates: {
'companion-1': {
affinity: 100,
relationState: { affinity: 100, stance: 'bonded' },
helpUsed: false,
chattedCount: 0,
giftsGiven: 0,
inventory: [],
recruited: true,
revealedFacts: [],
knownAttributeRumors: [],
firstMeaningfulContactResolved: true,
seenBackstoryChapterIds: [],
},
},
storyEngineMemory: {
companionResolutions: [
{
characterId: companionCharacter.id,
resolutionType: 'bonded',
summary: '潮声与同行者完成誓约。',
relatedThreadIds: ['thread-1'],
},
],
} as unknown as GameState['storyEngineMemory'],
})}
onClose={() => undefined}
onOpenCharacterChat={() => undefined}
/>,
);
const privateChatPanel = screen.getByTestId('private-chat-panel');
const companionResolutionEcho = screen.getByTestId(
'companion-resolution-echo',
);
expect(privateChatPanel.className).toContain('border-sky-400/18');
expect(privateChatPanel.className).toContain('bg-sky-500/8');
expect(privateChatPanel.className).toContain('rounded-[1.35rem]');
expect(companionResolutionEcho.className).toContain('border-emerald-400/18');
expect(companionResolutionEcho.className).toContain('bg-emerald-500/8');
expect(companionResolutionEcho.className).toContain('rounded-xl');
});
test('技能详情静态标签复用暗色 PlatformPillBadge chrome', () => {
render(
<AdventureEntityModal
selection={{ kind: 'player' }}
gameState={createGameState({
playerCharacter: createPlayerCharacter(),
})}
onClose={() => undefined}
/>,
);
fireEvent.click(screen.getByRole('button', { name: //u }));
const skillPanel = screen
.getByText('技能详情')
.closest('.pixel-modal-shell') as HTMLElement;
const deliveryBadge = within(skillPanel).getAllByText('近战')[0]!;
const styleBadge = within(skillPanel).getAllByText('爆发')[0]!;
const buffSummaryBadge = within(skillPanel).getByText('附带 1 个状态标签');
const buffBadge = within(skillPanel).getByText('潮湿 / 控制、潮汐 / 2 回合');
const damagePanel = within(skillPanel)
.getByText('伤害')
.closest('section') as HTMLElement;
const descriptionPanel = within(skillPanel)
.getByText(/ 线/u)
.closest('section') as HTMLElement;
const buffPanel = within(skillPanel)
.getByText('附带状态标签')
.closest('section') as HTMLElement;
expect(deliveryBadge.className).toContain('bg-white/6');
expect(styleBadge.className).toContain('bg-sky-500/10');
expect(buffSummaryBadge.className).toContain('bg-emerald-500/10');
expect(buffBadge.className).toContain('rounded-full');
expect(buffBadge.className).toContain('bg-sky-500/10');
expect(damagePanel.className).toContain('bg-black/25');
expect(damagePanel.className).toContain('border-white/10');
expect(descriptionPanel.className).toContain('bg-black/25');
expect(buffPanel.className).toContain('bg-black/25');
});