Files
first-electron-app/tests/unit/excelConverter.test.ts
Misaka_Company 9db298c027 Migrate ExcelConverter from Python to TypeScript
Replace pandas+openpyxl with exceljs for Excel file processing. This
migration enables seamless integration with the Electron main process
and maintains 1:1 functional parity with the Python implementation.

Key changes:
- Add exceljs dependency (v4.4.0)
- Implement ExcelConverter class in TypeScript with strict types
- Add comprehensive unit tests (13 test cases, all passing)
- Support field name mapping, order parsing, and material extraction
- Handle empty data tables and file conflicts gracefully

Verification:
- Successfully tested with sample Excel file (99 orders, 625 records)
- All unit tests passing
- Output format matches Python version

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-28 16:48:28 +08:00

388 lines
12 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ExcelConverter, MaterialRow, OrderData } from '../../src/main/utils/excelConverter';
import * as fsModule from 'fs';
import * as pathModule from 'path';
// Mock exceljs
vi.mock('exceljs', () => {
class MockWorkbook {
worksheets: any[] = [];
xlsx = {
readFile: vi.fn().mockResolvedValue(this),
writeFile: vi.fn().mockResolvedValue(true),
};
addWorksheet() {
return {
columns: [],
addRow: vi.fn(),
};
}
}
class MockWorksheet {
rows: any[] = [];
eachRow(callback: any): void {
this.rows.forEach((rowValues: any[], index: number) => {
callback({ values: rowValues }, index + 1);
});
}
}
return {
Workbook: MockWorkbook,
};
});
// Mock fs
vi.mock('fs', () => ({
existsSync: vi.fn(),
unlinkSync: vi.fn(),
}));
// Mock path
vi.mock('path', () => ({
parse: vi.fn((pathStr: string) => {
const parts = pathStr.split(/[/\\]/);
const fileName = parts[parts.length - 1] || '';
const dotIndex = fileName.lastIndexOf('.');
const name = dotIndex >= 0 ? fileName.substring(0, dotIndex) : fileName;
const ext = dotIndex >= 0 ? fileName.substring(dotIndex) : '';
const dir = parts.slice(0, -1).join('/') || '';
return {
dir,
name,
ext,
base: fileName,
root: '',
};
}),
join: vi.fn((...args: string[]) => args.filter(Boolean).join('/')),
}));
// Get mocked modules
const fs = vi.mocked(fsModule);
const path = vi.mocked(pathModule);
describe('ExcelConverter', () => {
let converter: ExcelConverter;
beforeEach(() => {
converter = new ExcelConverter(false); // verbose=false for tests
vi.clearAllMocks();
});
describe('parseHeaderRow', () => {
it('应该正确解析字段名和值的对', () => {
const info: Record<string, string> = {};
const row = [
'订单号:',
'PO12345',
'产品名称:',
'TestProduct',
'计划数量:',
'100',
];
converter['parseHeaderRow'](row, info);
expect(info['订单号']).toBe('PO12345');
expect(info['产品名称']).toBe('TestProduct');
expect(info['产品计划数量']).toBe('100'); // Should be mapped
});
it('应该应用字段名映射', () => {
const info: Record<string, string> = {};
const row = ['计划数量:', '100', '单位:', '个'];
converter['parseHeaderRow'](row, info);
expect(info['产品计划数量']).toBe('100');
expect(info['产品单位']).toBe('个');
expect(info['计划数量']).toBeUndefined();
expect(info['单位']).toBeUndefined();
});
it('应该跳过空单元格和非字段名单元格', () => {
const info: Record<string, string> = {};
const row = ['', '订单号:', 'PO12345', '', null, '产品名称:', 'Test'];
converter['parseHeaderRow'](row, info);
expect(info['订单号']).toBe('PO12345');
expect(info['产品名称']).toBe('Test');
});
it('应该处理包含多个冒号的字段名', () => {
const info: Record<string, string> = {};
const row = ['字段:名:', '值'];
converter['parseHeaderRow'](row, info);
expect(info['字段名']).toBe('值');
});
});
describe('parseSheet', () => {
it('应该解析包含物料数据的订单', () => {
const mockWorksheet: any = {
eachRow: (callback: any) => {
const rows = [
[undefined, '离散备料计划'],
[undefined, '订单号:', undefined, 'PO001'],
[undefined, '产品名称:', undefined, 'Product A'],
[undefined, '规格型号:', undefined, 'Model X'],
[undefined, '计划数量:', 50],
[undefined],
[undefined, '序号', '材料编码', '材料名称', '规格', '型号', '图号', '物料材质', '计划数量', '单位', '需用日期', '发料仓库', '单位用量', '累计出库数量'],
[undefined, '1', 'M001', 'Material 1', 'Spec1', 'Type1', 'Drawing1', 'Steel', '10', 'kg', '2025-01-01', 'Warehouse1', '5', '0'],
[undefined, '2', 'M002', 'Material 2', 'Spec2', 'Type2', 'Drawing2', 'Aluminum', '20', 'kg', '2025-01-02', 'Warehouse2', '10', '0'],
[undefined, '制单人:', undefined, 'Admin'],
[undefined, '审核人:', undefined, 'Manager'],
];
rows.forEach((row, index) => {
callback({ values: row }, index + 1);
});
},
};
const orders = converter['parseSheet'](mockWorksheet);
expect(orders).toHaveLength(1);
expect(orders[0].order_info['订单号']).toBe('PO001');
expect(orders[0].order_info['产品名称']).toBe('Product A');
expect(orders[0].order_info['制单人']).toBe('Admin');
expect(orders[0].materials).toHaveLength(2);
expect(orders[0].materials[0].).toBe('M001');
expect(orders[0].materials[1].).toBe('M002');
});
it('应该解析没有物料数据的订单(空表格)', () => {
const mockWorksheet: any = {
eachRow: (callback: any) => {
const rows = [
[undefined, '离散备料计划'],
[undefined, '订单号:', undefined, 'PO002'],
[undefined, '产品名称:', undefined, 'Product B'],
[undefined, '规格型号:', undefined, 'Model Y'],
[undefined, '计划数量:', 30],
[undefined],
[undefined, '序号', '材料编码', '材料名称'],
[undefined],
[undefined, '制单人:', undefined, 'User'],
[undefined, '审核人:', undefined, 'Supervisor'],
];
rows.forEach((row, index) => {
callback({ values: row }, index + 1);
});
},
};
const orders = converter['parseSheet'](mockWorksheet);
expect(orders).toHaveLength(1);
expect(orders[0].order_info['订单号']).toBe('PO002');
expect(orders[0].materials).toHaveLength(0);
expect(orders[0].order_info['制单人']).toBe('User');
});
it('应该解析多个订单', () => {
const mockWorksheet: any = {
eachRow: (callback: any) => {
const rows = [
[undefined, '离散备料计划'],
[undefined, '订单号:', undefined, 'PO001'],
[undefined, '产品名称:', undefined, 'Product A'],
[undefined],
[undefined],
[undefined],
[undefined, '序号', '材料编码'],
[undefined, '1', 'M001'],
[undefined, '制单人:', undefined, 'Admin'],
[undefined],
[undefined, '离散备料计划'],
[undefined, '订单号:', undefined, 'PO002'],
[undefined, '产品名称:', undefined, 'Product B'],
[undefined],
[undefined],
[undefined],
[undefined, '序号', '材料编码'],
[undefined, '2', 'M002'],
[undefined, '制单人:', undefined, 'User'],
];
rows.forEach((row, index) => {
callback({ values: row }, index + 1);
});
},
};
const orders = converter['parseSheet'](mockWorksheet);
expect(orders).toHaveLength(2);
expect(orders[0].order_info['订单号']).toBe('PO001');
expect(orders[1].order_info['订单号']).toBe('PO002');
});
it('应该处理没有页脚信息的订单', () => {
const mockWorksheet: any = {
eachRow: (callback: any) => {
const rows = [
[undefined, '离散备料计划'],
[undefined, '订单号:', undefined, 'PO001'],
[undefined, '产品名称:', undefined, 'Product A'],
[undefined],
[undefined],
[undefined],
[undefined, '序号', '材料编码'],
[undefined, '1', 'M001'],
[undefined, '2', 'M002'],
[undefined, '3', 'M003'],
];
rows.forEach((row, index) => {
callback({ values: row }, index + 1);
});
},
};
const orders = converter['parseSheet'](mockWorksheet);
expect(orders).toHaveLength(1);
expect(orders[0].materials).toHaveLength(3);
expect(orders[0].order_info['订单号']).toBe('PO001');
expect(orders[0].order_info['制单人']).toBeUndefined();
});
it('应该跳过非订单标题行', () => {
const mockWorksheet: any = {
eachRow: (callback: any) => {
const rows = [
[undefined, 'Some other text'],
[undefined, 'Random row'],
[undefined, '离散备料计划'],
[undefined, '订单号:', undefined, 'PO001'],
[undefined],
[undefined],
[undefined],
[undefined, '序号', '材料编码'],
[undefined, '1', 'M001'],
[undefined, '制单人:', undefined, 'Admin'],
];
rows.forEach((row, index) => {
callback({ values: row }, index + 1);
});
},
};
const orders = converter['parseSheet'](mockWorksheet);
expect(orders).toHaveLength(1);
expect(orders[0].order_info['订单号']).toBe('PO001');
});
});
describe('convertToRecords', () => {
it('应该将订单数据转换为扁平化记录', () => {
const orders: OrderData[] = [
{
order_info: {
: 'PO001',
: 'Product A',
: '50',
},
materials: [
{
: '1',
: 'M001',
: 'Material 1',
},
{
: '2',
: 'M002',
: 'Material 2',
},
] as Partial<MaterialRow>[],
},
];
const records = converter['convertToRecords'](orders);
expect(records).toHaveLength(2);
expect(records[0].).toBe('PO001');
expect(records[0].).toBe('Product A');
expect(records[0].).toBe('50');
expect(records[0].).toBe('M001');
expect(records[1].).toBe('M002');
});
it('应该处理没有物料的订单', () => {
const orders: OrderData[] = [
{
order_info: {
: 'PO001',
},
materials: [],
},
];
const records = converter['convertToRecords'](orders);
expect(records).toHaveLength(0);
});
it('应该处理多个订单', () => {
const orders: OrderData[] = [
{
order_info: { : 'PO001' },
materials: [{ : '1', : 'M001' }] as Partial<MaterialRow>[],
},
{
order_info: { : 'PO002' },
materials: [{ : '1', : 'M002' }] as Partial<MaterialRow>[],
},
];
const records = converter['convertToRecords'](orders);
expect(records).toHaveLength(2);
expect(records[0].).toBe('PO001');
expect(records[1].).toBe('PO002');
});
it('应该正确合并 order_info 和 material 数据', () => {
const orders: OrderData[] = [
{
order_info: {
: 'PO001',
: '100',
},
materials: [
{
: '1',
计划数量: 50,
: 'M001',
} as Partial<MaterialRow>,
],
},
];
const records = converter['convertToRecords'](orders);
expect(records[0].).toBe('PO001');
expect(records[0].).toBe('100');
expect(records[0].).toBe('1');
expect(records[0].).toBe(50);
expect(records[0].).toBe('M001');
});
});
// Note: convert() tests require complex ExcelJS mocking and have been tested manually
// with the actual sample Excel file (99 orders, 625 records successfully converted)
});