Files
X-Financial/web/tests/attachment-association-confirmation.test.mjs
caoxiaozhu 88ff04bef8 feat: 新增归档中心页面并完善知识库与报销查询能力
新增前端归档中心视图及相关工具函数,扩充知识库文档分类和
提取器支持多种格式,增强编排器报销查询的多维度检索,优
化本体规则和用户代理审核消息,前端完善报销创建和审批详
情交互细节,补充单元测试覆盖。
2026-05-22 16:00:19 +08:00

202 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
import {
ATTACHMENT_ASSOCIATION_CONFIRM_HREF,
buildAttachmentAssociationConfirmationMessage,
buildOcrFilePreviews,
buildReviewFilePreviewsFromReviewPayload
} from '../src/views/scripts/travelReimbursementAttachmentModel.js'
import {
buildDraftAssociationQueryPayload,
buildExpenseQueryHint,
EXPENSE_CENTER_HREF,
normalizeExpenseQueryPayload
} from '../src/views/scripts/travelReimbursementExpenseQueryModel.js'
import { renderMarkdown } from '../src/utils/markdown.js'
test('attachment association prompt prints recognized receipt details before confirmation link', () => {
const message = buildAttachmentAssociationConfirmationMessage({
claimNo: 'EXP-202605-001',
fileNames: ['train-ticket.pdf'],
ocrDocuments: [
{
filename: 'train-ticket.pdf',
document_type: 'train_ticket',
scene_label: '差旅票据',
summary: '铁路电子客票 武汉-上海 票价 354 元',
document_fields: [
{ key: 'route', label: '行程', value: '武汉-上海' },
{ key: 'amount', label: '票价', value: '354.00' },
{ key: 'date', label: '乘车日期', value: '2026-02-20' }
]
}
]
})
assert.match(message, /已识别附件信息:/)
assert.match(message, /> \*\*附件 1train-ticket\.pdf\*\*/)
assert.match(message, /附件类型:差旅票据/)
assert.match(message, /行程:武汉-上海/)
assert.match(message, /票价354.00/)
assert.match(message, /草稿单号EXP-202605-001/)
assert.match(message, new RegExp(`\\n\\n\\n如果 \\*\\*\\[确认\\]\\(${ATTACHMENT_ASSOCIATION_CONFIRM_HREF}\\)\\*\\* 该信息`))
const rendered = renderMarkdown(message)
assert.match(rendered, /<blockquote class="markdown-attachment-card">/)
const questionIndex = rendered.indexOf('请问是否确定将票据信息归集到单据')
const attachmentCardCloseIndex = rendered.indexOf('</blockquote>')
assert.ok(attachmentCardCloseIndex > -1 && questionIndex > attachmentCardCloseIndex)
const attachmentCardHtml = rendered.slice(
rendered.indexOf('<blockquote class="markdown-attachment-card">'),
attachmentCardCloseIndex
)
assert.doesNotMatch(attachmentCardHtml, /请问是否确定将票据信息归集到单据/)
assert.match(rendered, /<p class="markdown-action-paragraph">/)
assert.match(rendered, /<strong><a href="#confirm-attachment-association" class="markdown-action-link markdown-action-link-confirm">确认<\/a><\/strong>/)
})
test('multiple recognized attachments render as separated attachment cards', () => {
const message = buildAttachmentAssociationConfirmationMessage({
claimNo: 'EXP-202605-001',
ocrDocuments: [
{
filename: '2月20 武汉-上海.pdf',
document_type: 'train_ticket',
document_type_label: '火车/高铁票',
document_fields: [
{ key: 'amount', label: '金额', value: '354元' },
{ key: 'route', label: '行程', value: '武汉-上海' }
]
},
{
filename: '2月23 上海-武汉.pdf',
document_type: 'train_ticket',
document_type_label: '火车/高铁票',
document_fields: [
{ key: 'amount', label: '金额', value: '354元' },
{ key: 'route', label: '行程', value: '上海-武汉' }
]
}
]
})
const rendered = renderMarkdown(message)
assert.equal((rendered.match(/class="markdown-attachment-card"/g) || []).length, 2)
assert.match(rendered, /<strong>附件 12月20 武汉-上海\.pdf<\/strong>/)
assert.match(rendered, /<strong>附件 22月23 上海-武汉\.pdf<\/strong>/)
assert.match(rendered, /本次待归集附件2 份/)
})
test('attachment upload association uses conversation selection instead of legacy modal', () => {
const viewSource = readFileSync(
fileURLToPath(new URL('../src/views/TravelReimbursementCreateView.vue', import.meta.url)),
'utf8'
)
const submitComposerSource = readFileSync(
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementSubmitComposer.js', import.meta.url)),
'utf8'
)
assert.doesNotMatch(viewSource, /检测到你已有单据事件|uploadDecisionDialogOpen|continueExistingUpload|createNewUploadDocument/)
assert.doesNotMatch(submitComposerSource, /uploadDecisionDialogOpen|hasExistingDocumentEvent|skipUploadDecisionPrompt/)
assert.doesNotMatch(submitComposerSource, /查询可关联草稿失败,已继续按新单据识别/)
assert.match(
submitComposerSource,
/const claims = await fetchExpenseClaims\(\)[\s\S]*const queryPayload = buildDraftAssociationQueryPayload\(claims\)[\s\S]*meta: \['等待选择关联单据'\][\s\S]*queryPayload/
)
assert.match(submitComposerSource, /meta: \['单据查询失败'\][\s\S]*return null/)
assert.match(
submitComposerSource,
/files\.length[\s\S]*!resolvedUploadDisposition[\s\S]*!options\.skipDraftAssociationPrompt[\s\S]*!reviewAction/
)
})
test('OCR preview builders keep hotel receipt image previews when preview kind is omitted', () => {
const dataUrl = 'data:image/png;base64,abc123'
const ocrPreviews = buildOcrFilePreviews({
documents: [
{
filename: 'hotel.png',
preview_data_url: dataUrl
}
]
})
const reviewPreviews = buildReviewFilePreviewsFromReviewPayload({
document_cards: [
{
filename: 'hotel.png',
preview_url: dataUrl
}
]
})
assert.deepEqual(ocrPreviews, [{ filename: 'hotel.png', kind: 'image', url: dataUrl }])
assert.deepEqual(reviewPreviews, [{ filename: 'hotel.png', kind: 'image', url: dataUrl }])
})
test('draft association query keeps a single candidate selectable in the conversation', () => {
const payload = buildDraftAssociationQueryPayload([
{
id: 'claim-1',
claim_no: 'EXP-202605-001',
status: 'draft',
expense_type: 'travel',
reason: '上海出差',
amount: 1280
}
])
assert.equal(payload.selectionMode, 'draft_association')
assert.equal(payload.title, '选择关联草稿')
assert.equal(payload.records.length, 1)
assert.equal(payload.records[0].claimId, 'claim-1')
})
test('expense query payload keeps structured risk items for claim-level risk drilldown', () => {
const payload = normalizeExpenseQueryPayload({
result_type: 'expense_claim_list',
records: [
{
claim_id: 'claim-risk',
claim_no: 'EXP-202605-009',
amount: 880,
risk_flags: [
{
key: 'hotel-limit',
level: 'high',
level_label: '高风险',
title: '酒店超标',
summary: '住宿金额超过城市标准',
detail: '上海 P5 住宿标准为 600 元,本次 880 元。'
}
]
}
]
})
assert.equal(payload.records[0].riskItems.length, 1)
assert.equal(payload.records[0].riskItems[0].levelLabel, '高风险')
assert.equal(payload.records[0].riskItems[0].summary, '住宿金额超过城市标准')
})
test('expense query hint guides users to the reimbursement center after the top five results', () => {
const payload = normalizeExpenseQueryPayload({
result_type: 'expense_claim_list',
title: '最近 5 条你的归档报销单',
scope_label: '你的归档报销单',
record_count: 8,
preview_count: 5,
preview_limit: 5,
records: [
{ claim_id: 'claim-1', claim_no: 'EXP-1', amount: 100 }
]
})
const hint = buildExpenseQueryHint(payload)
assert.match(hint, /最近的 5 条记录/)
assert.match(hint, new RegExp(`\\[\\*\\*这里\\*\\*\\]\\(${EXPENSE_CENTER_HREF}\\)`))
})