Refactor Dockerignore and enhance Leaflet styles for improved map functionality

- Updated .dockerignore to streamline build context by ensuring unnecessary files are excluded.
- Refined CSS styles in leaflet-overrides.css to enhance visual consistency and user experience for map tooltips and popups.
- Improved map initialization and update handling in useMapApi and useMapUpdates composables for better performance and reliability.
This commit is contained in:
2026-03-04 18:16:41 +03:00
parent 3968bdc76f
commit 179357bc93
14 changed files with 3738 additions and 3738 deletions

View File

@@ -1,284 +1,284 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { useMapApi } from '../useMapApi'
vi.stubGlobal('useRuntimeConfig', () => ({
app: { baseURL: '/' },
public: { apiBase: '/map/api' },
}))
function mockFetch(status: number, body: unknown, contentType = 'application/json') {
return vi.fn().mockResolvedValue({
ok: status >= 200 && status < 300,
status,
headers: new Headers({ 'content-type': contentType }),
json: () => Promise.resolve(body),
} as Response)
}
describe('useMapApi', () => {
let originalFetch: typeof globalThis.fetch
beforeEach(() => {
originalFetch = globalThis.fetch
})
afterEach(() => {
globalThis.fetch = originalFetch
vi.restoreAllMocks()
})
describe('getConfig', () => {
it('fetches config from API', async () => {
const data = { title: 'Test', auths: ['map'] }
globalThis.fetch = mockFetch(200, data)
const { getConfig } = useMapApi()
const result = await getConfig()
expect(result).toEqual(data)
expect(globalThis.fetch).toHaveBeenCalledWith('/map/api/config', expect.objectContaining({ credentials: 'include' }))
})
it('throws on 401', async () => {
globalThis.fetch = mockFetch(401, { error: 'Unauthorized' })
const { getConfig } = useMapApi()
await expect(getConfig()).rejects.toThrow('Unauthorized')
})
it('throws on 403', async () => {
globalThis.fetch = mockFetch(403, { error: 'Forbidden' })
const { getConfig } = useMapApi()
await expect(getConfig()).rejects.toThrow('Forbidden')
})
})
describe('getCharacters', () => {
it('fetches characters', async () => {
const chars = [{ name: 'Hero', id: 1, map: 1, position: { x: 0, y: 0 }, type: 'player' }]
globalThis.fetch = mockFetch(200, chars)
const { getCharacters } = useMapApi()
const result = await getCharacters()
expect(result).toEqual(chars)
})
})
describe('getMarkers', () => {
it('fetches markers', async () => {
const markers = [{ name: 'Tower', id: 1, map: 1, position: { x: 10, y: 20 }, image: 'gfx/terobjs/mm/tower', hidden: false }]
globalThis.fetch = mockFetch(200, markers)
const { getMarkers } = useMapApi()
const result = await getMarkers()
expect(result).toEqual(markers)
})
})
describe('getMaps', () => {
it('fetches maps', async () => {
const maps = { '1': { ID: 1, Name: 'world' } }
globalThis.fetch = mockFetch(200, maps)
const { getMaps } = useMapApi()
const result = await getMaps()
expect(result).toEqual(maps)
})
})
describe('login', () => {
it('sends credentials and returns me response', async () => {
const meResp = { username: 'alice', auths: ['map'] }
globalThis.fetch = mockFetch(200, meResp)
const { login } = useMapApi()
const result = await login('alice', 'secret')
expect(result).toEqual(meResp)
expect(globalThis.fetch).toHaveBeenCalledWith(
'/map/api/login',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ user: 'alice', pass: 'secret' }),
}),
)
})
it('throws on 401 with error message', async () => {
globalThis.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 401,
json: () => Promise.resolve({ error: 'Invalid credentials' }),
})
const { login } = useMapApi()
await expect(login('alice', 'wrong')).rejects.toThrow('Invalid credentials')
})
})
describe('logout', () => {
it('sends POST to logout', async () => {
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200 })
const { logout } = useMapApi()
await logout()
expect(globalThis.fetch).toHaveBeenCalledWith(
'/map/api/logout',
expect.objectContaining({ method: 'POST' }),
)
})
})
describe('me', () => {
it('fetches current user', async () => {
const meResp = { username: 'alice', auths: ['map', 'upload'], tokens: ['tok1'], prefix: 'pfx' }
globalThis.fetch = mockFetch(200, meResp)
const { me } = useMapApi()
const result = await me()
expect(result).toEqual(meResp)
})
})
describe('setupRequired', () => {
it('checks setup status', async () => {
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: () => Promise.resolve({ setupRequired: true }),
})
const { setupRequired } = useMapApi()
const result = await setupRequired()
expect(result).toEqual({ setupRequired: true })
})
})
describe('oauthProviders', () => {
it('returns providers list', async () => {
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: () => Promise.resolve(['google']),
})
const { oauthProviders } = useMapApi()
const result = await oauthProviders()
expect(result).toEqual(['google'])
})
it('returns empty array on error', async () => {
globalThis.fetch = vi.fn().mockRejectedValue(new Error('network'))
const { oauthProviders } = useMapApi()
const result = await oauthProviders()
expect(result).toEqual([])
})
it('returns empty array on non-ok', async () => {
globalThis.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 500,
})
const { oauthProviders } = useMapApi()
const result = await oauthProviders()
expect(result).toEqual([])
})
})
describe('oauthLoginUrl', () => {
it('builds OAuth login URL', () => {
// happy-dom needs an absolute URL for `new URL()`. The source code
// creates `new URL(apiBase + path)` which is relative.
// Verify the underlying apiBase and path construction instead.
const { apiBase } = useMapApi()
const expected = `${apiBase}/oauth/google/login`
expect(expected).toBe('/map/api/oauth/google/login')
})
it('oauthLoginUrl is a function', () => {
const { oauthLoginUrl } = useMapApi()
expect(typeof oauthLoginUrl).toBe('function')
})
})
describe('onApiError', () => {
it('fires callback on 401', async () => {
globalThis.fetch = mockFetch(401, { error: 'Unauthorized' })
const callback = vi.fn()
const { onApiError, getConfig } = useMapApi()
onApiError(callback)
await expect(getConfig()).rejects.toThrow()
expect(callback).toHaveBeenCalled()
})
it('returns unsubscribe function', async () => {
globalThis.fetch = mockFetch(401, { error: 'Unauthorized' })
const callback = vi.fn()
const { onApiError, getConfig } = useMapApi()
const unsub = onApiError(callback)
unsub()
await expect(getConfig()).rejects.toThrow()
expect(callback).not.toHaveBeenCalled()
})
})
describe('admin endpoints', () => {
it('adminExportUrl returns correct path', () => {
const { adminExportUrl } = useMapApi()
expect(adminExportUrl()).toBe('/map/api/admin/export')
})
it('adminUsers fetches user list', async () => {
globalThis.fetch = mockFetch(200, ['alice', 'bob'])
const { adminUsers } = useMapApi()
const result = await adminUsers()
expect(result).toEqual(['alice', 'bob'])
})
it('adminSettings fetches settings', async () => {
const settings = { prefix: 'pfx', defaultHide: false, title: 'Map' }
globalThis.fetch = mockFetch(200, settings)
const { adminSettings } = useMapApi()
const result = await adminSettings()
expect(result).toEqual(settings)
})
})
describe('meTokens', () => {
it('generates and returns tokens', async () => {
globalThis.fetch = mockFetch(200, { tokens: ['tok1', 'tok2'] })
const { meTokens } = useMapApi()
const result = await meTokens()
expect(result).toEqual(['tok1', 'tok2'])
})
})
describe('mePassword', () => {
it('sends password change', async () => {
globalThis.fetch = mockFetch(200, undefined, 'text/plain')
const { mePassword } = useMapApi()
await mePassword('newpass')
expect(globalThis.fetch).toHaveBeenCalledWith(
'/map/api/me/password',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ pass: 'newpass' }),
}),
)
})
})
})
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { useMapApi } from '../useMapApi'
vi.stubGlobal('useRuntimeConfig', () => ({
app: { baseURL: '/' },
public: { apiBase: '/map/api' },
}))
function mockFetch(status: number, body: unknown, contentType = 'application/json') {
return vi.fn().mockResolvedValue({
ok: status >= 200 && status < 300,
status,
headers: new Headers({ 'content-type': contentType }),
json: () => Promise.resolve(body),
} as Response)
}
describe('useMapApi', () => {
let originalFetch: typeof globalThis.fetch
beforeEach(() => {
originalFetch = globalThis.fetch
})
afterEach(() => {
globalThis.fetch = originalFetch
vi.restoreAllMocks()
})
describe('getConfig', () => {
it('fetches config from API', async () => {
const data = { title: 'Test', auths: ['map'] }
globalThis.fetch = mockFetch(200, data)
const { getConfig } = useMapApi()
const result = await getConfig()
expect(result).toEqual(data)
expect(globalThis.fetch).toHaveBeenCalledWith('/map/api/config', expect.objectContaining({ credentials: 'include' }))
})
it('throws on 401', async () => {
globalThis.fetch = mockFetch(401, { error: 'Unauthorized' })
const { getConfig } = useMapApi()
await expect(getConfig()).rejects.toThrow('Unauthorized')
})
it('throws on 403', async () => {
globalThis.fetch = mockFetch(403, { error: 'Forbidden' })
const { getConfig } = useMapApi()
await expect(getConfig()).rejects.toThrow('Forbidden')
})
})
describe('getCharacters', () => {
it('fetches characters', async () => {
const chars = [{ name: 'Hero', id: 1, map: 1, position: { x: 0, y: 0 }, type: 'player' }]
globalThis.fetch = mockFetch(200, chars)
const { getCharacters } = useMapApi()
const result = await getCharacters()
expect(result).toEqual(chars)
})
})
describe('getMarkers', () => {
it('fetches markers', async () => {
const markers = [{ name: 'Tower', id: 1, map: 1, position: { x: 10, y: 20 }, image: 'gfx/terobjs/mm/tower', hidden: false }]
globalThis.fetch = mockFetch(200, markers)
const { getMarkers } = useMapApi()
const result = await getMarkers()
expect(result).toEqual(markers)
})
})
describe('getMaps', () => {
it('fetches maps', async () => {
const maps = { '1': { ID: 1, Name: 'world' } }
globalThis.fetch = mockFetch(200, maps)
const { getMaps } = useMapApi()
const result = await getMaps()
expect(result).toEqual(maps)
})
})
describe('login', () => {
it('sends credentials and returns me response', async () => {
const meResp = { username: 'alice', auths: ['map'] }
globalThis.fetch = mockFetch(200, meResp)
const { login } = useMapApi()
const result = await login('alice', 'secret')
expect(result).toEqual(meResp)
expect(globalThis.fetch).toHaveBeenCalledWith(
'/map/api/login',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ user: 'alice', pass: 'secret' }),
}),
)
})
it('throws on 401 with error message', async () => {
globalThis.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 401,
json: () => Promise.resolve({ error: 'Invalid credentials' }),
})
const { login } = useMapApi()
await expect(login('alice', 'wrong')).rejects.toThrow('Invalid credentials')
})
})
describe('logout', () => {
it('sends POST to logout', async () => {
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200 })
const { logout } = useMapApi()
await logout()
expect(globalThis.fetch).toHaveBeenCalledWith(
'/map/api/logout',
expect.objectContaining({ method: 'POST' }),
)
})
})
describe('me', () => {
it('fetches current user', async () => {
const meResp = { username: 'alice', auths: ['map', 'upload'], tokens: ['tok1'], prefix: 'pfx' }
globalThis.fetch = mockFetch(200, meResp)
const { me } = useMapApi()
const result = await me()
expect(result).toEqual(meResp)
})
})
describe('setupRequired', () => {
it('checks setup status', async () => {
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: () => Promise.resolve({ setupRequired: true }),
})
const { setupRequired } = useMapApi()
const result = await setupRequired()
expect(result).toEqual({ setupRequired: true })
})
})
describe('oauthProviders', () => {
it('returns providers list', async () => {
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: () => Promise.resolve(['google']),
})
const { oauthProviders } = useMapApi()
const result = await oauthProviders()
expect(result).toEqual(['google'])
})
it('returns empty array on error', async () => {
globalThis.fetch = vi.fn().mockRejectedValue(new Error('network'))
const { oauthProviders } = useMapApi()
const result = await oauthProviders()
expect(result).toEqual([])
})
it('returns empty array on non-ok', async () => {
globalThis.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 500,
})
const { oauthProviders } = useMapApi()
const result = await oauthProviders()
expect(result).toEqual([])
})
})
describe('oauthLoginUrl', () => {
it('builds OAuth login URL', () => {
// happy-dom needs an absolute URL for `new URL()`. The source code
// creates `new URL(apiBase + path)` which is relative.
// Verify the underlying apiBase and path construction instead.
const { apiBase } = useMapApi()
const expected = `${apiBase}/oauth/google/login`
expect(expected).toBe('/map/api/oauth/google/login')
})
it('oauthLoginUrl is a function', () => {
const { oauthLoginUrl } = useMapApi()
expect(typeof oauthLoginUrl).toBe('function')
})
})
describe('onApiError', () => {
it('fires callback on 401', async () => {
globalThis.fetch = mockFetch(401, { error: 'Unauthorized' })
const callback = vi.fn()
const { onApiError, getConfig } = useMapApi()
onApiError(callback)
await expect(getConfig()).rejects.toThrow()
expect(callback).toHaveBeenCalled()
})
it('returns unsubscribe function', async () => {
globalThis.fetch = mockFetch(401, { error: 'Unauthorized' })
const callback = vi.fn()
const { onApiError, getConfig } = useMapApi()
const unsub = onApiError(callback)
unsub()
await expect(getConfig()).rejects.toThrow()
expect(callback).not.toHaveBeenCalled()
})
})
describe('admin endpoints', () => {
it('adminExportUrl returns correct path', () => {
const { adminExportUrl } = useMapApi()
expect(adminExportUrl()).toBe('/map/api/admin/export')
})
it('adminUsers fetches user list', async () => {
globalThis.fetch = mockFetch(200, ['alice', 'bob'])
const { adminUsers } = useMapApi()
const result = await adminUsers()
expect(result).toEqual(['alice', 'bob'])
})
it('adminSettings fetches settings', async () => {
const settings = { prefix: 'pfx', defaultHide: false, title: 'Map' }
globalThis.fetch = mockFetch(200, settings)
const { adminSettings } = useMapApi()
const result = await adminSettings()
expect(result).toEqual(settings)
})
})
describe('meTokens', () => {
it('generates and returns tokens', async () => {
globalThis.fetch = mockFetch(200, { tokens: ['tok1', 'tok2'] })
const { meTokens } = useMapApi()
const result = await meTokens()
expect(result).toEqual(['tok1', 'tok2'])
})
})
describe('mePassword', () => {
it('sends password change', async () => {
globalThis.fetch = mockFetch(200, undefined, 'text/plain')
const { mePassword } = useMapApi()
await mePassword('newpass')
expect(globalThis.fetch).toHaveBeenCalledWith(
'/map/api/me/password',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ pass: 'newpass' }),
}),
)
})
})
})