/* @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: () =>
画布舞台
, })); vi.mock('./RpgRuntimeOverlayHost', () => ({ RpgRuntimeOverlayHost: () => null, })); vi.mock('./RpgRuntimeStageRouter', () => ({ RpgRuntimeStageRouter: () =>
运行时主面板
, })); 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( , ); await user.click(screen.getByRole('button', { name: '结束测试' })); expect(onExitRuntimePreview).toHaveBeenCalledTimes(1); }); test('正式运行态不显示结束测试按钮', () => { render(); expect(screen.queryByRole('button', { name: '结束测试' })).toBeNull(); });