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:
107
frontend-nuxt/lib/__tests__/Character.test.ts
Normal file
107
frontend-nuxt/lib/__tests__/Character.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
139
frontend-nuxt/lib/__tests__/Marker.test.ts
Normal file
139
frontend-nuxt/lib/__tests__/Marker.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
134
frontend-nuxt/lib/__tests__/UniqueList.test.ts
Normal file
134
frontend-nuxt/lib/__tests__/UniqueList.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user