515 lines
18 KiB
TypeScript
515 lines
18 KiB
TypeScript
/**
|
|
* Unit & Parity Tests for Diablo II v1.13c Authentic Dynamic Cursor System (Issue #148).
|
|
*
|
|
* Verifies:
|
|
* 1. Offline DC6 Cursor Extraction & Metadata Parity:
|
|
* - All 8 frames for protate.dc6, ohand.dc6, and orotate.dc6.
|
|
* - Zero runtime MPQ/DLL dependencies.
|
|
* 2. 3-State Cursor State Machine Transitions:
|
|
* - Initial state is POINTING.
|
|
* - Inactivity >= 2000ms advances to OPENING (plays ohand 8 frames sequentially).
|
|
* - Completion of OPENING advances to OPEN_IDLE (loops orotate 8 frames).
|
|
* - Any mouse movement or mouse event (mousedown/mouseup) resets the timer and reverts to POINTING.
|
|
* 3. Coordinates, Hotspot Alignment, and Rendering Bounds:
|
|
* - (mouseX, mouseY) tracking and bounds calculation.
|
|
* - Held items (cursorItem) rendered centered at cursor hotspot.
|
|
* 4. Integration with HudManager & Canvas.
|
|
*/
|
|
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import { existsSync, readFileSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import {
|
|
CursorController,
|
|
type CursorState,
|
|
type CursorRenderInfo,
|
|
} from '../src/ui/cursor.ts'
|
|
import {
|
|
BUYSELL_CURSOR_FRAMES,
|
|
CURSOR_METADATA,
|
|
type CursorAtlasMetadata,
|
|
type CursorFrameMeta,
|
|
} from '../src/ui/cursor-meta.ts'
|
|
import { BAKED_UI_MANIFEST } from '../src/ui/baked-ui-meta.ts'
|
|
import { HudManager } from '../src/ui/hud-manager.ts'
|
|
import { VENDOR_BUTTON_TABLE_X } from '../src/ui/world-panels.ts'
|
|
|
|
describe('Diablo II v1.13c Dynamic Cursor System (Issue #148 & #491)', () => {
|
|
describe('1. Asset Extraction & Atlas Metadata', () => {
|
|
it('verifies public/ui/cursor.png exists and is non-empty', () => {
|
|
const atlasPath = join(process.cwd(), 'public', 'ui', 'cursor.png')
|
|
expect(existsSync(atlasPath)).toBe(true)
|
|
const data = readFileSync(atlasPath)
|
|
expect(data.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('loads authentic D2 1.13c cursor atlas metadata with 34 total frames (24 hand + 10 buysell)', () => {
|
|
expect(CURSOR_METADATA.atlasWidth).toBe(256)
|
|
expect(CURSOR_METADATA.atlasHeight).toBe(176)
|
|
expect(CURSOR_METADATA.frameWidth).toBe(32)
|
|
expect(CURSOR_METADATA.frameHeight).toBe(32)
|
|
expect(CURSOR_METADATA.buysellFrameHeight).toBe(40)
|
|
|
|
// 8 frames per hand animation + 10 frames for buysell.dc6
|
|
expect(CURSOR_METADATA.protate).toHaveLength(8)
|
|
expect(CURSOR_METADATA.ohand).toHaveLength(8)
|
|
expect(CURSOR_METADATA.orotate).toHaveLength(8)
|
|
expect(CURSOR_METADATA.buysell).toHaveLength(10)
|
|
|
|
// 34 frames indexed in frames map
|
|
expect(Object.keys(CURSOR_METADATA.frames)).toHaveLength(34)
|
|
|
|
// D2Client.dll 0x6fbbb238 (+0x10) & 0x6fb36fb7 buysell.dc6 frame mapping
|
|
expect(BUYSELL_CURSOR_FRAMES).toEqual({
|
|
identify: 0,
|
|
repair: 1,
|
|
buy: 3,
|
|
sell: 4,
|
|
})
|
|
})
|
|
|
|
it('verifies all frame coordinates are bounded inside the 256x176 atlas', () => {
|
|
const allFrames = [
|
|
...CURSOR_METADATA.protate,
|
|
...CURSOR_METADATA.ohand,
|
|
...CURSOR_METADATA.orotate,
|
|
...CURSOR_METADATA.buysell,
|
|
]
|
|
|
|
for (const frame of allFrames) {
|
|
expect(frame.x).toBeGreaterThanOrEqual(0)
|
|
expect(frame.y).toBeGreaterThanOrEqual(0)
|
|
expect(frame.w).toBeGreaterThan(0)
|
|
expect(frame.h).toBeGreaterThan(0)
|
|
expect(frame.x + frame.w).toBeLessThanOrEqual(CURSOR_METADATA.atlasWidth)
|
|
expect(frame.y + frame.h).toBeLessThanOrEqual(CURSOR_METADATA.atlasHeight)
|
|
}
|
|
})
|
|
|
|
it('verifies DC6 offset parity and hotspot anchor definitions', () => {
|
|
// protate: offsetX = -1, hotspot = (0, 0)
|
|
for (const frame of CURSOR_METADATA.protate) {
|
|
expect(frame.offsetX).toBe(-1)
|
|
expect(frame.offsetY).toBe(24)
|
|
expect(frame.hotspotX).toBe(0)
|
|
expect(frame.hotspotY).toBe(0)
|
|
}
|
|
|
|
// ohand: frames 0..3 have offsetX = -1, frames 4..7 have offsetX = -2
|
|
for (let i = 0; i < 4; i++) {
|
|
expect(CURSOR_METADATA.ohand[i]!.offsetX).toBe(-1)
|
|
}
|
|
for (let i = 4; i < 8; i++) {
|
|
expect(CURSOR_METADATA.ohand[i]!.offsetX).toBe(-2)
|
|
}
|
|
|
|
// orotate: all frames have offsetX = -2
|
|
for (const frame of CURSOR_METADATA.orotate) {
|
|
expect(frame.offsetX).toBe(-2)
|
|
expect(frame.offsetY).toBe(24)
|
|
}
|
|
|
|
// buysell: 10 frames of 32x40, offset = (0, 0), hotspot = (0, 0)
|
|
for (const frame of CURSOR_METADATA.buysell) {
|
|
expect(frame.w).toBe(32)
|
|
expect(frame.h).toBe(40)
|
|
expect(frame.offsetX).toBe(0)
|
|
expect(frame.offsetY).toBe(0)
|
|
expect(frame.hotspotX).toBe(0)
|
|
expect(frame.hotspotY).toBe(0)
|
|
}
|
|
})
|
|
|
|
it('verifies baked UI manifest includes cursorAtlas entry', () => {
|
|
expect(BAKED_UI_MANIFEST.images.cursorAtlas).toBe('/ui/cursor.png')
|
|
})
|
|
})
|
|
|
|
describe('2. State Machine Logic & Idle Transitions', () => {
|
|
it('starts in POINTING state initially and while actively moving', () => {
|
|
const cursor = new CursorController(CURSOR_METADATA, 0)
|
|
expect(cursor.state).toBe('POINTING')
|
|
|
|
// Mouse movements keep resetting idle timer
|
|
cursor.handleMouseMove(100, 100, 500)
|
|
expect(cursor.state).toBe('POINTING')
|
|
expect(cursor.lastActivityTime).toBe(500)
|
|
|
|
cursor.handleMouseMove(150, 120, 1200)
|
|
expect(cursor.state).toBe('POINTING')
|
|
expect(cursor.lastActivityTime).toBe(1200)
|
|
|
|
// Below 2000ms idle, remains POINTING
|
|
expect(cursor.update(3199)).toBe('POINTING')
|
|
expect(cursor.state).toBe('POINTING')
|
|
})
|
|
|
|
it('transitions to OPENING after 2000ms idle and plays 8 frames sequentially', () => {
|
|
const cursor = new CursorController(CURSOR_METADATA, 1000)
|
|
cursor.frameDurationMs = 125
|
|
cursor.openingFrameDurationMs = 125
|
|
|
|
// At exactly 2000ms idle (t = 3000ms)
|
|
const stateAt2000 = cursor.update(3000)
|
|
expect(stateAt2000).toBe('OPENING')
|
|
expect(cursor.state).toBe('OPENING')
|
|
|
|
// Check frame progression during OPENING (8 frames * 125ms = 1000ms total)
|
|
// Frame 0 at t = 3000ms (0ms into opening)
|
|
let info = cursor.getCurrentFrame(3000)
|
|
expect(info.state).toBe('OPENING')
|
|
expect(info.animation).toBe('ohand')
|
|
expect(info.frameIndex).toBe(0)
|
|
|
|
// Frame 1 at t = 3125ms (125ms into opening)
|
|
info = cursor.getCurrentFrame(3125)
|
|
expect(info.frameIndex).toBe(1)
|
|
|
|
// Frame 4 at t = 3500ms (500ms into opening)
|
|
info = cursor.getCurrentFrame(3500)
|
|
expect(info.frameIndex).toBe(4)
|
|
|
|
// Frame 7 at t = 3875ms (875ms into opening)
|
|
info = cursor.getCurrentFrame(3875)
|
|
expect(info.frameIndex).toBe(7)
|
|
|
|
// Frame 7 clamped right before completion at t = 3999ms
|
|
info = cursor.getCurrentFrame(3999)
|
|
expect(info.state).toBe('OPENING')
|
|
expect(info.frameIndex).toBe(7)
|
|
})
|
|
|
|
it('advances from OPENING to OPEN_IDLE after ohand completes and loops orotate', () => {
|
|
const cursor = new CursorController(CURSOR_METADATA, 0)
|
|
cursor.frameDurationMs = 125
|
|
cursor.openingFrameDurationMs = 125
|
|
|
|
// 2000ms idle + 1000ms opening = 3000ms total to reach OPEN_IDLE
|
|
expect(cursor.update(2999)).toBe('OPENING')
|
|
expect(cursor.update(3000)).toBe('OPEN_IDLE')
|
|
expect(cursor.state).toBe('OPEN_IDLE')
|
|
|
|
// Frame 0 of orotate at t = 3000ms
|
|
let info = cursor.getCurrentFrame(3000)
|
|
expect(info.state).toBe('OPEN_IDLE')
|
|
expect(info.animation).toBe('orotate')
|
|
expect(info.frameIndex).toBe(0)
|
|
|
|
// Frame 1 of orotate at t = 3125ms
|
|
info = cursor.getCurrentFrame(3125)
|
|
expect(info.frameIndex).toBe(1)
|
|
|
|
// Frame 7 of orotate at t = 3875ms
|
|
info = cursor.getCurrentFrame(3875)
|
|
expect(info.frameIndex).toBe(7)
|
|
|
|
// Loops back to frame 0 at t = 4000ms (1000ms into open idle)
|
|
info = cursor.getCurrentFrame(4000)
|
|
expect(info.state).toBe('OPEN_IDLE')
|
|
expect(info.frameIndex).toBe(0)
|
|
|
|
// Loops to frame 3 at t = 4375ms
|
|
info = cursor.getCurrentFrame(4375)
|
|
expect(info.frameIndex).toBe(3)
|
|
})
|
|
|
|
it('reverts immediately to POINTING upon mouse movement at any stage', () => {
|
|
const cursor = new CursorController(CURSOR_METADATA, 0)
|
|
|
|
// Fast forward to OPEN_IDLE (t = 5000ms)
|
|
cursor.update(5000)
|
|
expect(cursor.state).toBe('OPEN_IDLE')
|
|
|
|
// Mouse moves
|
|
cursor.handleMouseMove(400, 300, 5001)
|
|
expect(cursor.state).toBe('POINTING')
|
|
expect(cursor.lastActivityTime).toBe(5001)
|
|
expect(cursor.mouseX).toBe(400)
|
|
expect(cursor.mouseY).toBe(300)
|
|
|
|
const info = cursor.getCurrentFrame(5001)
|
|
expect(info.state).toBe('POINTING')
|
|
expect(info.animation).toBe('protate')
|
|
})
|
|
|
|
it('reverts immediately to POINTING upon mouse click (mousedown/mouseup) events', () => {
|
|
const cursor = new CursorController(CURSOR_METADATA, 0)
|
|
|
|
// In OPENING state (t = 2500ms)
|
|
cursor.update(2500)
|
|
expect(cursor.state).toBe('OPENING')
|
|
|
|
// Mousedown triggers reset
|
|
cursor.handleMouseDown(2501)
|
|
expect(cursor.state).toBe('POINTING')
|
|
|
|
// Advance again to OPENING (t = 4501ms)
|
|
cursor.update(4502)
|
|
expect(cursor.state).toBe('OPENING')
|
|
|
|
// Mouseup triggers reset
|
|
cursor.handleMouseUp(4503)
|
|
expect(cursor.state).toBe('POINTING')
|
|
})
|
|
|
|
it('switches to buysell.dc6 frame (buy=3, sell=4, repair=1, identify=0) and suppresses idle transitions per D2CLIENT_UpdateCursorIdle (0x6fac62f0)', () => {
|
|
const cursor = new CursorController(CURSOR_METADATA, 0)
|
|
cursor.handleMouseMove(250, 180, 0)
|
|
|
|
// Buy cursor -> buysell frame 3 (32x40)
|
|
cursor.setTradeCursorMode('buy', 100)
|
|
expect(cursor.tradeCursorMode).toBe('buy')
|
|
let info = cursor.getCurrentFrame(100)
|
|
expect(info.animation).toBe('buysell')
|
|
expect(info.frameIndex).toBe(3)
|
|
expect(info.frame).toBe(CURSOR_METADATA.buysell[3])
|
|
expect(info.bounds).toEqual({ x: 250, y: 180, width: 32, height: 40 })
|
|
|
|
// Even after 5000ms idle, trade cursor mode suppresses OPENING/OPEN_IDLE
|
|
expect(cursor.update(6000)).toBe('POINTING')
|
|
info = cursor.getCurrentFrame(6000)
|
|
expect(info.animation).toBe('buysell')
|
|
expect(info.frameIndex).toBe(3)
|
|
|
|
// Sell cursor -> buysell frame 4
|
|
cursor.setTradeCursorMode('sell', 6100)
|
|
info = cursor.getCurrentFrame(6100)
|
|
expect(info.animation).toBe('buysell')
|
|
expect(info.frameIndex).toBe(4)
|
|
expect(info.frame).toBe(CURSOR_METADATA.buysell[4])
|
|
|
|
// Repair cursor -> buysell frame 1
|
|
cursor.setTradeCursorMode('repair', 6200)
|
|
info = cursor.getCurrentFrame(6200)
|
|
expect(info.animation).toBe('buysell')
|
|
expect(info.frameIndex).toBe(1)
|
|
expect(info.frame).toBe(CURSOR_METADATA.buysell[1])
|
|
|
|
// Identify cursor -> buysell frame 0
|
|
cursor.setTradeCursorMode('identify', 6300)
|
|
info = cursor.getCurrentFrame(6300)
|
|
expect(info.animation).toBe('buysell')
|
|
expect(info.frameIndex).toBe(0)
|
|
expect(info.frame).toBe(CURSOR_METADATA.buysell[0])
|
|
|
|
// Clearing tradeCursorMode restores standard hand cursor (protate)
|
|
cursor.setTradeCursorMode(null, 6000)
|
|
info = cursor.getCurrentFrame(6000)
|
|
expect(info.state).toBe('POINTING')
|
|
expect(info.animation).toBe('protate')
|
|
expect(info.frameIndex).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe('3. Coordinates, Hotspot & Rendering Bounds', () => {
|
|
it('accurately aligns draw coordinates with mouse position and frame hotspots', () => {
|
|
const cursor = new CursorController(CURSOR_METADATA, 0)
|
|
cursor.handleMouseMove(320, 240, 0)
|
|
|
|
const info = cursor.getCurrentFrame(0)
|
|
expect(info.drawX).toBe(320)
|
|
expect(info.drawY).toBe(240)
|
|
expect(info.bounds).toEqual({
|
|
x: 320,
|
|
y: 240,
|
|
width: info.frame.w,
|
|
height: info.frame.h,
|
|
})
|
|
|
|
const bounds = cursor.getRenderBounds(0)
|
|
expect(bounds).toEqual(info.bounds)
|
|
})
|
|
|
|
it('renders hand cursor frames onto Canvas 2D context', () => {
|
|
const cursor = new CursorController(CURSOR_METADATA, 0)
|
|
cursor.handleMouseMove(100, 200, 0)
|
|
|
|
const mockCtx = {
|
|
drawImage: vi.fn(),
|
|
} as unknown as CanvasRenderingContext2D
|
|
|
|
const mockAtlas = {} as CanvasImageSource
|
|
|
|
cursor.render(mockCtx, mockAtlas, 0)
|
|
|
|
expect(mockCtx.drawImage).toHaveBeenCalledTimes(1)
|
|
const frame0 = CURSOR_METADATA.protate[0]!
|
|
expect(mockCtx.drawImage).toHaveBeenCalledWith(
|
|
mockAtlas,
|
|
frame0.x,
|
|
frame0.y,
|
|
frame0.w,
|
|
frame0.h,
|
|
100,
|
|
200,
|
|
frame0.w,
|
|
frame0.h,
|
|
)
|
|
})
|
|
|
|
it('renders held item centered at cursor hotspot when cursorItem is present', () => {
|
|
const cursor = new CursorController(CURSOR_METADATA, 0)
|
|
cursor.handleMouseMove(400, 300, 0)
|
|
|
|
const mockCtx = {
|
|
drawImage: vi.fn(),
|
|
} as unknown as CanvasRenderingContext2D
|
|
|
|
const mockItemsAtlas = {} as CanvasImageSource
|
|
const mockCursorAtlas = {} as CanvasImageSource
|
|
|
|
// Held 2x3 broad sword (width 28, height 84)
|
|
cursor.render(mockCtx, mockCursorAtlas, 0, {
|
|
atlas: mockItemsAtlas,
|
|
rect: { x: 50, y: 100, w: 28, h: 84 },
|
|
})
|
|
|
|
// Held item should be drawn centered at mouseX=400, mouseY=300
|
|
expect(mockCtx.drawImage).toHaveBeenCalledTimes(1)
|
|
expect(mockCtx.drawImage).toHaveBeenCalledWith(
|
|
mockItemsAtlas,
|
|
50,
|
|
100,
|
|
28,
|
|
84,
|
|
400 - 14, // 386
|
|
300 - 42, // 258
|
|
28,
|
|
84,
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('4. HudManager Integration & Vendor Buy/Sell/Repair Cursor Switching (Issue #491)', () => {
|
|
function createTestHud() {
|
|
const mockCanvas = {
|
|
style: {} as CSSStyleDeclaration,
|
|
width: 800,
|
|
height: 600,
|
|
getContext: vi.fn(() => ({
|
|
save: vi.fn(),
|
|
restore: vi.fn(),
|
|
clearRect: vi.fn(),
|
|
translate: vi.fn(),
|
|
scale: vi.fn(),
|
|
drawImage: vi.fn(),
|
|
})),
|
|
addEventListener: vi.fn(),
|
|
getBoundingClientRect: vi.fn(() => ({
|
|
left: 0,
|
|
top: 0,
|
|
width: 800,
|
|
height: 600,
|
|
})),
|
|
} as unknown as HTMLCanvasElement
|
|
|
|
const hud = new HudManager(mockCanvas, {
|
|
onToggleAutomap: vi.fn(),
|
|
onWaypointTeleport: vi.fn(),
|
|
})
|
|
|
|
const clickAt = (clientX: number, clientY: number, button = 0) => {
|
|
hud.handleHudMouseDown({
|
|
clientX,
|
|
clientY,
|
|
button,
|
|
shiftKey: false,
|
|
stopPropagation: vi.fn(),
|
|
preventDefault: vi.fn(),
|
|
} as unknown as MouseEvent)
|
|
hud.worldPanels.releaseMomentaryVendorButton()
|
|
}
|
|
|
|
return { hud, mockCanvas, clickAt }
|
|
}
|
|
|
|
it('initializes HudManager with cursor: none and tracks cursor state in published state', () => {
|
|
const { hud, mockCanvas } = createTestHud()
|
|
|
|
// Canvas cursor style hidden
|
|
expect(mockCanvas.style.cursor).toBe('none')
|
|
|
|
// Initial cursor state
|
|
expect(hud.cursor).toBeInstanceOf(CursorController)
|
|
expect(hud.cursor.state).toBe('POINTING')
|
|
expect(hud.state.cursorState).toBe('POINTING')
|
|
expect(hud.state.cursorAnimation).toBe('protate')
|
|
expect(hud.state.cursorFrameIndex).toBe(0)
|
|
|
|
// Moving mouse updates hud.cursor position and stays POINTING
|
|
hud.cursor.handleMouseMove(200, 150, 100)
|
|
hud.syncPublishedState()
|
|
expect(hud.cursor.mouseX).toBe(200)
|
|
expect(hud.cursor.mouseY).toBe(150)
|
|
expect(hud.state.cursorState).toBe('POINTING')
|
|
|
|
// Advancing past 2000ms idle updates hud.cursor and published state to OPENING
|
|
hud.cursor.update(2200)
|
|
hud.syncPublishedState()
|
|
expect(hud.cursor.state).toBe('OPENING')
|
|
expect(hud.state.cursorState).toBe('OPENING')
|
|
|
|
// Advancing past opening duration updates to OPEN_IDLE
|
|
hud.cursor.update(3500)
|
|
hud.syncPublishedState()
|
|
expect(hud.cursor.state).toBe('OPEN_IDLE')
|
|
expect(hud.state.cursorState).toBe('OPEN_IDLE')
|
|
})
|
|
|
|
it('switches cursor to buysell.dc6 frame 3 (Buy), frame 4 (Sell), and frame 1 (Repair) when clicking vendor buttons, and resets on toggle/right-click/close', () => {
|
|
const { hud, clickAt } = createTestHud()
|
|
|
|
// Open Charsi vendor panel
|
|
expect(hud.openVendorSession('Charsi', 'trade', 0)).toBe(true)
|
|
expect(hud.state.leftPanel).toBe('vendor')
|
|
expect(hud.state.vendorTradeState).toBe('idle')
|
|
expect(hud.state.cursorTradeMode).toBeNull()
|
|
expect(hud.state.cursorAnimation).toBe('protate')
|
|
|
|
// 1. Click Buy button (slot 0) -> switches cursor to buysell.dc6 frame 3
|
|
clickAt(80 + VENDOR_BUTTON_TABLE_X[0] + 20, 60 + 390, 0)
|
|
expect(hud.state.vendorTradeState).toBe('buy')
|
|
expect(hud.state.cursorTradeMode).toBe('buy')
|
|
expect(hud.state.cursorAnimation).toBe('buysell')
|
|
expect(hud.state.cursorFrameIndex).toBe(3)
|
|
|
|
// 2. Click Sell button (slot 1) -> switches cursor to buysell.dc6 frame 4
|
|
clickAt(80 + VENDOR_BUTTON_TABLE_X[1] + 20, 60 + 390, 0)
|
|
expect(hud.state.vendorTradeState).toBe('sell')
|
|
expect(hud.state.cursorTradeMode).toBe('sell')
|
|
expect(hud.state.cursorAnimation).toBe('buysell')
|
|
expect(hud.state.cursorFrameIndex).toBe(4)
|
|
|
|
// 3. Click Repair button (slot 2 on Charsi) -> switches cursor to buysell.dc6 frame 1
|
|
clickAt(80 + VENDOR_BUTTON_TABLE_X[2] + 20, 60 + 390, 0)
|
|
expect(hud.state.vendorTradeState).toBe('repair')
|
|
expect(hud.state.cursorTradeMode).toBe('repair')
|
|
expect(hud.state.cursorAnimation).toBe('buysell')
|
|
expect(hud.state.cursorFrameIndex).toBe(1)
|
|
|
|
// 4. Clicking the active Repair button again toggles back to idle and restores protate
|
|
clickAt(80 + VENDOR_BUTTON_TABLE_X[2] + 20, 60 + 390, 0)
|
|
expect(hud.state.vendorTradeState).toBe('idle')
|
|
expect(hud.state.cursorTradeMode).toBeNull()
|
|
expect(hud.state.cursorAnimation).toBe('protate')
|
|
|
|
// 5. Activate Buy mode again, then right-click inside vendor panel -> cancels back to idle cursor
|
|
clickAt(80 + VENDOR_BUTTON_TABLE_X[0] + 20, 60 + 390, 0)
|
|
expect(hud.state.cursorTradeMode).toBe('buy')
|
|
clickAt(80 + 150, 60 + 200, 2)
|
|
expect(hud.state.vendorTradeState).toBe('idle')
|
|
expect(hud.state.cursorTradeMode).toBeNull()
|
|
expect(hud.state.cursorAnimation).toBe('protate')
|
|
|
|
// 6. Activate Sell mode, then close panels -> cancels back to idle cursor
|
|
clickAt(80 + VENDOR_BUTTON_TABLE_X[1] + 20, 60 + 390, 0)
|
|
expect(hud.state.cursorTradeMode).toBe('sell')
|
|
hud.closeAllPanels()
|
|
expect(hud.state.leftPanel).toBe('none')
|
|
expect(hud.state.cursorTradeMode).toBeNull()
|
|
expect(hud.state.cursorAnimation).toBe('protate')
|
|
})
|
|
})
|
|
})
|
|
|