Files
Genarrative/src/components/rpg-runtime-panels/RpgAdventurePanel.questOffer.test.tsx
高物 a9febe7678
Some checks failed
CI / verify (push) Has been cancelled
1
2026-04-28 10:57:40 +08:00

237 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* @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<StoryMoment>(
createPendingQuestStory(pendingQuest),
);
const [quests, setQuests] = useState<QuestLogEntry[]>([]);
const acceptPendingOffer = vi.fn(() => {
queueMicrotask(() => {
setQuests([pendingQuest]);
setCurrentStory(createAcceptedQuestStory(pendingQuest));
});
return pendingQuest.id;
});
return (
<RpgAdventurePanel
aiError={null}
currentStory={currentStory}
isLoading={false}
displayedOptions={currentStory.options}
hideOptions={false}
canRefreshOptions={false}
onRefreshOptions={() => 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(<QuestOfferHarness />);
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();
});