/** * 上传件读文本 —— **这段错了不报错,只是活动拿到一份读不懂的料**。 * * 测法不用录制的样本文件:测里现造一个**真的 xlsx**(ZIP + 那几份 XML,按规范逐字节拼), * 再让被测代码去读。造的那一侧与读的那一侧互不共享代码,读得回来才算真的读得懂。 */ import { describe, expect, it } from 'vitest'; import { deflateRawSync } from 'node:zlib'; import { extractText } from '../upload-extract.ts'; /* ==================== 现造一个 xlsx ==================== */ const enc = (s: string): Uint8Array => new TextEncoder().encode(s); /** 按 ZIP 规范拼一个包:本地头 + 数据 + 中央目录 + EOCD */ function makeZip(entries: ReadonlyArray<{ name: string; body: Uint8Array; store?: boolean }>): Uint8Array { const locals: Uint8Array[] = []; const centrals: Uint8Array[] = []; let offset = 0; for (const e of entries) { const name = enc(e.name); const data = e.store === true ? e.body : new Uint8Array(deflateRawSync(e.body)); const method = e.store === true ? 0 : 8; const local = new Uint8Array(30 + name.length + data.length); const lv = new DataView(local.buffer); lv.setUint32(0, 0x04034b50, true); lv.setUint16(8, method, true); lv.setUint32(18, data.length, true); lv.setUint32(22, e.body.length, true); lv.setUint16(26, name.length, true); local.set(name, 30); local.set(data, 30 + name.length); locals.push(local); const central = new Uint8Array(46 + name.length); const cv = new DataView(central.buffer); cv.setUint32(0, 0x02014b50, true); cv.setUint16(10, method, true); cv.setUint32(20, data.length, true); cv.setUint32(24, e.body.length, true); cv.setUint16(28, name.length, true); cv.setUint32(42, offset, true); central.set(name, 46); centrals.push(central); offset += local.length; } const centralSize = centrals.reduce((n, c) => n + c.length, 0); const eocd = new Uint8Array(22); const ev = new DataView(eocd.buffer); ev.setUint32(0, 0x06054b50, true); ev.setUint16(8, entries.length, true); ev.setUint16(10, entries.length, true); ev.setUint32(12, centralSize, true); ev.setUint32(16, offset, true); const total = offset + centralSize + eocd.length; const out = new Uint8Array(total); let at = 0; for (const b of [...locals, ...centrals, eocd]) { out.set(b, at); at += b.length; } return out; } /** 一份两列三行的 BOM:表头 + 两条料 —— 字符串走共享串表,数字直接落 `` */ function bomXlsx(): Uint8Array { const strings = ['型号', '年用量', 'LM358DR', 'NE555P']; const sharedXml = `` + strings.map((s) => `${s}`).join('') + ``; const sheetXml = `` + `01` + `212000` + `35000` + `` + ``; return makeZip([ { name: '[Content_Types].xml', body: enc('') }, { name: 'xl/sharedStrings.xml', body: enc(sharedXml) }, { name: 'xl/worksheets/sheet1.xml', body: enc(sheetXml) }, ]); } /* ==================== 断言 ==================== */ describe('xlsx —— 一份真的表读得成行列', () => { const got = extractText('BOM.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', bomXlsx()); it('读得出内容,而不是回一句「读不出」', () => { expect(got.text).toBeDefined(); }); it('共享串表查得对 —— 字符串单元格不是数字下标', () => { expect(got.text).toContain('LM358DR'); expect(got.text).toContain('型号'); }); it('数字单元格取原值,不查串表(查了会变成别的字)', () => { expect(got.text).toContain('12000'); }); it('一行一行、制表符分列 —— 行列对齐了模型才拆得动', () => { expect(got.text?.split('\n')).toEqual(['型号\t年用量', 'LM358DR\t12000', 'NE555P\t5000']); }); it('整行皆空的不留 —— 表尾那些格式化过的空行不该占一行', () => { expect(got.text?.split('\n')).toHaveLength(3); }); it('读数如实报行数', () => { expect(got.note).toContain('3 行'); }); }); describe('不压缩的条目也读得了 —— store(0) 与 deflate(8) 都是 xlsx 的出厂法', () => { it('store 法存的表照样读得出', () => { const zip = makeZip([ { name: 'xl/worksheets/sheet1.xml', body: enc('直存'), store: true, }, ]); expect(extractText('a.xlsx', '', zip).text).toBe('直存'); }); }); describe('读不出来就说读不出来 —— 绝不编一段内容出来', () => { it('不是 ZIP 的 .xlsx:如实说打不开', () => { const got = extractText('坏的.xlsx', '', enc('这不是一个 zip')); expect(got.text).toBeUndefined(); expect(got.note).toContain('打不开'); }); it('二进制类型(pdf):说清只有文件名进得了活动', () => { const got = extractText('规格书.pdf', 'application/pdf', new Uint8Array([1, 2, 3])); expect(got.text).toBeUndefined(); expect(got.note).toContain('读不出文本'); }); it('空表:不是「读出 0 行」,是明说这份表是空的', () => { const zip = makeZip([{ name: 'xl/worksheets/sheet1.xml', body: enc('') }]); expect(extractText('空.xlsx', '', zip).note).toContain('空的'); }); }); describe('纯文本族 —— 按 UTF-8 直接解', () => { it('csv 原样读出(中文不乱码)', () => { const got = extractText('bom.csv', 'text/csv', enc('型号,年用量\nLM358DR,12000')); expect(got.text).toBe('型号,年用量\nLM358DR,12000'); }); it('超长的截断,并在读数里说明截了', () => { const got = extractText('big.txt', 'text/plain', enc('x'.repeat(30_000))); expect(got.text).toHaveLength(20_000); expect(got.note).toContain('截断'); }); }); describe('XML 实体还原 —— `&lt;` 之流不能原样端给模型', () => { it('转义字符还原成本来的字', () => { const zip = makeZip([ // 表里真实存的形态:`A&B <100>` 在 XML 里写作 `A&B <100>` { name: 'xl/sharedStrings.xml', body: enc('A&B <100>') }, { name: 'xl/worksheets/sheet1.xml', body: enc('0'), }, ]); expect(extractText('e.xlsx', '', zip).text).toBe('A&B <100>'); }); });