import { act, renderHook } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { downloadBytes, readCachedText } from '../../project/cachedFiles'; import type { ProjectManifest } from '../../project/types'; import type { PhysicsAdapter } from '../../simulation/PhysicsAdapter'; import { useProjectSource } from './useProjectSource'; vi.mock('../../project/cachedFiles', async (importOriginal) => ({ ...(await importOriginal()), downloadBytes: vi.fn(), })); beforeEach(() => vi.clearAllMocks()); function fixture() { const text = '', data = new TextEncoder().encode(text); const manifest = { current: { id: 'p', name: '测试 工程.zip', files: [{ path: 'robot.urdf', data, size: data.length, source: 'zip', mimeType: '' }], entries: [{ path: 'robot.urdf', format: 'urdf', label: 'robot' }], maps: [], totalBytes: data.length, } as ProjectManifest | null, }; const exported = new TextEncoder().encode(''); const backend = { describeRobot: () => ({ profileId: 'lekiwi-v1' }), exportMjcf: vi.fn(() => exported), cachedSupportFiles: () => [], }; const events: string[] = []; const options: Parameters[0] = { manifest, adapter: { current: backend as unknown as PhysicsAdapter }, robotProfiles: { current: new Map() }, selectedEntry: 'robot.urdf', selectedFormat: 'urdf', onProjectUpdated: vi.fn(() => { events.push('project'); }), onDiagnostic: vi.fn(), notify: vi.fn(() => { events.push('notice'); }), loadEntry: vi.fn(async () => { events.push('load'); return false; }), }; return { options, backend, events, text, exported }; } describe('工程源码与导出动作', () => { it('先保留 profile/写缓存/发布清单和通知,再调用加载;编译失败仍保留可修改源码', async () => { const { options, events } = fixture(); const { result } = renderHook(() => useProjectSource(options)); await act(async () => { await result.current.saveCachedSource('cache.xml', ''); }); expect(events).toEqual(['project', 'notice', 'load']); expect(options.robotProfiles.current.get('cache.xml')).toBe('lekiwi-v1'); expect(readCachedText(options.manifest.current!, 'cache.xml')).toBe(''); expect(options.loadEntry).toHaveBeenCalledWith('cache.xml'); }); it('URDF 导出读取当前缓存,切换格式后入口失效;MJCF 保留 XML 资源提示', () => { const { options, text, exported } = fixture(); const { result, rerender } = renderHook((opts) => useProjectSource(opts), { initialProps: options, }); result.current.exportUrdf(); expect(downloadBytes).toHaveBeenCalledWith(new TextEncoder().encode(text), '测试_工程.urdf'); vi.mocked(downloadBytes).mockClear(); rerender({ ...options, selectedFormat: 'mjcf' }); result.current.exportUrdf(); expect(downloadBytes).not.toHaveBeenCalled(); result.current.exportMjcf(); expect(downloadBytes).toHaveBeenCalledWith(exported, '测试_工程.xml'); expect(options.notify).toHaveBeenCalledWith('MJCF 已导出', expect.stringContaining('仅 XML')); }); it('MJCF/ZIP 导出异常保留不同诊断类别与入口路径', () => { const { options, backend } = fixture(); backend.exportMjcf.mockImplementation(() => { throw new Error('导出失败'); }); const { result } = renderHook(() => useProjectSource(options)); result.current.exportMjcf(); result.current.exportProjectPackage(); expect(options.onDiagnostic).toHaveBeenNthCalledWith( 1, expect.objectContaining({ category: '模型编译', path: 'robot.urdf', detail: '导出失败' }), ); expect(options.onDiagnostic).toHaveBeenNthCalledWith( 2, expect.objectContaining({ category: '文件系统', path: 'robot.urdf', detail: '导出失败' }), ); expect(downloadBytes).not.toHaveBeenCalled(); }); });