diablo2-web/tests/frontend/flow-state.test.ts

147 lines
5.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
CAMPFIRE_CLASS_ORDER,
cancelConnecting,
clickBattleNetButton,
confirmCharacterSelect,
createInitialFlowState,
enterCharCreate,
hoverCampfireClass,
isValidCharacterName,
selectCampfireClass,
stepFlowTick,
submitBattleNetLogin,
submitCharacterCreate,
submitCreateGame,
toggleChatGem,
} from '../../src/client/frontend/flow-state.ts'
describe('Diablo II v1.13c Front-End & Battle.net Flow State Machine', () => {
it('validates character names according to D2Launch.dll rules', () => {
expect(isValidCharacterName('A')).toBe(false)
expect(isValidCharacterName('Isendra')).toBe(true)
expect(isValidCharacterName('Dark_Wanderer')).toBe(true)
expect(isValidCharacterName('-Invalid')).toBe(false)
expect(isValidCharacterName('Invalid-')).toBe(false)
expect(isValidCharacterName('In--valid')).toBe(false)
expect(isValidCharacterName('TooLongCharacterName')).toBe(false)
})
it('walks through all 7 stages from Main Menu to Act 1 Rogue Encampment', () => {
const state = createInitialFlowState()
expect(state.stage).toBe('main_menu')
// Stage 1 -> Stage 2 Connecting Modal
clickBattleNetButton(state)
expect(state.stage).toBe('bnet_connecting')
// Can cancel back to main menu
cancelConnecting(state)
expect(state.stage).toBe('main_menu')
// Re-click Battle.net and advance 18 ticks to Login Screen
clickBattleNetButton(state)
for (let i = 0; i < 18; i++) {
stepFlowTick(state)
}
expect(state.stage).toBe('bnet_login')
expect(state.activeInput).toBe('accountName')
// Stage 2: Mock authentication accepts ANY username & password
state.accountName = 'any_user_123'
state.accountPassword = 'any_password_456'
expect(submitBattleNetLogin(state)).toBe(true)
// With empty initial roster, transitions directly to Stage 3 (7-Class Campfire)
expect(state.stage).toBe('char_create')
// Stage 3: Hover Amazon ('am') -> transitions from 'nu1' to 'nu2'
hoverCampfireClass(state, 'am')
expect(state.campfireClasses.am.phase).toBe('nu2')
// Click Sorceress ('so') -> walks forward ('fw' -> 'nu3')
selectCampfireClass(state, 'so')
expect(state.selectedClass).toBe('so')
expect(state.campfireClasses.so.phase).toBe('fw')
expect(state.activeInput).toBe('charName')
const fwCount = state.campfireClasses.so.frameCount.fw
for (let i = 0; i < fwCount; i++) {
stepFlowTick(state)
}
expect(state.campfireClasses.so.phase).toBe('nu3')
// Switching to Necromancer ('ne') causes Sorceress to walk backward ('bw' -> 'nu1')
selectCampfireClass(state, 'ne')
expect(state.selectedClass).toBe('ne')
expect(state.campfireClasses.so.phase).toBe('bw')
expect(state.campfireClasses.ne.phase).toBe('fw')
const bwCount = state.campfireClasses.so.frameCount.bw
for (let i = 0; i < bwCount; i++) {
stepFlowTick(state)
}
expect(state.campfireClasses.so.phase).toBe('nu1')
// Enter character name and click OK -> transitions to Stage 4 (8-Slot Character Selection)
state.charName = 'XulNecro'
state.isExpansion = true
state.isHardcore = false
const created = submitCharacterCreate(state)
expect(created).not.toBeNull()
expect(created?.name).toBe('XulNecro')
expect(created?.heroToken).toBe('ne')
expect(state.stage).toBe('char_select')
expect(state.roster).toHaveLength(1)
expect(state.selectedSlotIndex).toBe(0)
// Stage 4 -> Stage 5 (Battle.net Lobby & Create Game Room)
expect(confirmCharacterSelect(state)).toBe(true)
expect(state.stage).toBe('bnet_lobby')
expect(state.lobbySubPanel).toBe('create')
// Toggle Chat Gem in Lobby
toggleChatGem(state)
expect(state.gemActivated).toBe(true)
expect(state.chatMessages.at(-1)).toBe('Gem Activated')
toggleChatGem(state)
expect(state.gemActivated).toBe(false)
expect(state.chatMessages.at(-1)).toBe('Gem Deactivated')
// Stage 5: Configure room and click CREATE GAME -> transitions to Stage 6 (Door Opening Loading)
state.gameName = 'Sanctuary1'
state.gamePassword = '123'
state.difficulty = 'nightmare'
expect(submitCreateGame(state)).toBe(true)
expect(state.stage).toBe('door_loading')
expect(state.doorFrame).toBe(0)
// Stage 6: Step through all 10 frames (0..9) of Loadingscreen.dc6
for (let i = 0; i < 27; i++) {
stepFlowTick(state)
}
expect(state.doorFrame).toBe(9)
// Does not enter game until sceneReady is true
expect(state.stage).toBe('door_loading')
state.sceneReady = true
for (let i = 0; i < 5; i++) {
stepFlowTick(state)
}
// Stage 7: Act 1 Rogue Encampment active!
expect(state.stage).toBe('in_game')
})
it('maps Assassin campfire class id "as" to in-game hero token "ai"', () => {
const state = createInitialFlowState()
enterCharCreate(state)
expect(CAMPFIRE_CLASS_ORDER).toHaveLength(7)
selectCampfireClass(state, 'as')
state.charName = 'Natalya'
const created = submitCharacterCreate(state)
expect(created?.classId).toBe('as')
expect(created?.heroToken).toBe('ai')
expect(created?.className).toBe('Assassin')
})
})