Add configuration files and update project documentation

- Introduced .editorconfig for consistent coding styles across the project.
- Added .golangci.yml for Go linting configuration.
- Updated AGENTS.md to clarify project structure and components.
- Enhanced CONTRIBUTING.md with Makefile usage for common tasks.
- Updated Dockerfiles to use Go 1.24 and improved build instructions.
- Refined README.md and deployment documentation for clarity.
- Added testing documentation in testing.md for backend and frontend tests.
- Introduced Makefile for streamlined development commands and tasks.
This commit is contained in:
2026-03-01 01:51:47 +03:00
parent 0466ff3087
commit 6529d7370e
92 changed files with 13411 additions and 8438 deletions

View File

@@ -0,0 +1,107 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
vi.mock('leaflet', () => {
const markerMock = {
on: vi.fn().mockReturnThis(),
addTo: vi.fn().mockReturnThis(),
setLatLng: vi.fn().mockReturnThis(),
}
return {
default: { marker: vi.fn(() => markerMock) },
marker: vi.fn(() => markerMock),
}
})
vi.mock('~/lib/LeafletCustomTypes', () => ({
HnHMaxZoom: 6,
}))
import { createCharacter, type CharacterData, type CharacterMapViewRef } from '../Character'
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 import('leaflet').Map,
mapid,
markerLayer: {
removeLayer: vi.fn(),
addLayer: vi.fn(),
} as unknown as import('leaflet').LayerGroup,
}
}
describe('createCharacter', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('creates character with correct properties', () => {
const char = createCharacter(makeCharData())
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())
expect(char.leafletMarker).toBeNull()
})
it('add creates marker when character is on correct map', () => {
const char = createCharacter(makeCharData())
const mapview = makeMapViewRef(1)
char.add(mapview)
expect(mapview.map.unproject).toHaveBeenCalled()
})
it('add does not create marker for different map', () => {
const char = createCharacter(makeCharData({ map: 2 }))
const mapview = makeMapViewRef(1)
char.add(mapview)
expect(mapview.map.unproject).not.toHaveBeenCalled()
})
it('update changes position and map', () => {
const char = createCharacter(makeCharData())
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())
const mapview = makeMapViewRef(1)
char.remove(mapview) // should not throw
expect(char.leafletMarker).toBeNull()
})
it('setClickCallback works', () => {
const char = createCharacter(makeCharData())
const cb = vi.fn()
char.setClickCallback(cb)
})
})

View File

@@ -0,0 +1,139 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
vi.mock('leaflet', () => {
const markerMock = {
on: vi.fn().mockReturnThis(),
addTo: vi.fn().mockReturnThis(),
setLatLng: vi.fn().mockReturnThis(),
remove: vi.fn().mockReturnThis(),
}
return {
default: {
marker: vi.fn(() => markerMock),
Icon: class {},
},
marker: vi.fn(() => markerMock),
Icon: class {},
}
})
vi.mock('~/lib/LeafletCustomTypes', () => ({
HnHMaxZoom: 6,
ImageIcon: class {
constructor(_opts: Record<string, unknown>) {}
},
}))
import { createMarker, type MarkerData, type MapViewRef } from '../Marker'
function makeMarkerData(overrides: Partial<MarkerData> = {}): MarkerData {
return {
id: 1,
position: { x: 100, y: 200 },
name: 'Tower',
image: 'gfx/terobjs/mm/tower',
hidden: false,
map: 1,
...overrides,
}
}
function makeMapViewRef(): MapViewRef {
return {
map: {
unproject: vi.fn(() => ({ lat: 0, lng: 0 })),
} as unknown as import('leaflet').Map,
mapid: 1,
markerLayer: {
removeLayer: vi.fn(),
addLayer: vi.fn(),
} as unknown as import('leaflet').LayerGroup,
}
}
describe('createMarker', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('creates a marker with correct properties', () => {
const marker = createMarker(makeMarkerData())
expect(marker.id).toBe(1)
expect(marker.name).toBe('Tower')
expect(marker.position).toEqual({ x: 100, y: 200 })
expect(marker.image).toBe('gfx/terobjs/mm/tower')
expect(marker.hidden).toBe(false)
expect(marker.map).toBe(1)
expect(marker.value).toBe(1)
expect(marker.text).toBe('Tower')
})
it('detects quest type', () => {
const marker = createMarker(makeMarkerData({ image: 'gfx/invobjs/small/bush' }))
expect(marker.type).toBe('quest')
})
it('detects quest type for bumling', () => {
const marker = createMarker(makeMarkerData({ image: 'gfx/invobjs/small/bumling' }))
expect(marker.type).toBe('quest')
})
it('detects custom type', () => {
const marker = createMarker(makeMarkerData({ image: 'custom' }))
expect(marker.type).toBe('custom')
})
it('extracts type from gfx path', () => {
const marker = createMarker(makeMarkerData({ image: 'gfx/terobjs/mm/village' }))
expect(marker.type).toBe('village')
})
it('starts with null leaflet marker', () => {
const marker = createMarker(makeMarkerData())
expect(marker.leafletMarker).toBeNull()
})
it('add creates a leaflet marker for non-hidden markers', () => {
const marker = createMarker(makeMarkerData())
const mapview = makeMapViewRef()
marker.add(mapview)
expect(mapview.map.unproject).toHaveBeenCalled()
})
it('add does nothing for hidden markers', () => {
const marker = createMarker(makeMarkerData({ hidden: true }))
const mapview = makeMapViewRef()
marker.add(mapview)
expect(mapview.map.unproject).not.toHaveBeenCalled()
})
it('update changes position and name', () => {
const marker = createMarker(makeMarkerData())
const mapview = makeMapViewRef()
marker.update(mapview, {
...makeMarkerData(),
position: { x: 300, y: 400 },
name: 'Castle',
})
expect(marker.position).toEqual({ x: 300, y: 400 })
expect(marker.name).toBe('Castle')
})
it('setClickCallback and setContextMenu work', () => {
const marker = createMarker(makeMarkerData())
const clickCb = vi.fn()
const contextCb = vi.fn()
marker.setClickCallback(clickCb)
marker.setContextMenu(contextCb)
})
it('remove on a marker without leaflet marker does nothing', () => {
const marker = createMarker(makeMarkerData())
const mapview = makeMapViewRef()
marker.remove(mapview) // should not throw
expect(marker.leafletMarker).toBeNull()
})
})

View File

@@ -0,0 +1,134 @@
import { describe, it, expect, vi } from 'vitest'
import {
createUniqueList,
uniqueListUpdate,
uniqueListGetElements,
uniqueListById,
} from '../UniqueList'
interface Item {
id: number
name: string
}
describe('createUniqueList', () => {
it('creates an empty list', () => {
const list = createUniqueList<Item>()
expect(list.elements).toEqual({})
expect(uniqueListGetElements(list)).toEqual([])
})
})
describe('uniqueListUpdate', () => {
it('adds new elements', () => {
const list = createUniqueList<Item>()
const addCb = vi.fn()
uniqueListUpdate(list, [{ id: 1, name: 'a' }, { id: 2, name: 'b' }], addCb)
expect(addCb).toHaveBeenCalledTimes(2)
expect(uniqueListGetElements(list)).toHaveLength(2)
expect(uniqueListById(list, 1)).toEqual({ id: 1, name: 'a' })
expect(uniqueListById(list, 2)).toEqual({ id: 2, name: 'b' })
})
it('removes elements no longer present', () => {
const list = createUniqueList<Item>()
const removeCb = vi.fn()
uniqueListUpdate(list, [{ id: 1, name: 'a' }, { id: 2, name: 'b' }])
uniqueListUpdate(list, [{ id: 1, name: 'a' }], undefined, removeCb)
expect(removeCb).toHaveBeenCalledTimes(1)
expect(removeCb).toHaveBeenCalledWith({ id: 2, name: 'b' })
expect(uniqueListGetElements(list)).toHaveLength(1)
expect(uniqueListById(list, 2)).toBeUndefined()
})
it('calls update callback for existing elements', () => {
const list = createUniqueList<Item>()
const updateCb = vi.fn()
uniqueListUpdate(list, [{ id: 1, name: 'a' }])
uniqueListUpdate(list, [{ id: 1, name: 'updated' }], undefined, undefined, updateCb)
expect(updateCb).toHaveBeenCalledTimes(1)
expect(updateCb).toHaveBeenCalledWith({ id: 1, name: 'a' }, { id: 1, name: 'updated' })
})
it('handles all callbacks together', () => {
const list = createUniqueList<Item>()
const addCb = vi.fn()
const removeCb = vi.fn()
const updateCb = vi.fn()
uniqueListUpdate(list, [{ id: 1, name: 'keep' }, { id: 2, name: 'remove' }])
uniqueListUpdate(
list,
[{ id: 1, name: 'kept' }, { id: 3, name: 'new' }],
addCb,
removeCb,
updateCb,
)
expect(addCb).toHaveBeenCalledTimes(1)
expect(addCb).toHaveBeenCalledWith({ id: 3, name: 'new' })
expect(removeCb).toHaveBeenCalledTimes(1)
expect(removeCb).toHaveBeenCalledWith({ id: 2, name: 'remove' })
expect(updateCb).toHaveBeenCalledTimes(1)
expect(updateCb).toHaveBeenCalledWith({ id: 1, name: 'keep' }, { id: 1, name: 'kept' })
})
it('works with string IDs', () => {
interface StringItem {
id: string
label: string
}
const list = createUniqueList<StringItem>()
uniqueListUpdate(list, [{ id: 'abc', label: 'first' }])
expect(uniqueListById(list, 'abc')).toEqual({ id: 'abc', label: 'first' })
})
it('handles empty update', () => {
const list = createUniqueList<Item>()
uniqueListUpdate(list, [{ id: 1, name: 'a' }])
const removeCb = vi.fn()
uniqueListUpdate(list, [], undefined, removeCb)
expect(removeCb).toHaveBeenCalledTimes(1)
expect(uniqueListGetElements(list)).toHaveLength(0)
})
it('handles update with no callbacks', () => {
const list = createUniqueList<Item>()
uniqueListUpdate(list, [{ id: 1, name: 'a' }])
uniqueListUpdate(list, [{ id: 2, name: 'b' }])
expect(uniqueListGetElements(list)).toHaveLength(1)
expect(uniqueListById(list, 2)).toEqual({ id: 2, name: 'b' })
})
})
describe('uniqueListGetElements', () => {
it('returns all elements as array', () => {
const list = createUniqueList<Item>()
uniqueListUpdate(list, [{ id: 1, name: 'a' }, { id: 2, name: 'b' }])
const elements = uniqueListGetElements(list)
expect(elements).toHaveLength(2)
expect(elements.map(e => e.id).sort()).toEqual([1, 2])
})
})
describe('uniqueListById', () => {
it('finds element by id', () => {
const list = createUniqueList<Item>()
uniqueListUpdate(list, [{ id: 42, name: 'target' }])
expect(uniqueListById(list, 42)?.name).toBe('target')
})
it('returns undefined for missing id', () => {
const list = createUniqueList<Item>()
expect(uniqueListById(list, 999)).toBeUndefined()
})
})