/* @vitest-environment jsdom */ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { useState } from 'react'; import { beforeAll, expect, test, vi } from 'vitest'; import { AnimationState, type Character, type QuestLogEntry, type StoryMoment, type StoryOption, WorldType, } from '../../types'; import { RpgAdventurePanel } from './RpgAdventurePanel'; function createCharacter(): Character { return { id: 'hero', name: '沈行', title: '试剑客', description: '测试主角', backstory: '测试背景', avatar: '/hero.png', portrait: '/hero.png', assetFolder: 'hero', assetVariant: 'default', attributes: { strength: 10, agility: 10, intelligence: 8, spirit: 9, }, personality: 'calm', skills: [], adventureOpenings: {}, } as Character; } function createOption(functionId: string, actionText: string): StoryOption { return { functionId, actionText, text: actionText, visuals: { playerAnimation: AnimationState.IDLE, playerMoveMeters: 0, playerOffsetY: 0, playerFacing: 'right', scrollWorld: false, monsterChanges: [], }, }; } function createPendingQuest(): QuestLogEntry { return { id: 'quest-liu-1', issuerNpcId: 'npc-liu', issuerNpcName: '柳无声', sceneId: 'scene-bamboo', title: '竹林密信', description: '替柳无声查清竹林中的密信来源。', summary: '去竹林查清密信来源。', objective: { kind: 'inspect_treasure', requiredCount: 1, }, progress: 0, status: 'active', reward: { affinityBonus: 5, currency: 10, items: [], }, rewardText: '完成后可获得报酬。', }; } function createPendingQuestStory(quest: QuestLogEntry): StoryMoment { const viewOption = createOption('npc_chat_quest_offer_view', '查看任务'); viewOption.runtimePayload = { npcChatQuestOfferAction: 'view', }; const replaceOption = createOption('npc_chat_quest_offer_replace', '更换任务'); replaceOption.runtimePayload = { npcChatQuestOfferAction: 'replace', }; const abandonOption = createOption('npc_chat_quest_offer_abandon', '放弃任务'); abandonOption.runtimePayload = { npcChatQuestOfferAction: 'abandon', }; return { text: '柳无声把真正的委托说了出来。', displayMode: 'dialogue', dialogue: [ { speaker: 'npc', speakerName: '柳无声', text: '这件事我只想正式托付给你。' }, ], options: [viewOption, replaceOption, abandonOption], npcChatState: { npcId: 'npc-liu', npcName: '柳无声', turnCount: 2, customInputPlaceholder: '输入你想对 TA 说的话', pendingQuestOffer: { quest, }, }, }; } function createAcceptedQuestStory(quest: QuestLogEntry): StoryMoment { return { text: '柳无声把接下来的线索正式交给了你。', displayMode: 'dialogue', dialogue: [ { speaker: 'npc', speakerName: '柳无声', text: '这件事我只想正式托付给你。' }, { speaker: 'player', text: '这件事我愿意接下,你把关键要点交给我。' }, { speaker: 'npc', speakerName: '柳无声', text: '先去竹林查清密信来源。' }, ], options: [ createOption('npc_chat', '这件事里你最担心哪一步'), createOption('npc_chat', '我回来时你最想先知道什么'), ], npcChatState: { npcId: 'npc-liu', npcName: '柳无声', turnCount: 2, customInputPlaceholder: '输入你想对 TA 说的话', pendingQuestOffer: null, }, }; } function QuestOfferHarness() { const pendingQuest = createPendingQuest(); const [currentStory, setCurrentStory] = useState( createPendingQuestStory(pendingQuest), ); const [quests, setQuests] = useState([]); const acceptPendingOffer = vi.fn(() => { queueMicrotask(() => { setQuests([pendingQuest]); setCurrentStory(createAcceptedQuestStory(pendingQuest)); }); return pendingQuest.id; }); return ( undefined} onChoice={() => undefined} onSubmitNpcChatInput={() => true} onExitNpcChat={() => true} onOpenCharacter={() => undefined} onOpenInventory={() => undefined} playerCharacter={createCharacter()} worldType={WorldType.WUXIA} quests={quests} questUi={{ acknowledgeQuestCompletion: () => undefined, claimQuestReward: () => null, }} npcChatQuestOfferUi={{ replacePendingOffer: () => false, abandonPendingOffer: () => false, acceptPendingOffer, }} goalStack={{ northStarGoal: null, activeGoal: null, immediateStepGoal: null, supportGoals: [], }} goalPulse={null} onDismissGoalPulse={() => undefined} battleRewardUi={{ reward: null, dismiss: () => undefined, }} playerHp={100} playerMaxHp={100} playerMana={20} playerMaxMana={20} playerSkillCooldowns={{}} inBattle={false} currentNpcBattleMode={null} statistics={{ playTimeMs: 0, hostileNpcsDefeated: 0, questsAccepted: 0, questsCompleted: 0, questsTurnedIn: 0, itemsUsed: 0, scenesTraveled: 0, currentSceneName: '竹林古道', playerCurrency: 0, inventoryItemCount: 0, inventoryStackCount: 0, activeCompanionCount: 0, rosterCompanionCount: 0, }} musicVolume={0.6} onMusicVolumeChange={() => undefined} onSaveAndExit={() => undefined} /> ); } beforeAll(() => { if (!HTMLElement.prototype.scrollTo) { HTMLElement.prototype.scrollTo = () => undefined; } }); test('quest offer accept button reuses the shared accepted-quest follow-up chain', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByRole('button', { name: /查看任务/u })); await user.click(await screen.findByRole('button', { name: '领取任务' })); expect(await screen.findByText('任务进度:0/1')).toBeTruthy(); expect(screen.getAllByText('竹林密信').length).toBeGreaterThan(0); expect(screen.queryByText('待领取')).toBeNull(); expect(screen.getByText('这件事里你最担心哪一步')).toBeTruthy(); });