// 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) }) })