新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件 迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome 补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
267 lines
7.6 KiB
TypeScript
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');
|
|
});
|