收口前端平台组件库能力
新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件 迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome 补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
This commit is contained in:
@@ -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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user