import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; import * as XLSX from 'xlsx'; import { buildSankeyData, parseCsvText, parseXlsxBuffer } from '../src/core'; describe('core parser & sankey', () => { it('可以解析 CSV 并生成列名与数据行', () => { const table = parseCsvText('A,B,C\n1,2,3\n4,5,6'); expect(table.headers).toEqual(['A', 'B', 'C']); expect(table.rows).toEqual([ ['1', '2', '3'], ['4', '5', '6'] ]); }); it('支持合并单元格语义向下补全并聚合', () => { const table = { headers: ['站点', '值', '目标', '模型'], rows: [ ['宁波北欧10', '2582', '嘉兴四级算力池', '小模型'], ['宁波北欧12', '2610', '', ''], ['宁波鄞中15', '507', '嘉兴四级算力池', '小模型'] ] }; const result = buildSankeyData(table, { sourceDataColumn: 1, sourceDescriptionColumns: [0, 1], targetDescriptionColumns: [2, 3], delimiter: '-' }); expect(result.meta.droppedRows).toBe(0); expect(result.links).toHaveLength(3); expect(result.links[1].target).toBe('嘉兴四级算力池-小模型'); }); it('可以解析 data/example0.xlsx', () => { const filePath = resolve(process.cwd(), 'data/example0.xlsx'); const fileBuffer = readFileSync(filePath); const table = parseXlsxBuffer(fileBuffer.buffer.slice(fileBuffer.byteOffset, fileBuffer.byteOffset + fileBuffer.byteLength)); expect(table.headers.length).toBeGreaterThan(1); expect(table.rows.length).toBeGreaterThan(0); }); it('xlsx !ref 范围虚高时,不应错误膨胀行列数量', () => { const sheet = XLSX.utils.aoa_to_sheet([ ['source', 'target', 'value'], ['A', 'B', 1] ]); sheet['!ref'] = 'A1:H20'; const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, sheet, 'S1'); const buffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'buffer' }); const table = parseXlsxBuffer( buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) ); expect(table.headers).toEqual(['source', 'target', 'value']); expect(table.rows).toEqual([['A', 'B', '1']]); }); it('xlsx 中 0 值应被正确保留', () => { const sheet = XLSX.utils.aoa_to_sheet([ ['source', 'target', 'value'], ['A', 'B', 0] ]); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, sheet, 'S1'); const buffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'buffer' }); const table = parseXlsxBuffer( buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) ); expect(table.rows).toEqual([['A', 'B', '0']]); }); it('xlsx 中常见中文乱码应尝试自动恢复', () => { const sheet = XLSX.utils.aoa_to_sheet([['人數'], ['张三']]); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, sheet, 'S1'); const buffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'buffer' }); const table = parseXlsxBuffer( buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) ); expect(table.headers).toEqual(['人數']); expect(table.rows).toEqual([['张三']]); }); it('源数据非法时,告警包含单元格内容和位置', () => { const table = { headers: ['source', 'value', 'target'], rows: [['A', 'abc', 'T1']] }; const result = buildSankeyData(table, { sourceDataColumn: 1, sourceDescriptionColumns: [0], targetDescriptionColumns: [2], delimiter: '-' }); expect(result.meta.droppedRows).toBe(1); expect(result.meta.warnings[0]).toContain('第 2 行'); expect(result.meta.warnings[0]).toContain('第 2 列'); expect(result.meta.warnings[0]).toContain('value'); expect(result.meta.warnings[0]).toContain('abc'); }); it('源描述为空时,告警包含描述字段内容和位置', () => { const table = { headers: ['source', 'value', 'target'], rows: [['', '12', 'T1']] }; const result = buildSankeyData(table, { sourceDataColumn: 1, sourceDescriptionColumns: [0], targetDescriptionColumns: [2], delimiter: '-' }); expect(result.meta.droppedRows).toBe(1); expect(result.meta.warnings[0]).toContain('源描述为空'); expect(result.meta.warnings[0]).toContain('第 1 列'); expect(result.meta.warnings[0]).toContain('source'); expect(result.meta.warnings[0]).toContain('(空)'); }); it('目标描述为空时,告警包含描述字段内容和位置', () => { const table = { headers: ['source', 'value', 'target'], rows: [['S1', '12', '']] }; const result = buildSankeyData(table, { sourceDataColumn: 1, sourceDescriptionColumns: [0], targetDescriptionColumns: [2], delimiter: '-' }); expect(result.meta.droppedRows).toBe(1); expect(result.meta.warnings[0]).toContain('目标描述为空'); expect(result.meta.warnings[0]).toContain('第 3 列'); expect(result.meta.warnings[0]).toContain('target'); expect(result.meta.warnings[0]).toContain('(空)'); expect(result.meta.warnings[0]).toContain('无可继承的上方值'); }); });