325 lines
8.3 KiB
TypeScript
325 lines
8.3 KiB
TypeScript
/* @vitest-environment jsdom */
|
|
|
|
import { render, screen } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { beforeEach, expect, test, vi } from 'vitest';
|
|
|
|
import {
|
|
AnimationState,
|
|
WorldType,
|
|
type GameState,
|
|
type StoryMoment,
|
|
} from '../../types';
|
|
import { RpgRuntimeShell } from './RpgRuntimeShell';
|
|
import type { RpgRuntimeShellProps } from './types';
|
|
|
|
const noop = () => {};
|
|
const asyncFalse = async () => false;
|
|
|
|
vi.mock('../auth/AuthUiContext', () => ({
|
|
useAuthUi: () => null,
|
|
}));
|
|
|
|
vi.mock('./useRpgRuntimeShellViewModel', () => ({
|
|
useRpgRuntimeShellViewModel: () => ({
|
|
selectionStage: 'platform',
|
|
setSelectionStage: () => {},
|
|
overlayPanel: null,
|
|
openOverlayPanel: () => {},
|
|
closeOverlayPanel: () => {},
|
|
selectedSceneEntity: null,
|
|
setSelectedSceneEntity: () => {},
|
|
openPartyMemberDetails: () => {},
|
|
closeAdventureEntityModal: () => {},
|
|
showTeamModal: false,
|
|
openCampModal: () => {},
|
|
closeCampModal: () => {},
|
|
resetForSaveAndExit: () => {},
|
|
shouldMountAdventureEntityModal: false,
|
|
shouldMountCampModal: false,
|
|
shouldMountMapModal: false,
|
|
shouldMountCharacterChatModal: false,
|
|
shouldMountNpcModals: false,
|
|
visibleGameState: mockVisibleGameState,
|
|
visibleCurrentStory: {
|
|
text: '测试故事',
|
|
options: [],
|
|
},
|
|
sceneTransitionPhase: 'idle',
|
|
sceneTransitionToken: 0,
|
|
setSceneTransitionDurations: () => {},
|
|
isCharacterSelectionStage: false,
|
|
shouldHideStoryOptions: false,
|
|
hideSelectionHero: false,
|
|
dialogueIndicator: {
|
|
showPlayer: false,
|
|
showEncounter: false,
|
|
activeSpeaker: null,
|
|
},
|
|
characterChatSummaries: {},
|
|
canvasCompanionRenderStates: [],
|
|
adventureStatistics: {
|
|
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,
|
|
},
|
|
handleSceneTransitionChoice: () => {},
|
|
}),
|
|
}));
|
|
|
|
vi.mock('./RpgRuntimeCanvasStage', () => ({
|
|
RpgRuntimeCanvasStage: () => <div>画布舞台</div>,
|
|
}));
|
|
|
|
vi.mock('./RpgRuntimeOverlayHost', () => ({
|
|
RpgRuntimeOverlayHost: () => null,
|
|
}));
|
|
|
|
vi.mock('./RpgRuntimeStageRouter', () => ({
|
|
RpgRuntimeStageRouter: () => <div>运行时主面板</div>,
|
|
}));
|
|
|
|
let mockVisibleGameState: GameState;
|
|
|
|
function createGameState(
|
|
runtimeMode: GameState['runtimeMode'],
|
|
runtimePersistenceDisabled?: boolean,
|
|
): GameState {
|
|
return {
|
|
worldType: WorldType.CUSTOM,
|
|
customWorldProfile: null,
|
|
playerCharacter: {
|
|
id: 'player-1',
|
|
name: '测试角色',
|
|
title: '测试者',
|
|
description: '测试角色',
|
|
backstory: '测试背景',
|
|
personality: '冷静',
|
|
avatar: '',
|
|
portrait: '',
|
|
assetFolder: '',
|
|
assetVariant: '',
|
|
attributes: {
|
|
strength: 5,
|
|
agility: 5,
|
|
intelligence: 5,
|
|
spirit: 5,
|
|
},
|
|
backstoryReveal: {
|
|
publicSummary: '测试',
|
|
privateChatUnlockAffinity: 60,
|
|
chapters: [],
|
|
},
|
|
skills: [],
|
|
adventureOpenings: {},
|
|
},
|
|
runtimeMode,
|
|
runtimePersistenceDisabled: runtimePersistenceDisabled ?? false,
|
|
runtimeStats: {
|
|
playTimeMs: 0,
|
|
lastPlayTickAt: null,
|
|
hostileNpcsDefeated: 0,
|
|
questsAccepted: 0,
|
|
itemsUsed: 0,
|
|
scenesTraveled: 0,
|
|
},
|
|
playerProgression: {
|
|
level: 1,
|
|
currentLevelXp: 0,
|
|
totalXp: 0,
|
|
xpToNextLevel: 100,
|
|
},
|
|
currentScene: 'Story',
|
|
storyHistory: [],
|
|
characterChats: {},
|
|
animationState: AnimationState.IDLE,
|
|
currentEncounter: null,
|
|
npcInteractionActive: false,
|
|
currentScenePreset: null,
|
|
sceneHostileNpcs: [],
|
|
playerX: 0,
|
|
playerOffsetY: 0,
|
|
playerFacing: 'right',
|
|
playerActionMode: 'idle',
|
|
scrollWorld: false,
|
|
inBattle: false,
|
|
playerHp: 100,
|
|
playerMaxHp: 100,
|
|
playerMana: 50,
|
|
playerMaxMana: 50,
|
|
playerSkillCooldowns: {},
|
|
activeCombatEffects: [],
|
|
playerCurrency: 0,
|
|
playerInventory: [],
|
|
playerEquipment: {
|
|
weapon: null,
|
|
armor: null,
|
|
relic: null,
|
|
},
|
|
npcStates: {},
|
|
quests: [],
|
|
roster: [],
|
|
companions: [],
|
|
currentBattleNpcId: null,
|
|
currentNpcBattleMode: null,
|
|
currentNpcBattleOutcome: null,
|
|
sparReturnEncounter: null,
|
|
sparPlayerHpBefore: null,
|
|
sparPlayerMaxHpBefore: null,
|
|
sparStoryHistoryBefore: null,
|
|
};
|
|
}
|
|
|
|
function buildProps(
|
|
runtimeMode: GameState['runtimeMode'],
|
|
runtimePersistenceDisabled?: boolean,
|
|
): RpgRuntimeShellProps {
|
|
const gameState = createGameState(runtimeMode, runtimePersistenceDisabled);
|
|
const currentStory: StoryMoment = {
|
|
text: '测试故事',
|
|
options: [],
|
|
};
|
|
mockVisibleGameState = gameState;
|
|
return {
|
|
session: {
|
|
gameState,
|
|
currentStory,
|
|
isLoading: false,
|
|
aiError: null,
|
|
bottomTab: 'adventure',
|
|
setBottomTab: () => {},
|
|
isMapOpen: false,
|
|
setIsMapOpen: () => {},
|
|
},
|
|
story: {
|
|
displayedOptions: [],
|
|
canRefreshOptions: false,
|
|
handleRefreshOptions: () => {},
|
|
handleChoice: () => {},
|
|
handleNpcChatInput: () => false,
|
|
refreshNpcChatOptions: () => false,
|
|
exitNpcChat: () => false,
|
|
handleMapTravelToScene: () => false,
|
|
npcUi: {
|
|
tradeModal: null,
|
|
giftModal: null,
|
|
recruitModal: null,
|
|
setTradeMode: noop,
|
|
selectTradeNpcItem: noop,
|
|
selectTradePlayerItem: noop,
|
|
setTradeQuantity: noop,
|
|
closeTradeModal: noop,
|
|
confirmTrade: noop,
|
|
selectGiftItem: noop,
|
|
closeGiftModal: noop,
|
|
confirmGift: noop,
|
|
selectRecruitRelease: noop,
|
|
closeRecruitModal: noop,
|
|
confirmRecruit: noop,
|
|
},
|
|
characterChatUi: {
|
|
modal: null,
|
|
openChat: noop,
|
|
closeChat: noop,
|
|
setDraft: noop,
|
|
useSuggestion: noop,
|
|
refreshSuggestions: noop,
|
|
sendDraft: noop,
|
|
},
|
|
inventoryUi: {
|
|
useInventoryItem: asyncFalse,
|
|
equipInventoryItem: asyncFalse,
|
|
unequipItem: asyncFalse,
|
|
playerCurrency: 0,
|
|
currencyText: '0',
|
|
backpackItems: [],
|
|
equipmentSlots: [],
|
|
forgeRecipes: [],
|
|
craftRecipe: asyncFalse,
|
|
dismantleItem: asyncFalse,
|
|
reforgeItem: asyncFalse,
|
|
},
|
|
battleRewardUi: {
|
|
reward: null,
|
|
dismiss: noop,
|
|
},
|
|
questUi: {
|
|
acknowledgeQuestCompletion: noop,
|
|
claimQuestReward: () => null,
|
|
},
|
|
npcChatQuestOfferUi: {
|
|
replacePendingOffer: () => false,
|
|
abandonPendingOffer: () => false,
|
|
acceptPendingOffer: () => null,
|
|
},
|
|
goalUi: {
|
|
goalStack: {
|
|
northStarGoal: null,
|
|
activeGoal: null,
|
|
immediateStepGoal: null,
|
|
supportGoals: [],
|
|
},
|
|
pulse: null,
|
|
dismissPulse: noop,
|
|
},
|
|
},
|
|
entry: {
|
|
hasSavedGame: false,
|
|
savedSnapshot: null,
|
|
handleContinueGame: () => {},
|
|
handleStartNewGame: () => {},
|
|
handleSaveAndExit: () => {},
|
|
handleCustomWorldSelect: () => {},
|
|
handleBackToWorldSelect: () => {},
|
|
handleCharacterSelect: () => {},
|
|
},
|
|
companions: {
|
|
companionRenderStates: [],
|
|
buildCompanionRenderStates: () => [],
|
|
onBenchCompanion: () => {},
|
|
onActivateRosterCompanion: () => {},
|
|
},
|
|
audio: {
|
|
musicVolume: 0.5,
|
|
onMusicVolumeChange: () => {},
|
|
},
|
|
};
|
|
}
|
|
|
|
beforeEach(() => {
|
|
mockVisibleGameState = createGameState('play');
|
|
});
|
|
|
|
test('结果页测试入口可显示结束测试按钮并触发退出回调', async () => {
|
|
const user = userEvent.setup();
|
|
const onExitRuntimePreview = vi.fn();
|
|
|
|
render(
|
|
<RpgRuntimeShell
|
|
{...buildProps('play', true)}
|
|
onExitRuntimePreview={onExitRuntimePreview}
|
|
showRuntimePreviewExit
|
|
/>,
|
|
);
|
|
|
|
await user.click(screen.getByRole('button', { name: '结束测试' }));
|
|
|
|
expect(onExitRuntimePreview).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('正式运行态不显示结束测试按钮', () => {
|
|
render(<RpgRuntimeShell {...buildProps('play')} />);
|
|
|
|
expect(screen.queryByRole('button', { name: '结束测试' })).toBeNull();
|
|
});
|