import assert from 'node:assert/strict'; import test from 'node:test'; import { buildCsvContent, buildWorksheetXml, escapeXmlText, resolveExportRows, spreadsheetColumnName, } from '../src/utils/tableExport.js'; test('spreadsheetColumnName maps indexes to excel column letters', () => { assert.equal(spreadsheetColumnName(0), 'A'); assert.equal(spreadsheetColumnName(25), 'Z'); assert.equal(spreadsheetColumnName(26), 'AA'); assert.equal(spreadsheetColumnName(51), 'AZ'); assert.equal(spreadsheetColumnName(52), 'BA'); assert.equal(spreadsheetColumnName(701), 'ZZ'); }); test('escapeXmlText escapes xml special characters', () => { assert.equal(escapeXmlText('a&"c"\''), 'a<b>&"c"''); }); test('resolveExportRows applies exportValue and falls back to dataIndex', () => { const columns = [ { title: '名称', dataIndex: 'name' }, { title: '达标', dataIndex: 'qualified', exportValue: (value) => (value ? '达标' : '未达标') }, ]; const rows = [{ name: '商品<1>', qualified: true }, { name: '商品,2', qualified: false }]; assert.deepEqual(resolveExportRows(columns, rows), [ ['商品<1>', '达标'], ['商品,2', '未达标'], ]); }); test('exportValue receives the cell value like antd render, not the row object', () => { // 回归:导出 Invalid Date / NaN% 的根因是 exportValue 曾被传入整行对象 const formatDate = (value) => (value ? `D:${value.slice(0, 10)}` : '--'); const percent = (value) => (value === null || value === undefined ? '--' : `${(Number(value) * 100).toFixed(2)}%`); const columns = [ { title: '上架时间', dataIndex: 'firstCreatedAt', exportValue: formatDate }, { title: '动销率', dataIndex: 'rate', exportValue: percent }, { title: '是否达标', dataIndex: 'qualified', exportValue: (value) => (value ? '达标' : '未达标') }, ]; const rows = [ { firstCreatedAt: '2026-07-28T00:00:00.000Z', rate: 0.9667, qualified: true }, { firstCreatedAt: null, rate: null, qualified: false }, ]; assert.deepEqual(resolveExportRows(columns, rows), [ ['D:2026-07-28', '96.67%', '达标'], ['--', '--', '未达标'], ]); }); test('buildCsvContent quotes cells containing delimiters and keeps CRLF rows', () => { const columns = [{ title: '商品,名称', dataIndex: 'name' }, { title: '数量', dataIndex: 'count' }]; const rows = [{ name: '含"引号"和\n换行', count: 3 }]; const content = buildCsvContent(columns, rows); const lines = content.split('\r\n'); assert.equal(lines[0], '"商品,名称",数量'); assert.equal(lines[1], '"含""引号""和\n换行",3'); }); test('buildWorksheetXml emits header row, inline strings and numeric cells', () => { const columns = [{ title: '名称<测试>', dataIndex: 'name' }, { title: '奖金', dataIndex: 'bonus' }]; const rows = [{ name: '商品&1', bonus: 400 }]; const xml = buildWorksheetXml(columns, rows); assert.match(xml, //); assert.match(xml, /t="inlineStr">名称<测试><\/t><\/is><\/c>/); assert.match(xml, /商品&1/); assert.match(xml, /400<\/v><\/c>/); assert.match(xml, /