2026-06-20 10:17:37 +08:00
|
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
|
import test from 'node:test'
|
|
|
|
|
|
|
|
|
|
|
|
import { renderAiConversationHtml } from '../src/utils/aiConversationHtmlRenderer.js'
|
|
|
|
|
|
|
|
|
|
|
|
test('AI conversation renderer turns business copy into spacious semantic HTML', () => {
|
|
|
|
|
|
const rendered = renderAiConversationHtml([
|
|
|
|
|
|
'### 出差申请办理确认',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'**我已在您的输入中提取到关键信息**,如下表所示:',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'> **前置查询结果**:我已查询您名下可关联的差旅申请单,当前未查到可关联单据。',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'> **需要您确认**:发起新的出差申请属于业务操作,需要您手动确认后我再继续办理。',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'点击下方 **确认发起出差申请** 后,我会继续完成:',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'- **单据重叠核查**:检查同一时间段是否已有申请单,避免重复申请。',
|
|
|
|
|
|
'- **预算与审批预审**:查看部门预算影响,判断是否可能增加预算管理者审核。'
|
|
|
|
|
|
].join('\n'))
|
|
|
|
|
|
|
|
|
|
|
|
assert.match(rendered, /<div class="ai-html-flow">/)
|
|
|
|
|
|
assert.match(rendered, /<h3 class="ai-html-title">出差申请办理确认<\/h3>/)
|
|
|
|
|
|
assert.match(rendered, /<section class="ai-html-focus-grid" aria-label="重点信息">/)
|
|
|
|
|
|
assert.match(rendered, /<article class="ai-html-focus-card">[\s\S]*前置查询结果[\s\S]*当前未查到可关联单据/)
|
|
|
|
|
|
assert.match(rendered, /<article class="ai-html-focus-card">[\s\S]*需要您确认[\s\S]*需要您手动确认后我再继续办理/)
|
|
|
|
|
|
assert.match(rendered, /<ul class="ai-html-steps">[\s\S]*单据重叠核查[\s\S]*预算与审批预审/)
|
|
|
|
|
|
assert.doesNotMatch(rendered, /<blockquote>/)
|
|
|
|
|
|
assert.doesNotMatch(rendered, /<ul>\s*<li><strong>/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('AI conversation renderer supports tables and escapes unsafe HTML', () => {
|
|
|
|
|
|
const rendered = renderAiConversationHtml([
|
|
|
|
|
|
'### 查询结果',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'| 字段 | 内容 |',
|
|
|
|
|
|
'| --- | --- |',
|
|
|
|
|
|
'| 事由 | 辅助 <script>alert(1)</script> 部署 |',
|
|
|
|
|
|
'| 地点 | 上海 |'
|
|
|
|
|
|
].join('\n'))
|
|
|
|
|
|
|
|
|
|
|
|
assert.match(rendered, /<div class="ai-html-table-wrap">/)
|
|
|
|
|
|
assert.match(rendered, /<th>字段<\/th>/)
|
|
|
|
|
|
assert.match(rendered, /<script>alert\(1\)<\/script>/)
|
|
|
|
|
|
assert.doesNotMatch(rendered, /<script>/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('AI conversation renderer renders application detail action links as buttons', () => {
|
|
|
|
|
|
const rendered = renderAiConversationHtml([
|
2026-06-21 22:49:53 +08:00
|
|
|
|
'| 单据类型 | 单据编号 | 单据状态 | 当前节点 | 日期 | 地点 | 事由 | 操作 |',
|
|
|
|
|
|
'| --- | --- | --- | --- | --- | --- | --- | --- |',
|
|
|
|
|
|
'| 出差申请 | AP-OVERLAP | submitted | 直属领导审批 | 2026-02-20 至 2026-02-23 | 上海 | 辅助国网仿生产服务器部署 | [查看](#ai-open-application-detail:AP-OVERLAP) |'
|
2026-06-20 10:17:37 +08:00
|
|
|
|
].join('\n'))
|
|
|
|
|
|
|
2026-06-21 22:49:53 +08:00
|
|
|
|
assert.match(rendered, /<section class="ai-document-card-list" role="list" aria-label="单据结果">/)
|
|
|
|
|
|
assert.match(rendered, /<article class="ai-document-card is-pending" role="listitem" aria-label="单据详情">/)
|
|
|
|
|
|
assert.match(rendered, /<strong class="ai-document-card__reason">出差申请<\/strong>/)
|
|
|
|
|
|
assert.match(rendered, /<span class="ai-document-card__status">审批中<\/span>/)
|
|
|
|
|
|
assert.match(rendered, /<div class="ai-document-card__summary">/)
|
|
|
|
|
|
assert.match(rendered, /<span class="ai-document-card__label">日期<\/span>/)
|
|
|
|
|
|
assert.match(rendered, /2026-02-20 至 2026-02-23/)
|
|
|
|
|
|
assert.match(rendered, /<span class="ai-document-card__label">当前节点<\/span>/)
|
|
|
|
|
|
assert.match(rendered, /直属领导审批/)
|
|
|
|
|
|
assert.match(rendered, /<span class="ai-document-card__label">地点<\/span>/)
|
|
|
|
|
|
assert.match(rendered, /上海/)
|
|
|
|
|
|
assert.match(rendered, /<span class="ai-document-card__label">事由<\/span>/)
|
|
|
|
|
|
assert.match(rendered, /辅助国网仿生产服务器部署/)
|
|
|
|
|
|
assert.match(rendered, /<strong class="ai-document-card__value ai-document-card__number">AP-OVERLAP<\/strong>/)
|
|
|
|
|
|
assert.match(rendered, /class="ai-html-action-link ai-document-card__action ai-html-action-link-application"/)
|
2026-06-20 10:17:37 +08:00
|
|
|
|
assert.match(rendered, /data-ai-action="open-application-detail"/)
|
|
|
|
|
|
assert.match(rendered, /href="#ai-open-application-detail:AP-OVERLAP"/)
|
2026-06-20 21:44:16 +08:00
|
|
|
|
assert.doesNotMatch(rendered, /<table>/)
|
2026-06-21 22:49:53 +08:00
|
|
|
|
assert.doesNotMatch(rendered, /ai-html-record-item/)
|
2026-06-20 10:17:37 +08:00
|
|
|
|
assert.doesNotMatch(rendered, /target="_blank"[\s\S]{0,120}#ai-open-application-detail/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-20 21:44:16 +08:00
|
|
|
|
test('AI conversation renderer renders deleted application detail actions as disabled buttons', () => {
|
|
|
|
|
|
const rendered = renderAiConversationHtml([
|
|
|
|
|
|
'| 单据类型 | 单据编号 | 单据状态 | 当前节点 | 操作 |',
|
|
|
|
|
|
'| --- | --- | --- | --- | --- |',
|
|
|
|
|
|
'| 出差申请 | AP-20260620-DRAFT | 已删除 | 已删除 | [草稿已删除](#ai-deleted-application-detail:claim-draft-1) |'
|
|
|
|
|
|
].join('\n'))
|
|
|
|
|
|
|
2026-06-21 22:49:53 +08:00
|
|
|
|
assert.match(rendered, /class="ai-html-action-link ai-document-card__action ai-html-action-link-application is-disabled"/)
|
2026-06-20 21:44:16 +08:00
|
|
|
|
assert.match(rendered, /aria-disabled="true"/)
|
|
|
|
|
|
assert.match(rendered, /data-ai-action="deleted-application-detail"/)
|
|
|
|
|
|
assert.doesNotMatch(rendered, /href="#ai-deleted-application-detail/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('AI conversation renderer turns application conflict tables into record lists', () => {
|
|
|
|
|
|
const rendered = renderAiConversationHtml([
|
|
|
|
|
|
'| 单据编号 | 申请时间 | 状态 | 事由 | 操作 |',
|
|
|
|
|
|
'| --- | --- | --- | --- | --- |',
|
|
|
|
|
|
'| AP-20260620063557-4JU2MWEF | 2026-02-20 至 2026-02-23 | 审批中 | 辅助国网仿生产服务器部署 | [查看](#ai-open-application-detail:AP-20260620063557-4JU2MWEF) |'
|
|
|
|
|
|
].join('\n'))
|
|
|
|
|
|
|
2026-06-21 22:49:53 +08:00
|
|
|
|
assert.match(rendered, /<section class="ai-document-card-list" role="list" aria-label="单据结果">/)
|
|
|
|
|
|
assert.match(rendered, /<article class="ai-document-card is-pending" role="listitem" aria-label="单据详情">/)
|
|
|
|
|
|
assert.match(rendered, /<div class="ai-document-card__summary">/)
|
|
|
|
|
|
assert.match(rendered, /<span class="ai-document-card__label">日期<\/span>/)
|
2026-06-20 21:44:16 +08:00
|
|
|
|
assert.match(rendered, /2026-02-20 至 2026-02-23/)
|
2026-06-21 22:49:53 +08:00
|
|
|
|
assert.match(rendered, /<span class="ai-document-card__label">当前节点<\/span>/)
|
2026-06-20 21:44:16 +08:00
|
|
|
|
assert.match(rendered, /辅助国网仿生产服务器部署/)
|
2026-06-21 22:49:53 +08:00
|
|
|
|
assert.match(rendered, /ai-document-card__field--action/)
|
2026-06-20 21:44:16 +08:00
|
|
|
|
assert.doesNotMatch(rendered, /<table>/)
|
2026-06-21 22:49:53 +08:00
|
|
|
|
assert.doesNotMatch(rendered, /ai-html-record-item/)
|
2026-06-20 21:44:16 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-20 10:17:37 +08:00
|
|
|
|
test('AI conversation renderer renders document detail action links as buttons', () => {
|
|
|
|
|
|
const rendered = renderAiConversationHtml('[查看单据](#ai-open-document-detail:CL-20260221001)')
|
|
|
|
|
|
|
|
|
|
|
|
assert.match(rendered, /class="ai-html-action-link ai-html-action-link-document"/)
|
|
|
|
|
|
assert.match(rendered, /data-ai-action="open-document-detail"/)
|
|
|
|
|
|
assert.match(rendered, /href="#ai-open-document-detail:CL-20260221001"/)
|
|
|
|
|
|
assert.doesNotMatch(rendered, /target="_blank"[\s\S]{0,120}#ai-open-document-detail/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-24 22:59:05 +08:00
|
|
|
|
test('AI conversation renderer renders deleted document detail actions as disabled buttons', () => {
|
|
|
|
|
|
const rendered = renderAiConversationHtml('[单据已删除](#ai-deleted-document-detail:claim-deleted-1)')
|
|
|
|
|
|
|
|
|
|
|
|
assert.match(rendered, /class="ai-html-action-link ai-html-action-link-document is-disabled"/)
|
|
|
|
|
|
assert.match(rendered, /aria-disabled="true"/)
|
|
|
|
|
|
assert.match(rendered, /data-ai-action="deleted-document-detail"/)
|
|
|
|
|
|
assert.doesNotMatch(rendered, /href="#ai-deleted-document-detail/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-20 10:17:37 +08:00
|
|
|
|
test('AI conversation renderer renders images as html and rejects unsafe image sources', () => {
|
|
|
|
|
|
const rendered = renderAiConversationHtml([
|
|
|
|
|
|
'### 图片材料',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'内联图片:)'
|
|
|
|
|
|
].join('\n'))
|
|
|
|
|
|
|
|
|
|
|
|
assert.match(rendered, /<figure class="ai-html-image-frame">/)
|
|
|
|
|
|
assert.match(rendered, /<img class="ai-html-image" src="https:\/\/example\.com\/receipt\.png" alt="票据预览" loading="lazy" \/>/)
|
|
|
|
|
|
assert.match(rendered, /<figcaption class="ai-html-image-caption">票据预览<\/figcaption>/)
|
|
|
|
|
|
assert.doesNotMatch(rendered, /javascript:alert/)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
test('AI conversation renderer keeps separated step bullets in one numbered sequence', () => {
|
|
|
|
|
|
const rendered = renderAiConversationHtml([
|
|
|
|
|
|
'点击下方 **确认发起出差申请** 后,我会继续完成:',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'- **单据重叠核查**:检查同一时间段是否已有申请单,避免重复申请。',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'- **预算与审批预审**:查看部门预算影响,判断是否可能增加预算管理者审核。',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'- **申请表生成**:预审完成后,再展示完整申请表并自动预填已识别信息。'
|
|
|
|
|
|
].join('\n'))
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal((rendered.match(/class="ai-html-steps"/g) || []).length, 1)
|
|
|
|
|
|
assert.match(rendered, /<span class="ai-html-step-index">1<\/span>[\s\S]*单据重叠核查/)
|
|
|
|
|
|
assert.match(rendered, /<span class="ai-html-step-index">2<\/span>[\s\S]*预算与审批预审/)
|
|
|
|
|
|
assert.match(rendered, /<span class="ai-html-step-index">3<\/span>[\s\S]*申请表生成/)
|
|
|
|
|
|
})
|
2026-06-22 11:58:53 +08:00
|
|
|
|
|
|
|
|
|
|
test('AI conversation renderer hides noisy attachment association reason fragments', () => {
|
|
|
|
|
|
const rendered = renderAiConversationHtml([
|
|
|
|
|
|
'### 我已先识别票据,并匹配到最可能的报销单',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'本次附件:1 份(2月20 武汉-上海.pdf)',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'识别摘要:2月20 武汉-上海.pdf:电子发票(铁路电子客票) 武汉-上海 票价 354元',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'推荐关联:R74CB7C2R',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'单据事项::26429165800002785705; :2026; 05',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'匹配依据:票据日期与报销单日期一致;地点或行程包含 上海;当前单据仍是可归集草稿'
|
|
|
|
|
|
].join('\n'))
|
|
|
|
|
|
|
|
|
|
|
|
assert.match(rendered, /ai-attachment-association-card/)
|
|
|
|
|
|
assert.match(rendered, /R74CB7C2R/)
|
|
|
|
|
|
assert.doesNotMatch(rendered, /单据事项/)
|
|
|
|
|
|
assert.doesNotMatch(rendered, /关联事项/)
|
|
|
|
|
|
assert.doesNotMatch(rendered, /26429165800002785705/)
|
|
|
|
|
|
assert.doesNotMatch(rendered, /:2026/)
|
|
|
|
|
|
})
|