Files
huihuiSquare/digital-avatar-app/scripts/avatar-page-data.test.mjs

181 lines
7.9 KiB
JavaScript

import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import {
buildAvatarUpdatePayload,
normalizeAvatarEditForm,
pickAvatarId,
unwrapListData,
} from '../src/utils/avatar-page-data.js'
import { renderChatMarkdownCharacters } from '../src/utils/chat-markdown.js'
assert.deepEqual(unwrapListData([{ id: 'a1' }]), [{ id: 'a1' }], 'unwrapListData should return raw arrays')
assert.deepEqual(
unwrapListData({ data: [{ id: 'a2' }], total: 1 }),
[{ id: 'a2' }],
'unwrapListData should unwrap wrapped collection payloads'
)
assert.deepEqual(unwrapListData(null), [], 'unwrapListData should fall back to an empty list')
assert.equal(
pickAvatarId('current-id', [{ id: 'first-id' }]),
'current-id',
'pickAvatarId should prefer current avatar id'
)
assert.equal(
pickAvatarId('', [{ id: 'first-id' }]),
'first-id',
'pickAvatarId should fall back to the first avatar id'
)
assert.equal(pickAvatarId('', []), null, 'pickAvatarId should return null when no avatar exists')
const boldReply = renderChatMarkdownCharacters('请注意:**不能自行诊断或随意用药**。')
assert.equal(
boldReply.map((character) => character.text).join(''),
'请注意:不能自行诊断或随意用药。',
'chat markdown should hide bold markers'
)
assert.equal(
boldReply.filter((character) => character.bold).map((character) => character.text).join(''),
'不能自行诊断或随意用药',
'chat markdown should style bold text'
)
assert.equal(
renderChatMarkdownCharacters('****重点****').map((character) => character.text).join(''),
'重点',
'chat markdown should tolerate repeated bold markers'
)
assert.deepEqual(
normalizeAvatarEditForm({
name: '我的分身',
displayName: '会会助手',
description: '描述',
status: 'inactive',
photoUrl: 'https://img.example/avatar.png',
config: {
replyStyle: 'friendly',
creativity: 72,
rigor: 88,
humor: 16,
responseLength: 'short',
systemPrompt: '不要编造',
profession: '医生',
position: '主任医师',
organization: '测试医院',
organizationAddress: '测试路 1 号',
autoReply: false
},
}),
{
name: '我的分身',
displayName: '会会助手',
description: '描述',
status: 'inactive',
photoUrl: 'https://img.example/avatar.png',
replyStyle: 'friendly',
creativity: 72,
rigor: 88,
humor: 16,
responseLength: 'short',
systemPrompt: '不要编造',
profession: '医生',
position: '主任医师',
organization: '测试医院',
organizationAddress: '测试路 1 号',
autoReply: false,
},
'normalizeAvatarEditForm should map API avatars into edit form state'
)
assert.deepEqual(
buildAvatarUpdatePayload({
name: '更新后的分身',
displayName: '更新后的助手',
description: '新描述',
status: 'active',
photoUrl: 'https://img.example/new-avatar.png',
replyStyle: 'casual',
creativity: 65,
rigor: 70,
humor: 25,
responseLength: 'medium',
systemPrompt: '回答简洁',
profession: '医生',
position: '主任医师',
organization: '测试医院',
organizationAddress: '测试路 1 号',
autoReply: true,
}),
{
name: '更新后的分身',
displayName: '更新后的助手',
description: '新描述',
status: 'active',
photoUrl: 'https://img.example/new-avatar.png',
config: {
replyStyle: 'casual',
creativity: 65,
rigor: 70,
humor: 25,
responseLength: 'medium',
systemPrompt: '回答简洁',
profession: '医生',
position: '主任医师',
organization: '测试医院',
organizationAddress: '测试路 1 号',
autoReply: true,
},
},
'buildAvatarUpdatePayload should emit API-ready update payloads'
)
const knowledgeView = fs.readFileSync(path.resolve('src/views/KnowledgeManage.vue'), 'utf8')
assert.match(knowledgeView, /文档知识库/, 'knowledge page should expose the document tab')
assert.match(knowledgeView, /标准问答对/, 'knowledge page should expose the QA tab')
assert.match(knowledgeView, /activeTab/, 'knowledge page should switch active tabs')
assert.match(knowledgeView, /accept="\.md,\.txt,\.pdf,\.doc,\.docx,\.xlsx"/, 'knowledge page should accept md and txt')
assert.match(knowledgeView, /mobile-card-list/, 'knowledge page should render mobile-first card lists')
assert.match(knowledgeView, /knowledge-card/, 'knowledge page should expose document and QA cards')
const chatView = fs.readFileSync(path.resolve('src/views/AvatarChat.vue'), 'utf8')
assert.match(chatView, /avatar\?\.photoUrl/, 'chat should render the active avatar photo when available')
assert.match(chatView, /userAvatarUrl/, 'chat should render the logged-in user photo when available')
assert.match(chatView, /avatarStatus/, 'chat should synchronize the visible status indicator with avatar status')
assert.match(chatView, /document\.title = avatar\.value/, 'chat should use the avatar name as the page title')
assert.match(chatView, /position: sticky/, 'chat header should remain visible while the message list scrolls')
assert.match(chatView, /typing-character/, 'chat replies should animate one character at a time')
assert.match(chatView, /renderChatMarkdownCharacters/, 'chat replies should render markdown as safe web text')
assert.match(chatView, /markdown-bold/, 'chat replies should style markdown emphasis without showing markers')
assert.match(chatView, /streamAvatarChat/, 'private chat should consume SSE response chunks')
assert.match(chatView, /streamPublicAvatarChat/, 'public chat should consume SSE response chunks')
assert.match(chatView, /scrollDuringStream/, 'streaming replies should throttle scrolling to animation frames')
assert.match(chatView, /typing-character\.newline/, 'streaming replies should render sentence line breaks')
assert.match(chatView, /let attached = false/, 'assistant bubble should wait for the first streamed text chunk')
assert.match(chatView, /reactive<DisplayMessage>/, 'every streamed character should update through a reactive reply object')
assert.doesNotMatch(chatView, /你好,我是\{\{/, 'chat welcome card should not introduce the avatar by name')
assert.doesNotMatch(chatView, /\/\[。!?;\]\/\.test\(character\)/, 'chat should not force a line break after every sentence')
assert.match(chatView, /previous === '\\n'/, 'streaming text should collapse whitespace at line boundaries')
assert.match(chatView, /welcome-avatar/, 'chat welcome should use the active avatar image instead of a generic icon')
assert.doesNotMatch(chatView, /我会优先参考标准问答和知识库/, 'chat welcome should not expose internal answer sources')
assert.match(chatView, /welcome-description/, 'chat welcome should render the avatar description')
assert.doesNotMatch(chatView, /介绍一下你自己/, 'chat welcome should not contain fixed starter questions')
const editView = fs.readFileSync(path.resolve('src/views/AvatarEdit.vue'), 'utf8')
assert.match(editView, />分身微调</, 'avatar edit page should use the requested title')
assert.match(editView, /uploadAvatarPhoto/, 'avatar edit page should upload a clicked replacement photo')
assert.doesNotMatch(editView, />头像链接</, 'avatar edit page should not expose a photo URL input')
for (const field of ['profession', 'position', 'organization', 'organizationAddress']) {
assert.match(editView, new RegExp(`formData\\.${field}`), `avatar edit page should expose ${field}`)
}
const manageView = fs.readFileSync(path.resolve('src/views/AvatarManage.vue'), 'utf8')
assert.match(manageView, /shareAvatar/, 'avatar management should offer a share action')
assert.match(manageView, /createAvatarShareLink/, 'share action should create a public share link')
const router = fs.readFileSync(path.resolve('src/router/index.ts'), 'utf8')
assert.match(router, /path: '\/share\/:shareToken'/, 'router should expose a public chat route')
console.log('avatar-page-data tests passed')