收口前端平台组件库能力
新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件 迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome 补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
This commit is contained in:
266
src/components/NpcModals.test.tsx
Normal file
266
src/components/NpcModals.test.tsx
Normal file
@@ -0,0 +1,266 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import type { StoryGenerationNpcUi } from '../hooks/rpg-runtime-story';
|
||||
import {
|
||||
type Encounter,
|
||||
type GameState,
|
||||
type InventoryItem,
|
||||
WorldType,
|
||||
} from '../types';
|
||||
import { NpcModals } from './NpcModals';
|
||||
|
||||
vi.mock('motion/react', () => ({
|
||||
AnimatePresence: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||
motion: {
|
||||
div: ({
|
||||
animate: _animate,
|
||||
children,
|
||||
exit: _exit,
|
||||
initial: _initial,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement> & {
|
||||
animate?: unknown;
|
||||
exit?: unknown;
|
||||
initial?: unknown;
|
||||
}) => <div {...props}>{children}</div>,
|
||||
},
|
||||
}));
|
||||
|
||||
const encounter = {
|
||||
id: 'npc-merchant',
|
||||
kind: 'npc',
|
||||
npcName: '潮市商人',
|
||||
} as Encounter;
|
||||
|
||||
const tradeItem: InventoryItem = {
|
||||
id: 'moon-shell',
|
||||
category: '材料',
|
||||
name: '月壳',
|
||||
quantity: 3,
|
||||
rarity: 'rare',
|
||||
tags: [],
|
||||
};
|
||||
|
||||
const giftItem: InventoryItem = {
|
||||
id: 'rose-token',
|
||||
category: '礼物',
|
||||
name: '玫瑰信物',
|
||||
quantity: 1,
|
||||
rarity: 'rare',
|
||||
tags: [],
|
||||
};
|
||||
|
||||
function createNpcUi(): StoryGenerationNpcUi {
|
||||
return {
|
||||
tradeModal: {
|
||||
encounter,
|
||||
actionText: '交易',
|
||||
introText: '商人压低声音提示你。',
|
||||
mode: 'buy',
|
||||
selectedNpcItemId: 'moon-shell',
|
||||
selectedPlayerItemId: null,
|
||||
selectedQuantity: 1,
|
||||
},
|
||||
giftModal: {
|
||||
encounter,
|
||||
actionText: '赠礼',
|
||||
introText: '她更喜欢有纪念意义的礼物。',
|
||||
selectedItemId: 'rose-token',
|
||||
},
|
||||
recruitModal: null,
|
||||
setTradeMode: vi.fn(),
|
||||
selectTradeNpcItem: vi.fn(),
|
||||
selectTradePlayerItem: vi.fn(),
|
||||
setTradeQuantity: vi.fn(),
|
||||
closeTradeModal: vi.fn(),
|
||||
confirmTrade: vi.fn(),
|
||||
selectGiftItem: vi.fn(),
|
||||
closeGiftModal: vi.fn(),
|
||||
confirmGift: vi.fn(),
|
||||
selectRecruitRelease: vi.fn(),
|
||||
closeRecruitModal: vi.fn(),
|
||||
confirmRecruit: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
function createEmptyNpcUi(): StoryGenerationNpcUi {
|
||||
const ui = createNpcUi();
|
||||
|
||||
return {
|
||||
...ui,
|
||||
tradeModal: ui.tradeModal
|
||||
? {
|
||||
...ui.tradeModal,
|
||||
selectedNpcItemId: null,
|
||||
selectedPlayerItemId: null,
|
||||
}
|
||||
: null,
|
||||
giftModal: ui.giftModal
|
||||
? {
|
||||
...ui.giftModal,
|
||||
selectedItemId: null,
|
||||
}
|
||||
: null,
|
||||
recruitModal: {
|
||||
encounter,
|
||||
actionText: '邀请同行',
|
||||
introText: '同行名额已满,需要先让一人离队。',
|
||||
selectedReleaseNpcId: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createGameState(): GameState {
|
||||
return {
|
||||
worldType: WorldType.CUSTOM,
|
||||
playerCurrency: 24,
|
||||
runtimeNpcInteraction: {
|
||||
npcId: 'npc-merchant',
|
||||
npcName: '潮市商人',
|
||||
playerCurrency: 24,
|
||||
currencyName: '贝币',
|
||||
trade: {
|
||||
buyItems: [
|
||||
{
|
||||
itemId: 'moon-shell',
|
||||
item: tradeItem,
|
||||
mode: 'buy',
|
||||
unitPrice: 5,
|
||||
maxQuantity: 3,
|
||||
canSubmit: true,
|
||||
},
|
||||
],
|
||||
sellItems: [],
|
||||
},
|
||||
gift: {
|
||||
items: [
|
||||
{
|
||||
itemId: 'rose-token',
|
||||
item: giftItem,
|
||||
affinityGain: 8,
|
||||
canSubmit: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} as unknown as GameState;
|
||||
}
|
||||
|
||||
function createEmptyGameState(): GameState {
|
||||
const state = createGameState();
|
||||
|
||||
return {
|
||||
...state,
|
||||
companions: [],
|
||||
runtimeNpcInteraction: state.runtimeNpcInteraction
|
||||
? {
|
||||
...state.runtimeNpcInteraction,
|
||||
trade: {
|
||||
buyItems: [],
|
||||
sellItems: [],
|
||||
},
|
||||
gift: {
|
||||
items: [],
|
||||
},
|
||||
}
|
||||
: state.runtimeNpcInteraction,
|
||||
} as GameState;
|
||||
}
|
||||
|
||||
test('NPC 交易数量和赠礼好感复用暗色平台胶囊标签', () => {
|
||||
render(<NpcModals gameState={createGameState()} npcUi={createNpcUi()} />);
|
||||
|
||||
const quantityBadge = screen.getByText('x3');
|
||||
const affinityBadge = screen.getByText('好感 +8');
|
||||
const buyModeCard = screen.getByRole('button', { name: '购买物品' });
|
||||
const tradeItemCard = screen.getByRole('button', { name: /月壳/ });
|
||||
const giftItemCard = screen.getByRole('button', { name: /玫瑰信物/ });
|
||||
|
||||
expect(quantityBadge.className).toContain('rounded-full');
|
||||
expect(quantityBadge.className).toContain('font-black');
|
||||
expect(quantityBadge.className).toContain('bg-black/20');
|
||||
expect(affinityBadge.className).toContain('rounded-full');
|
||||
expect(affinityBadge.className).toContain('font-black');
|
||||
expect(affinityBadge.className).toContain('bg-rose-500/10');
|
||||
expect(buyModeCard.className).toContain('platform-dark-option-card');
|
||||
expect(buyModeCard.className).toContain('border-emerald-400/45');
|
||||
expect(tradeItemCard.className).toContain('platform-dark-option-card');
|
||||
expect(tradeItemCard.className).toContain('border-emerald-400/45');
|
||||
expect(giftItemCard.className).toContain('platform-dark-option-card');
|
||||
expect(giftItemCard.className).toContain('border-rose-400/60');
|
||||
});
|
||||
|
||||
test('NPC 交易静态信息卡复用暗色 PlatformSubpanel chrome', () => {
|
||||
render(<NpcModals gameState={createGameState()} npcUi={createNpcUi()} />);
|
||||
|
||||
[
|
||||
'npc-trade-list-summary',
|
||||
'npc-trade-detail-panel',
|
||||
'npc-trade-quantity-stepper',
|
||||
'npc-trade-total-panel',
|
||||
].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('NPC 弹窗叙事提示复用暗色平台状态条', () => {
|
||||
render(<NpcModals gameState={createGameState()} npcUi={createNpcUi()} />);
|
||||
|
||||
const tradeIntro = screen.getByText('商人压低声音提示你。');
|
||||
const giftIntro = screen.getByText('她更喜欢有纪念意义的礼物。');
|
||||
|
||||
expect(tradeIntro.className).toContain('platform-status-message');
|
||||
expect(tradeIntro.className).toContain('border-amber-300/15');
|
||||
expect(tradeIntro.className).toContain('bg-amber-500/10');
|
||||
expect(giftIntro.className).toContain('platform-status-message');
|
||||
expect(giftIntro.className).toContain('border-rose-300/15');
|
||||
expect(giftIntro.className).toContain('bg-rose-500/10');
|
||||
});
|
||||
|
||||
test('NPC 交易详情静态属性复用暗色 PlatformSubpanel chrome', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<NpcModals gameState={createGameState()} npcUi={createNpcUi()} />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /月壳/ }));
|
||||
|
||||
['不可装备', '不可即时使用', '标签:无'].forEach((text) => {
|
||||
const panel = screen.getByText(text);
|
||||
|
||||
expect(panel.className).toContain('border-white/10');
|
||||
expect(panel.className).toContain('bg-black/25');
|
||||
expect(panel.className).toContain('rounded-xl');
|
||||
});
|
||||
});
|
||||
|
||||
test('NPC 弹窗空态复用暗色平台空态', () => {
|
||||
render(
|
||||
<NpcModals gameState={createEmptyGameState()} npcUi={createEmptyNpcUi()} />,
|
||||
);
|
||||
|
||||
[
|
||||
'对方暂时没有可出售的物品。',
|
||||
'当前没有适合送出的礼物。',
|
||||
'当前没有可替换的同行角色。',
|
||||
].forEach((text) => {
|
||||
const emptyState = screen.getByText(text);
|
||||
|
||||
expect(emptyState.className).toContain('platform-empty-state');
|
||||
expect(emptyState.className).toContain('border-dashed');
|
||||
expect(emptyState.className).toContain('bg-black/20');
|
||||
});
|
||||
|
||||
const recruitIntro = screen.getByText('同行名额已满,需要先让一人离队。');
|
||||
|
||||
expect(recruitIntro.className).toContain('platform-status-message');
|
||||
expect(recruitIntro.className).toContain('border-amber-300/15');
|
||||
});
|
||||
Reference in New Issue
Block a user