/* @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 & { animate?: unknown; exit?: unknown; initial?: unknown; }) =>
{children}
, }, })); 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(); 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(); [ '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(); 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(); 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( , ); [ '对方暂时没有可出售的物品。', '当前没有适合送出的礼物。', '当前没有可替换的同行角色。', ].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'); });