import { fireEvent, render, screen } from '@testing-library/react'; import type { EditableMapDocument } from '../../map/editor/types'; import type { SimulationSnapshot } from '../../simulation/SimulationSession'; import { SceneOutliner, countSceneSearchResults } from './SceneOutliner'; const snapshot = { bodies: [ { id: 0, name: 'world', parentId: 0 }, { id: 1, name: 'base', parentId: 0 }, { id: 2, name: 'arm', parentId: 1 }, ], joints: [ { id: 7, name: 'arm_joint', bodyId: 2, type: 3, value: 0, min: -1, max: 1, limitMin: -1, limitMax: 1, limited: true, limitsIgnored: false, editable: true, axis: [0, 0, 1], }, ], model: { nbody: 3 }, } as SimulationSnapshot; const document: EditableMapDocument = { schemaVersion: 1, mapId: 'warehouse', revision: 0, objects: [ { id: 'crate-1', name: '木箱', type: 'box', pose: { position: [0, 0, 0.5], quaternion: [1, 0, 0, 0] }, parameters: { sizeX: 1, sizeY: 1, sizeZ: 1 }, friction: [1, 0.005, 0.0001], rgba: [0.5, 0.6, 0.7, 1], placementMode: 'auto_ground', enabled: true, }, ], spawnPoints: [], }; const maps = [ { id: 'map-a', name: '仓库', selection: { kind: 'project' as const, descriptorPath: 'maps/warehouse/map.json' }, }, ]; const documents = new Map([['maps/warehouse/map.json', document]]); function renderOutliner(overrides: Record = {}) { const props = { snapshot, maps, documents, selection: null, query: '', loading: false, pendingSceneChangeCount: 0, pendingSceneIds: [], onSelectBody: vi.fn(), onSelectJoint: vi.fn(), onJointHover: vi.fn(), onSelectMap: vi.fn(), onSelectMapObject: vi.fn(), onRemoveMap: vi.fn(), onApplyScene: vi.fn(), onDiscardScene: vi.fn(), ...overrides, }; render(); return props; } describe('SceneOutliner', () => { it('在一棵场景大纲中统一展示机器人、地图实例和地图物体', () => { renderOutliner(); expect(screen.getByLabelText('场景资产树')).toHaveTextContent('机器人'); expect(screen.getByLabelText('场景资产树')).toHaveTextContent('地图与环境'); expect(screen.getByRole('treeitem', { name: /arm_joint/ })).toBeVisible(); expect(screen.getByRole('treeitem', { name: /木箱/ })).toBeVisible(); expect(countSceneSearchResults(snapshot, maps, documents, '木箱')).toBe(2); }); it('把所有选择统一上报为稳定 id', () => { const props = renderOutliner(); fireEvent.click(screen.getByRole('treeitem', { name: 'base' })); fireEvent.click(screen.getByRole('treeitem', { name: /arm_joint/ })); fireEvent.click(screen.getByRole('treeitem', { name: /^仓库/ })); fireEvent.click(screen.getByRole('treeitem', { name: /木箱/ })); expect(props.onSelectBody).toHaveBeenCalledWith(1); expect(props.onSelectJoint).toHaveBeenCalledWith(7, 2); expect(props.onSelectMap).toHaveBeenCalledWith('map-a'); expect(props.onSelectMapObject).toHaveBeenCalledWith('map-a', 'crate-1'); }); it('在大纲中集中处理场景草稿与实例删除', () => { const props = renderOutliner({ pendingSceneChangeCount: 2, pendingSceneIds: ['map-a'], }); expect(screen.getByRole('status')).toHaveTextContent('2 项场景更改待应用'); fireEvent.click(screen.getByRole('button', { name: '应用场景' })); fireEvent.click(screen.getByRole('button', { name: '放弃更改' })); fireEvent.click(screen.getByRole('button', { name: '删除地图实例 仓库' })); expect(props.onApplyScene).toHaveBeenCalledOnce(); expect(props.onDiscardScene).toHaveBeenCalledOnce(); expect(props.onRemoveMap).toHaveBeenCalledWith('map-a'); }); });