import { describe, expect, it } from 'vitest'; import { isModel3dViewerFormatSupported, isModel3dViewerOverSize, MODEL3D_VIEWER_MAX_MODEL_BYTES, Model3dViewerAbortedError, readModel3dViewerSource, resolveModel3dViewerFormat, resolveModel3dViewerFormatFromBytes, resolveModel3dViewerFormatFromMimeType, } from './loader'; describe('模型格式承诺范围', () => { it('只承诺 glb / gltf / fbx', () => { for (const format of ['glb', 'gltf', 'fbx']) { expect(isModel3dViewerFormatSupported(format)).toBe(true); } for (const format of [ 'obj', 'stl', 'usdz', '3mf', '', null, undefined, 7, ]) { expect(isModel3dViewerFormatSupported(format)).toBe(false); } }); }); describe('模型体积上限', () => { it('超过 64 MiB 判定为超限', () => { expect(isModel3dViewerOverSize(0)).toBe(false); expect(isModel3dViewerOverSize(MODEL3D_VIEWER_MAX_MODEL_BYTES)).toBe(false); expect(isModel3dViewerOverSize(MODEL3D_VIEWER_MAX_MODEL_BYTES + 1)).toBe( true, ); }); it('非有限值不算超限,由加载流程自行失败', () => { expect(isModel3dViewerOverSize(Number.NaN)).toBe(false); }); }); const GLB_MAGIC = new Uint8Array([0x67, 0x6c, 0x54, 0x46, 0x02, 0x00, 0x00]); const FBX_MAGIC = new Uint8Array([ 0x4b, 0x61, 0x79, 0x64, 0x61, 0x72, 0x61, 0x20, 0x46, 0x42, 0x58, 0x20, 0x42, 0x69, 0x6e, 0x61, 0x72, 0x79, 0x00, ]); describe('模型格式判定', () => { it('声明类型按子串匹配各家拼法', () => { expect(resolveModel3dViewerFormatFromMimeType('model/gltf-binary')).toBe( 'glb', ); expect(resolveModel3dViewerFormatFromMimeType('model/gltf+json')).toBe( 'gltf', ); expect( resolveModel3dViewerFormatFromMimeType('application/x-fbx; charset=x'), ).toBe('fbx'); expect( resolveModel3dViewerFormatFromMimeType('application/octet-stream'), ).toBeNull(); expect(resolveModel3dViewerFormatFromMimeType(null)).toBeNull(); }); it('字节魔数认出自包含的三种容器', () => { expect(resolveModel3dViewerFormatFromBytes(GLB_MAGIC)).toBe('glb'); expect(resolveModel3dViewerFormatFromBytes(FBX_MAGIC)).toBe('fbx'); expect( resolveModel3dViewerFormatFromBytes( new TextEncoder().encode('\n {"asset":{"version":"2.0"}}'), ), ).toBe('gltf'); expect( resolveModel3dViewerFormatFromBytes( new TextEncoder().encode('\uFEFF{"asset":{}}'), ), ).toBe('gltf'); expect( resolveModel3dViewerFormatFromBytes(new Uint8Array([0x00, 0x01])), ).toBeNull(); }); it('顺序是声明 → 魔数 → 地址扩展名', () => { // 声明正确时轮不到魔数。 expect( resolveModel3dViewerFormat({ declaredType: 'model/gltf-binary', url: 'https://x/model.fbx', bytes: FBX_MAGIC, }), ).toBe('glb'); // 声明写歪(provider 给 octet-stream、扩展名也不对)时按魔数纠正。 expect( resolveModel3dViewerFormat({ declaredType: 'application/octet-stream', url: 'https://x/model.obj', bytes: GLB_MAGIC, }), ).toBe('glb'); // 没声明也没字节时只剩扩展名这条派生线索。 expect( resolveModel3dViewerFormat({ url: 'https://x/model.glb?sig=1' }), ).toBe('glb'); expect(resolveModel3dViewerFormat({ url: 'https://x/model.FBX' })).toBe( 'fbx', ); }); it('三条线索都给不出答案时返回 null,交给加载流程报 unsupported-format', () => { expect( resolveModel3dViewerFormat({ declaredType: 'application/octet-stream', url: 'https://x/model.obj', bytes: new Uint8Array([0x00, 0x01]), }), ).toBeNull(); }); }); describe('模型下载可中止', () => { it('把宿主的 signal 交给 fetch,中止后抛取消错误而不是加载失败', async () => { const controller = new AbortController(); const originalFetch = globalThis.fetch; const calls: Array<{ url: string; signal?: AbortSignal | null }> = []; globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => { calls.push({ url: String(input), signal: init?.signal }); controller.abort(); return Promise.reject(new DOMException('aborted', 'AbortError')); }) as typeof fetch; try { await expect( readModel3dViewerSource( { kind: 'url', url: 'https://x/model.glb' }, controller.signal, ), ).rejects.toBeInstanceOf(Model3dViewerAbortedError); } finally { globalThis.fetch = originalFetch; } expect(calls).toEqual([ { url: 'https://x/model.glb', signal: controller.signal }, ]); }); });