Files
Genarrative/src/components/NpcModals.test.tsx
kdletters 1ad25e30f8 收口前端平台组件库能力
新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件
迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome
补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
2026-06-10 10:24:18 +08:00

267 lines
7.6 KiB
TypeScript

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