import { discoverMapEntries, resolveProjectMap, visualMapAssets } from './MapLoader'; import type { ProjectFile, ProjectManifest } from '../project/types'; const encode = (value: string) => new TextEncoder().encode(value); const file = (path: string, value = ''): ProjectFile => ({ path, data: encode(value), size: encode(value).byteLength, source: 'directory', mimeType: '', }); const descriptor = JSON.stringify({ schemaVersion: 1, id: 'warehouse', name: '仓库', coordinateSystem: { units: 'm', up: 'Z', forward: '+X' }, physics: { source: 'physics/world.xml' }, visual: { source: 'visuals/scene.glb' }, spawnPoints: [{ id: 'door', name: '入口', position: [1, 2, 0.3], yawDeg: 90 }], }); describe('MapLoader', () => { it('发现 map.json 并解析工程内资源', () => { const files = [ file('maps/warehouse/map.json', descriptor), file('maps/warehouse/physics/world.xml', ''), file('maps/warehouse/visuals/scene.glb'), ]; expect(discoverMapEntries(files)).toEqual([ { descriptorPath: 'maps/warehouse/map.json', schemaVersion: 1, id: 'warehouse', name: '仓库', physicsPath: 'maps/warehouse/physics/world.xml', visualPath: 'maps/warehouse/visuals/scene.glb', spawnPoints: [{ id: 'door', name: '入口' }], }, ]); const manifest: ProjectManifest = { id: 'test', name: 'test', files, entries: [], maps: discoverMapEntries(files), totalBytes: files.reduce((sum, item) => sum + item.size, 0), }; expect(resolveProjectMap(manifest, 'maps/warehouse/map.json')).toMatchObject({ physicsPath: 'maps/warehouse/physics/world.xml', visualPath: 'maps/warehouse/visuals/scene.glb', }); }); it('只为已放置实例解析视觉地图,并保留实例 id', () => { const files = [ file('maps/warehouse/map.json', descriptor), file('maps/warehouse/physics/world.xml', ''), file('maps/warehouse/visuals/scene.glb', 'glb'), ]; const projectManifest: ProjectManifest = { id: 'test', name: 'test', files, entries: [], maps: discoverMapEntries(files), totalBytes: files.reduce((sum, item) => sum + item.size, 0), }; expect(visualMapAssets(projectManifest, [])).toEqual([]); expect( visualMapAssets(projectManifest, [ { id: 'instance-a', name: '仓库副本', selection: { kind: 'project', descriptorPath: 'maps/warehouse/map.json', positionX: 2, positionY: -1, yawDeg: 30, }, }, ]), ).toEqual([ expect.objectContaining({ id: 'instance-a', name: '仓库副本', path: 'maps/warehouse/visuals/scene.glb', positionX: 2, positionY: -1, yawDeg: 30, }), ]); }); it('发现 schema V2 authoring 创作层', () => { const editable = JSON.stringify({ ...JSON.parse(descriptor), schemaVersion: 2, authoring: { source: 'authoring/map.scene.json' }, }); const files = [ file('maps/warehouse/map.json', editable), file('maps/warehouse/physics/world.xml'), file('maps/warehouse/visuals/scene.glb'), file( 'maps/warehouse/authoring/map.scene.json', JSON.stringify({ schemaVersion: 1, mapId: 'warehouse', revision: 0, objects: [], spawnPoints: [], }), ), ]; expect(discoverMapEntries(files)[0]).toMatchObject({ schemaVersion: 2, authoringPath: 'maps/warehouse/authoring/map.scene.json', }); }); it('拒绝没有物理产物的 authoring 地图', () => { const invalid = JSON.stringify({ schemaVersion: 2, id: 'editable', name: 'editable', coordinateSystem: { units: 'm', up: 'Z', forward: '+X' }, visual: { source: 'scene.glb' }, authoring: { source: 'map.scene.json' }, spawnPoints: [], }); expect(() => discoverMapEntries([file('map.json', invalid)])).toThrow('physics.source'); }); it('拒绝坐标约定错误、缺失资源和重复地图 id', () => { const wrongCoordinates = descriptor.replace('"Z"', '"Y"'); expect(() => discoverMapEntries([file('map.json', wrongCoordinates)])).toThrow( 'coordinateSystem', ); expect(() => discoverMapEntries([file('map.json', descriptor)])).toThrow('资源不存在'); const physics = file('physics/world.xml'); const visual = file('visuals/scene.glb'); expect(() => discoverMapEntries([ file( 'a/map.json', descriptor.replaceAll('physics/', '../physics/').replaceAll('visuals/', '../visuals/'), ), file( 'b/map.json', descriptor.replaceAll('physics/', '../physics/').replaceAll('visuals/', '../visuals/'), ), physics, visual, ]), ).toThrow('地图 id 重复'); }); });