145 lines
5.2 KiB
JavaScript
145 lines
5.2 KiB
JavaScript
// The invariant the whole map model rests on: a position without a level is not
|
|
// drawn on the default level.
|
|
//
|
|
// Nothing else catches this. Rewriting the composable left four components
|
|
// reading a property that no longer existed, which is `undefined` rather than a
|
|
// compile error - Vite built it happily and every marker would have been
|
|
// positioned at NaN percent. A build proves the code parses, not that a marker
|
|
// lands anywhere.
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
const list = vi.fn()
|
|
vi.mock('../api', () => ({ mapLevelsApi: { list: (...args) => list(...args) } }))
|
|
|
|
const {
|
|
state, loadMapConfig, reloadMapConfig, blueprintUrlFor, dimensionsFor,
|
|
hasLevel, levelName, levelOptions, setCurrentLevel,
|
|
} = await import('./mapConfig')
|
|
|
|
// Two buildings, three levels, sized like the real before and after: the ground
|
|
// floor at 3300x2550 and a second floor at 3308x4000.
|
|
const PAYLOAD = {
|
|
data: {
|
|
data: {
|
|
buildings: [
|
|
{
|
|
buildingid: 1, buildingname: 'Main', levels: [
|
|
{ levelid: 1, levelname: 'Ground floor', sortorder: 0, mapwidth: 3300,
|
|
mapheight: 2550, blueprintlight: '/x/g-light.png',
|
|
blueprintdark: '/x/g-dark.png', isdefault: true },
|
|
{ levelid: 2, levelname: 'Second floor', sortorder: 1, mapwidth: 3308,
|
|
mapheight: 4000, blueprintlight: '/x/2-light.png',
|
|
blueprintdark: null, isdefault: false },
|
|
],
|
|
},
|
|
{
|
|
buildingid: 2, buildingname: 'Annex', levels: [
|
|
{ levelid: 3, levelname: 'Ground floor', sortorder: 0, mapwidth: 1200,
|
|
mapheight: 900, blueprintlight: '/x/a-light.png',
|
|
blueprintdark: '/x/a-dark.png', isdefault: false },
|
|
],
|
|
},
|
|
],
|
|
defaultlevelid: 1,
|
|
},
|
|
},
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
list.mockReset()
|
|
list.mockResolvedValue(PAYLOAD)
|
|
state.loaded = false
|
|
state.currentlevelid = null
|
|
await reloadMapConfig()
|
|
})
|
|
|
|
describe('loading', () => {
|
|
it('indexes every level of every building and adopts the default', () => {
|
|
expect(Object.keys(state.levels)).toEqual(['1', '2', '3'])
|
|
expect(state.defaultlevelid).toBe(1)
|
|
expect(state.currentlevelid).toBe(1)
|
|
})
|
|
|
|
it('fetches once across concurrent callers', async () => {
|
|
list.mockClear()
|
|
state.loaded = false
|
|
await Promise.all([loadMapConfig(), loadMapConfig(), loadMapConfig()])
|
|
expect(list).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('survives an unreachable server without hanging every map on the page', async () => {
|
|
list.mockReset()
|
|
list.mockRejectedValue(new Error('network'))
|
|
state.loaded = false
|
|
await reloadMapConfig()
|
|
expect(state.loaded).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('dimensions belong to the level, not the site', () => {
|
|
it('returns each level its own native size', () => {
|
|
expect(dimensionsFor(1)).toEqual({ width: 3300, height: 2550 })
|
|
expect(dimensionsFor(2)).toEqual({ width: 3308, height: 4000 })
|
|
expect(dimensionsFor(3)).toEqual({ width: 1200, height: 900 })
|
|
})
|
|
|
|
it('does not report one level size for another', () => {
|
|
// The bug this guards: a marker on level 2 measured against level 1's
|
|
// height renders at 2550/4000 of the way down - plausible, and wrong.
|
|
expect(dimensionsFor(2).height).not.toBe(dimensionsFor(1).height)
|
|
})
|
|
})
|
|
|
|
describe('a position without a level is not drawn', () => {
|
|
it('has no blueprint for a null level', () => {
|
|
expect(blueprintUrlFor('light', null)).toBeNull()
|
|
expect(hasLevel(null)).toBe(false)
|
|
})
|
|
|
|
it('has no blueprint for a level this instance does not know', () => {
|
|
expect(blueprintUrlFor('light', 99)).toBeNull()
|
|
expect(hasLevel(99)).toBe(false)
|
|
})
|
|
|
|
it('never substitutes the default level for a missing one', () => {
|
|
const groundfloor = blueprintUrlFor('light', 1)
|
|
expect(groundfloor).toContain('g-light.png')
|
|
// The failure mode: returning the default blueprint for an unknown level.
|
|
expect(blueprintUrlFor('light', null)).not.toBe(groundfloor)
|
|
expect(blueprintUrlFor('light', 99)).not.toBe(groundfloor)
|
|
})
|
|
})
|
|
|
|
describe('blueprints', () => {
|
|
it('serves the theme asked for', () => {
|
|
expect(blueprintUrlFor('light', 1)).toContain('g-light.png')
|
|
expect(blueprintUrlFor('dark', 1)).toContain('g-dark.png')
|
|
})
|
|
|
|
it('falls back to the other theme rather than showing nothing', () => {
|
|
// Level 2 has no dark blueprint. A hard-to-read floor plan beats no floor
|
|
// plan, and a site that uploaded one image should still work in both themes.
|
|
expect(blueprintUrlFor('dark', 2)).toContain('2-light.png')
|
|
})
|
|
})
|
|
|
|
describe('naming and selection', () => {
|
|
it('qualifies a level by building only when there is more than one', () => {
|
|
// Both buildings have a 'Ground floor', so the name alone is ambiguous.
|
|
expect(levelName(1)).toBe('Main / Ground floor')
|
|
expect(levelName(3)).toBe('Annex / Ground floor')
|
|
})
|
|
|
|
it('offers every level in building then level order', () => {
|
|
expect(levelOptions().map(option => option.levelid)).toEqual([1, 2, 3])
|
|
})
|
|
|
|
it('refuses to make an unknown level current', () => {
|
|
setCurrentLevel(99)
|
|
expect(state.currentlevelid).toBe(1)
|
|
setCurrentLevel(2)
|
|
expect(state.currentlevelid).toBe(2)
|
|
})
|
|
})
|