导入表头修复
This commit is contained in:
1 parent
8a0f02c9be
commit
a3505f3921
28 files changed
+281
-45
No files matched your search
@@ -136,6 +136,102 @@ test('XLSX is deterministically normalized to the canonical 13-column TSV', asyn
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
test('an absent phone header produces blank phones without guessing unheaded values', async () => {
|
||||
const baseline = await normalizeSynthetic(await syntheticWorkbook());
|
||||
const expected = baseline.canonicalTsv.split('\n').map((line, index) => {
|
||||
if (index === 0) return line;
|
||||
const fields = line.split('\t');
|
||||
fields[11] = '';
|
||||
return fields.join('\t');
|
||||
}).join('\n');
|
||||
for (const removeColumn of [false, true]) {
|
||||
const result = await normalizeSynthetic(await syntheticWorkbook(workbook => {
|
||||
const sheet = workbook.getWorksheet('Sheet1')!;
|
||||
if (removeColumn) sheet.spliceColumns(13, 1);
|
||||
else sheet.getCell('M2').value = null; // The original number below remains unheaded.
|
||||
}));
|
||||
assert.equal(result.canonicalTsv, expected);
|
||||
assert.equal(result.rowCount, 2);
|
||||
const parsed = await parseProgramInput({
|
||||
rawText: `导入散拼子单名单\n预订客户:测试客户\n出发日期:2026-09-18\n名单内容:\n${result.canonicalTsv}`,
|
||||
receivedAt: new Date('2026-09-16T00:00:00Z')
|
||||
});
|
||||
assert.equal(parsed.status, 'agent_parse_passed');
|
||||
}
|
||||
});
|
||||
|
||||
test('phone-free rosters support reordered alias headers, merged remarks and XLS conversion', async () => {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const sheet = workbook.addWorksheet('无电话模板');
|
||||
sheet.mergeCells('B1:K1');
|
||||
sheet.getCell('B1').value = '脱敏团队信息';
|
||||
sheet.addRow(['序号', '姓名', '英文姓名', '护照号码', '性别', '出生日期', '年龄', '出生地点', '签发地', '签发日期', '有效期至', '备注']);
|
||||
for (let sequence = 1; sequence <= 8; sequence++) {
|
||||
sheet.addRow([sequence, '测试游客', 'TEST', `P0000000${sequence}`, '女', '1990-01-02', 36, '测试地', '测试地', '2025-01-02', '2035-01-01', '普通备注']);
|
||||
}
|
||||
sheet.mergeCells('L3:L10');
|
||||
sheet.getCell('M10').value = 13800000000; // Extra unheaded content must not become a phone.
|
||||
const xlsx = Buffer.from(await workbook.xlsx.writeBuffer());
|
||||
const expected = await normalizeSynthetic(xlsx);
|
||||
assert.equal(expected.rowCount, 8);
|
||||
for (const row of expected.canonicalTsv.split('\n').slice(1)) {
|
||||
assert.equal(row.split('\t').length, 13);
|
||||
assert.deepEqual(row.split('\t').slice(11), ['', '普通备注']);
|
||||
}
|
||||
assert.doesNotMatch(expected.canonicalTsv, /13800000000/);
|
||||
const legacy = await normalizePassengerRosterWorkbook({
|
||||
content: Buffer.from([0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1]),
|
||||
fileName: 'synthetic.xls', contentType: 'application/vnd.ms-excel'
|
||||
}, { convertLegacyXls: async () => xlsx });
|
||||
assert.equal(legacy.canonicalTsv, expected.canonicalTsv);
|
||||
});
|
||||
|
||||
test('phone is the only optional semantic and duplicate phone headers stay blocked', async () => {
|
||||
for (const column of [1, 2, 3, 4, 6, 8, 9, 10, 11, 12, 14]) {
|
||||
const content = await syntheticWorkbook(workbook => {
|
||||
const sheet = workbook.getWorksheet('Sheet1')!;
|
||||
sheet.getCell('M2').value = null;
|
||||
sheet.getCell(2, column).value = null;
|
||||
});
|
||||
await expectRosterError(() => normalizeSynthetic(content), 'roster_workbook_header_not_found');
|
||||
}
|
||||
await expectRosterError(() => syntheticWorkbook(workbook => {
|
||||
workbook.getWorksheet('Sheet1')!.getCell('O2').value = '电话';
|
||||
}).then(normalizeSynthetic), 'roster_workbook_header_not_found');
|
||||
});
|
||||
|
||||
test('present phone columns retain formula, value, hidden-data and row checks', async () => {
|
||||
for (const [value, code] of [
|
||||
[{ formula: '1+1', result: 2 }, 'roster_workbook_formula_not_allowed'],
|
||||
[{ error: '#VALUE!' }, 'roster_workbook_invalid_cell'],
|
||||
[1.5, 'roster_workbook_invalid_identifier']
|
||||
] as const) {
|
||||
await expectRosterError(() => syntheticWorkbook(workbook => {
|
||||
workbook.getWorksheet('Sheet1')!.getCell('M3').value = value;
|
||||
}).then(normalizeSynthetic), code, { row: 3, column: 13 });
|
||||
}
|
||||
await expectRosterError(() => syntheticWorkbook(workbook => {
|
||||
workbook.getWorksheet('Sheet1')!.getColumn(13).hidden = true;
|
||||
}).then(normalizeSynthetic), 'roster_workbook_hidden_data');
|
||||
await expectRosterError(() => syntheticWorkbook(workbook => {
|
||||
workbook.getWorksheet('Sheet1')!.getCell('M5').value = '13800000000';
|
||||
}).then(normalizeSynthetic), 'roster_workbook_invalid_sequence', { row: 5, column: 1 });
|
||||
});
|
||||
|
||||
test('a marked leader still requires a phone when the source phone column is absent', async () => {
|
||||
const result = await normalizeSynthetic(await syntheticWorkbook(workbook => {
|
||||
const sheet = workbook.getWorksheet('Sheet1')!;
|
||||
sheet.getCell('M2').value = null;
|
||||
sheet.getCell('N3').value = '领队';
|
||||
}));
|
||||
const parsed = await parseProgramInput({
|
||||
rawText: `导入散拼子单名单\n预订客户:测试客户\n出发日期:2026-09-18\n名单内容:\n${result.canonicalTsv}`,
|
||||
receivedAt: new Date('2026-09-16T00:00:00Z')
|
||||
});
|
||||
assert.notEqual(parsed.status, 'agent_parse_passed');
|
||||
assert.match(JSON.stringify(parsed), /姓名和电话都必须填写/);
|
||||
});
|
||||
|
||||
test('a merged title error is skipped, while header and passenger errors stay blocked', async () => {
|
||||
const baseline = await normalizeSynthetic(await syntheticWorkbook());
|
||||
const titleError = await normalizeSynthetic(await syntheticWorkbook(workbook => {
|
||||
|
||||
Reference in new issue
Block a user