Files
hnh-map/frontend-nuxt/lib/__tests__/Character.test.ts
Nikolay Tatarinov 40945c818b Enhance character marker functionality and tooltip integration
- Updated character icon properties for improved visual representation.
- Introduced tooltip binding to character markers, displaying coordinates and character names.
- Implemented smooth position animation for character markers during updates.
- Added tests to verify tooltip content updates and animation cancellation on removal.
2026-03-04 16:02:54 +03:00

188 lines
6.1 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import type L from 'leaflet'
import type { Map, LayerGroup } from 'leaflet'
import { createCharacter, type CharacterData, type CharacterMapViewRef } from '../Character'
const { leafletMock } = vi.hoisted(() => {
const markerMock = {
on: vi.fn().mockReturnThis(),
addTo: vi.fn().mockReturnThis(),
setLatLng: vi.fn().mockReturnThis(),
setIcon: vi.fn().mockReturnThis(),
bindTooltip: vi.fn().mockReturnThis(),
setTooltipContent: vi.fn().mockReturnThis(),
getLatLng: vi.fn().mockReturnValue({ lat: 0, lng: 0 }),
}
const Icon = vi.fn().mockImplementation(function (this: unknown) {
return {}
})
const L = {
marker: vi.fn(() => markerMock),
Icon,
point: vi.fn((x: number, y: number) => ({ x, y })),
}
return { leafletMock: L }
})
vi.mock('leaflet', () => ({
__esModule: true,
default: leafletMock,
marker: leafletMock.marker,
Icon: leafletMock.Icon,
}))
vi.mock('~/lib/LeafletCustomTypes', () => ({
HnHMaxZoom: 6,
TileSize: 100,
}))
function getL(): L {
return leafletMock as unknown as L
}
function makeCharData(overrides: Partial<CharacterData> = {}): CharacterData {
return {
name: 'Hero',
position: { x: 100, y: 200 },
type: 'player',
id: 1,
map: 1,
...overrides,
}
}
function makeMapViewRef(mapid = 1): CharacterMapViewRef {
return {
map: {
unproject: vi.fn(() => ({ lat: 0, lng: 0 })),
removeLayer: vi.fn(),
} as unknown as Map,
mapid,
markerLayer: {
removeLayer: vi.fn(),
addLayer: vi.fn(),
} as unknown as LayerGroup,
}
}
describe('createCharacter', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('creates character with correct properties', () => {
const char = createCharacter(makeCharData(), getL())
expect(char.id).toBe(1)
expect(char.name).toBe('Hero')
expect(char.position).toEqual({ x: 100, y: 200 })
expect(char.type).toBe('player')
expect(char.map).toBe(1)
expect(char.text).toBe('Hero')
expect(char.value).toBe(1)
})
it('starts with null leaflet marker', () => {
const char = createCharacter(makeCharData(), getL())
expect(char.leafletMarker).toBeNull()
})
it('add creates marker when character is on correct map', () => {
const char = createCharacter(makeCharData(), getL())
const mapview = makeMapViewRef(1)
char.add(mapview)
expect(mapview.map.unproject).toHaveBeenCalled()
})
it('add creates marker without title and binds Leaflet tooltip', () => {
const char = createCharacter(makeCharData({ position: { x: 100, y: 200 } }), getL())
const mapview = makeMapViewRef(1)
char.add(mapview)
expect(leafletMock.marker).toHaveBeenCalledWith(
expect.anything(),
expect.not.objectContaining({ title: expect.anything() })
)
const marker = char.leafletMarker as { bindTooltip: ReturnType<typeof vi.fn> }
expect(marker.bindTooltip).toHaveBeenCalledWith(
'Hero · 1, 2',
expect.objectContaining({ direction: 'top', permanent: false })
)
})
it('add does not create marker for different map', () => {
const char = createCharacter(makeCharData({ map: 2 }), getL())
const mapview = makeMapViewRef(1)
char.add(mapview)
expect(mapview.map.unproject).not.toHaveBeenCalled()
})
it('update changes position and map', () => {
const char = createCharacter(makeCharData(), getL())
const mapview = makeMapViewRef(1)
char.update(mapview, {
...makeCharData(),
position: { x: 300, y: 400 },
map: 2,
})
expect(char.position).toEqual({ x: 300, y: 400 })
expect(char.map).toBe(2)
})
it('remove on a character without leaflet marker does nothing', () => {
const char = createCharacter(makeCharData(), getL())
const mapview = makeMapViewRef(1)
char.remove(mapview) // should not throw
expect(char.leafletMarker).toBeNull()
})
it('setClickCallback works', () => {
const char = createCharacter(makeCharData(), getL())
const cb = vi.fn()
char.setClickCallback(cb)
})
it('update with changed ownedByMe updates marker icon', () => {
const char = createCharacter(makeCharData({ ownedByMe: false }), getL())
const mapview = makeMapViewRef(1)
char.add(mapview)
const marker = char.leafletMarker as { setIcon: ReturnType<typeof vi.fn> }
expect(marker.setIcon).not.toHaveBeenCalled()
char.update(mapview, makeCharData({ ownedByMe: true }))
expect(marker.setIcon).toHaveBeenCalledTimes(1)
})
it('update with position change updates tooltip content when marker exists', () => {
const char = createCharacter(makeCharData(), getL())
const mapview = makeMapViewRef(1)
char.add(mapview)
const marker = char.leafletMarker as { setTooltipContent: ReturnType<typeof vi.fn> }
marker.setTooltipContent.mockClear()
char.update(mapview, makeCharData({ position: { x: 350, y: 450 } }))
expect(marker.setTooltipContent).toHaveBeenCalledWith('Hero · 3, 4')
})
it('remove cancels active position animation', () => {
const cancelSpy = vi.spyOn(global, 'cancelAnimationFrame').mockImplementation(() => {})
let rafCallback: (() => void) | null = null
vi.spyOn(global, 'requestAnimationFrame').mockImplementation((cb: (() => void) | (FrameRequestCallback)) => {
rafCallback = typeof cb === 'function' ? cb : () => {}
return 1
})
const char = createCharacter(makeCharData(), getL())
const mapview = makeMapViewRef(1)
mapview.map.unproject = vi.fn(() => ({ lat: 1, lng: 1 }))
char.add(mapview)
const marker = char.leafletMarker as { getLatLng: ReturnType<typeof vi.fn> }
marker.getLatLng.mockReturnValue({ lat: 0, lng: 0 })
char.update(mapview, makeCharData({ position: { x: 200, y: 200 } }))
expect(rafCallback).not.toBeNull()
cancelSpy.mockClear()
char.remove(mapview)
expect(cancelSpy).toHaveBeenCalledWith(1)
cancelSpy.mockRestore()
vi.restoreAllMocks()
})
})