furniture-3d-system-Wu/tests/lib/zip-parser.test.ts

320 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* ZIP 解析器测试
*
* 测试范围:
* - ZIP 文件验证
* - hardware.json 格式解析
* - 错误处理
*/
import { describe, it, expect } from '@jest/globals';
import JSZip from 'jszip';
describe('ZIP 解析器测试', () => {
describe('文件验证', () => {
it('应该拒绝非 ZIP 文件', async () => {
const { quickValidateZip } = await import('@/lib/zip-parser');
const notZipFile = new File(['test content'], 'test.txt', { type: 'text/plain' });
const result = await quickValidateZip(notZipFile);
expect(result.valid).toBe(false);
expect(result.error).toContain('ZIP');
});
it('应该拒绝超过 100MB 的文件', async () => {
const { quickValidateZip } = await import('@/lib/zip-parser');
// 创建一个模拟的大文件
const largeFile = {
name: 'large.zip',
size: 101 * 1024 * 1024, // 101MB
} as File;
const result = await quickValidateZip(largeFile);
expect(result.valid).toBe(false);
expect(result.error).toContain('100MB');
});
it('应该接受有效的 ZIP 文件', async () => {
const { quickValidateZip } = await import('@/lib/zip-parser');
const validZipFile = new File([''], 'valid.zip', { type: 'application/zip' });
// 注意:这里的 File 对象大小为 0,小于 100MB
Object.defineProperty(validZipFile, 'size', { value: 1024 * 1024 }); // 1MB
const result = await quickValidateZip(validZipFile);
expect(result.valid).toBe(true);
});
});
describe('hardware.json 格式', () => {
it('应该正确解析 hardware.json 数据', async () => {
const hardwareData = {
version: '2.0',
exportedAt: '2024-01-01T00:00:00Z',
software: 'AutoCAD',
modelFile: 'model.glb',
panels: [],
holes: [],
hardwareTypes: []
};
expect(hardwareData.version).toBe('2.0');
expect(hardwareData.software).toBe('AutoCAD');
expect(Array.isArray(hardwareData.panels)).toBe(true);
expect(Array.isArray(hardwareData.holes)).toBe(true);
});
it('应该处理缺少可选字段的数据', () => {
const minimalData: {
modelFile: string
hardwareTypes: unknown[]
version?: string
exportedAt?: string
software?: string
panels?: unknown[]
holes?: unknown[]
} = {
modelFile: 'model.glb',
hardwareTypes: []
};
// 使用默认值
const processedData = {
version: minimalData.version ?? '2.0',
exportedAt: minimalData.exportedAt ?? new Date().toISOString(),
software: minimalData.software ?? 'AutoCAD',
modelFile: minimalData.modelFile,
panels: minimalData.panels ?? [],
holes: minimalData.holes ?? [],
};
expect(processedData.version).toBe('2.0');
expect(processedData.software).toBe('AutoCAD');
expect(processedData.panels).toEqual([]);
});
});
describe('错误处理', () => {
it('应该处理无效的 JSON 格式', () => {
const invalidJson = 'not a json';
expect(() => JSON.parse(invalidJson)).toThrow();
});
it('应该检测缺少必需的 hardware.json', async () => {
// 创建一个没有 hardware.json 的 ZIP
const zip = new JSZip();
zip.file('model.glb', 'model content');
// 不添加 hardware.json
const content = await zip.generateAsync({ type: 'arraybuffer' });
const fileNames = Object.keys(zip.files);
const hasHardwareJson = fileNames.some(name =>
name.toLowerCase() === 'hardware.json' ||
name.toLowerCase().endsWith('/hardware.json')
);
expect(hasHardwareJson).toBe(false);
});
it('应该检测缺少模型文件', async () => {
// 创建一个没有模型文件的 ZIP
const zip = new JSZip();
zip.file('hardware.json', JSON.stringify({ version: '2.0' }));
// 不添加模型文件
const fileNames = Object.keys(zip.files);
const hasModelFile = fileNames.some(name => {
const lower = name.toLowerCase();
return lower.endsWith('.glb') || lower.endsWith('.obj');
});
expect(hasModelFile).toBe(false);
});
});
describe('支持的文件类型', () => {
it('应该支持 GLB 文件', () => {
const allowedExtensions = ['.glb', '.obj', '.json'];
expect(allowedExtensions).toContain('.glb');
});
it('应该支持 OBJ 文件', () => {
const allowedExtensions = ['.glb', '.obj', '.json'];
expect(allowedExtensions).toContain('.obj');
});
it('应该拒绝不支持的文件类型', () => {
const allowedExtensions = ['.glb', '.obj', '.json'];
const unsupportedExtension = '.fbx';
expect(allowedExtensions).not.toContain(unsupportedExtension);
});
});
});
describe('数据格式转换', () => {
it('应该将 hardware.json 转换为内部格式', () => {
const hardwareJson = {
version: '2.2.0',
pluginVersion: '2.2.0',
exportedAt: '2024-01-01T00:00:00Z',
software: 'AutoCAD 2024',
modelFile: 'model.glb',
panels: [
{
id: 'panel-1',
sourceHandle: 'handle-1',
position: [0, 0, 0],
size: [100, 50, 18],
normal: [0, 0, 1],
}
],
holes: [
{
id: 'hole-1',
panelId: 'panel-1',
position: [10, 20, 0],
diameter: 5,
depth: 10,
normal: [0, 0, 1],
type: 'through',
inferredUsage: 'screw',
}
],
hardwareTypes: []
};
// 转换后的格式
const internalFormat = {
version: hardwareJson.version,
exportedAt: hardwareJson.exportedAt,
software: hardwareJson.software,
modelFile: hardwareJson.modelFile,
holes: hardwareJson.holes,
panels: hardwareJson.panels,
};
expect(internalFormat.version).toBe('2.2.0');
expect(internalFormat.panels.length).toBe(1);
expect(internalFormat.holes.length).toBe(1);
expect(internalFormat.holes[0].panelId).toBe('panel-1');
});
it('应该支持 v2.2+ 导出(position/normal 为数组)', async () => {
const { parseZipFile } = await import('@/lib/zip-parser');
const zip = new JSZip();
zip.file('model.glb', 'model content');
zip.file('hardware.json', JSON.stringify({
version: '2.2.14',
pluginVersion: '2.2.14',
exportedAt: '2026-02-05T00:00:00Z',
software: 'AutoCAD 2026',
sourceFile: 'demo.dwg',
modelFile: 'model.glb',
panelCount: 1,
units: 'mm',
panels: [
{
id: 'panel-1',
sourceHandle: 'handle-1',
position: [0, 0, 0],
size: [100, 50, 18],
normal: [0, 0, 1],
panelType: 'panel',
},
],
holes: [
{
id: 'hole-1',
panelId: 'panel-1',
position: [10, 20, 0],
normal: [0, 0, 1],
diameter: 5,
depth: 10,
type: 'through',
inferredUsage: 'screw',
},
],
}));
const content = await zip.generateAsync({ type: 'arraybuffer' });
const file = new File([content], 'demo.zip', { type: 'application/zip' });
Object.defineProperty(file, 'size', { value: content.byteLength });
// Jest/jsdom 的 File 可能没有 arrayBuffer 方法,这里补一个最小实现
;(file as any).arrayBuffer = async () => content;
const result = await parseZipFile(file);
expect(result.success).toBe(true);
expect(result.cadExportData?.holes?.length).toBe(1);
expect(result.cadExportData?.holes?.[0].position).toEqual([10, 20, 0]);
expect(result.cadExportData?.holes?.[0].normal).toEqual([0, 0, 1]);
expect(result.cadExportData?.holes?.[0].panelId).toBe('panel-1');
expect(result.cadExportData?.holes?.[0].inferredUsage).toBe('screw');
});
it('应该拒绝 v2.1 及以下导出(开发阶段不做旧版本兼容)', async () => {
const { parseZipFile } = await import('@/lib/zip-parser');
const zip = new JSZip();
zip.file('model.glb', 'model content');
zip.file('hardware.json', JSON.stringify({
version: '2.1.0',
pluginVersion: '2.1.0',
exportedAt: '2026-02-05T00:00:00Z',
software: 'AutoCAD 2026',
sourceFile: 'demo.dwg',
modelFile: 'model.glb',
panelCount: 0,
units: 'mm',
}));
const content = await zip.generateAsync({ type: 'arraybuffer' });
const file = new File([content], 'old.zip', { type: 'application/zip' });
Object.defineProperty(file, 'size', { value: content.byteLength });
;(file as any).arrayBuffer = async () => content;
const result = await parseZipFile(file);
expect(result.success).toBe(false);
expect(result.error).toContain('v2.2+');
});
it('应该拒绝低于 v2.2.14 的插件版本(模型分组已知问题)', async () => {
const { parseZipFile } = await import('@/lib/zip-parser');
const zip = new JSZip();
zip.file('model.glb', 'model content');
zip.file('hardware.json', JSON.stringify({
version: '2.2.0',
pluginVersion: '2.2.0',
exportedAt: '2026-02-05T00:00:00Z',
software: 'AutoCAD 2026',
sourceFile: 'demo.dwg',
modelFile: 'model.glb',
panelCount: 1,
units: 'mm',
panels: [],
holes: [],
}));
const content = await zip.generateAsync({ type: 'arraybuffer' });
const file = new File([content], 'old-2-2.zip', { type: 'application/zip' });
Object.defineProperty(file, 'size', { value: content.byteLength });
;(file as any).arrayBuffer = async () => content;
const result = await parseZipFile(file);
expect(result.success).toBe(false);
expect(result.error).toContain('v2.2.14+');
});
});