fischer-agentkit/src/agentkit/server/frontend/tests/unit/components/AssistantText.test.ts

163 lines
5.5 KiB
TypeScript
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.

/**
* AssistantText 路由标签悬停显示单元测试 (U5)。
*
* 验证场景:
* - 默认状态路由 tag 不可见
* - 鼠标悬停助手消息时 tag 淡入显示
* - 鼠标移开时 tag 淡出
* - 无 matched_skill 的消息悬停时也不显示 tag
* - 淡入淡出过渡平滑无闪烁v-show 保留 DOM无重挂载
*
* 说明happy-dom 不计算 CSS transition因此"淡入/淡出"通过
* `assistant-text__routing--visible` class 的增删来断言;"平滑无闪烁"
* 通过断言 DOM 节点恒定v-show 而非 v-ifhover 前后同一节点)验证。
*/
import { afterEach, describe, expect, it, vi } from 'vitest'
import { createApp, h, nextTick, type App } from 'vue'
import AssistantText from '@/components/chat/messages/AssistantText.vue'
import type { IChatMessage } from '@/api/types'
// ── Mocks ────────────────────────────────────────────────────────────
// 路由标签可见性逻辑不依赖 markdown 渲染mock 掉重量级依赖以隔离逻辑、
// 加快测试。ant-design-vue 的 Tag/Spin 与图标保留真实加载happy-dom 可渲染)。
vi.mock('markdown-it', () => {
const md = {
render: (s: string) => s,
utils: { escapeHtml: (s: string) => s },
renderer: { rules: {} as Record<string, unknown> },
}
return { default: vi.fn(() => md) }
})
vi.mock('dompurify', () => ({
default: { sanitize: (html: string) => html },
}))
vi.mock('highlight.js/lib/core', () => ({
default: {
registerLanguage: vi.fn(),
getLanguage: vi.fn(() => null),
highlight: vi.fn(() => ({ value: '' })),
},
}))
// ── Fixtures & helpers ───────────────────────────────────────────────
function makeMessage(overrides: Partial<IChatMessage> = {}): IChatMessage {
return {
id: 'msg-1',
role: 'assistant',
content: '你好',
timestamp: '2026-07-01T00:00:00.000Z',
status: 'completed',
matched_skill: 'react',
routing_method: 'semantic',
confidence: 0.9,
...overrides,
}
}
interface Mounted {
container: HTMLElement
root: HTMLElement
app: App
unmount: () => void
}
function mountAssistantText(message: IChatMessage): Mounted {
const container = document.createElement('div')
document.body.appendChild(container)
const app = createApp({
render: () => h(AssistantText, { message }),
})
app.mount(container)
const root = container.querySelector('.assistant-text') as HTMLElement
return { container, root, app, unmount: () => { app.unmount(); container.remove() } }
}
function hover(el: HTMLElement): void {
el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: false }))
}
function leave(el: HTMLElement): void {
el.dispatchEvent(new MouseEvent('mouseleave', { bubbles: false }))
}
function getRouting(container: HTMLElement): HTMLElement {
return container.querySelector('.assistant-text__routing') as HTMLElement
}
// ── Tests ────────────────────────────────────────────────────────────
describe('AssistantText — 路由标签悬停显示 (U5)', () => {
let mounted: Mounted | null = null
afterEach(() => {
mounted?.unmount()
mounted = null
})
it('默认状态路由 tag 不可见', () => {
mounted = mountAssistantText(makeMessage())
const routing = getRouting(mounted.container)
// v-show 保留 DOM非 v-if但默认 opacity:0 → 无 --visible class
expect(routing).not.toBeNull()
expect(routing.classList.contains('assistant-text__routing--visible')).toBe(false)
})
it('鼠标悬停助手消息时 tag 淡入显示', async () => {
mounted = mountAssistantText(makeMessage())
const routing = getRouting(mounted.container)
expect(routing.classList.contains('assistant-text__routing--visible')).toBe(false)
hover(mounted.root)
await nextTick()
expect(routing.classList.contains('assistant-text__routing--visible')).toBe(true)
})
it('鼠标移开时 tag 淡出', async () => {
mounted = mountAssistantText(makeMessage())
hover(mounted.root)
await nextTick()
const routing = getRouting(mounted.container)
expect(routing.classList.contains('assistant-text__routing--visible')).toBe(true)
leave(mounted.root)
await nextTick()
expect(routing.classList.contains('assistant-text__routing--visible')).toBe(false)
})
it('无 matched_skill 的消息悬停时也不显示 tag', async () => {
mounted = mountAssistantText(makeMessage({ matched_skill: undefined }))
const routing = getRouting(mounted.container)
// v-show=false → display:none
expect(routing.style.display).toBe('none')
hover(mounted.root)
await nextTick()
// 即使悬停,因无路由信息仍隐藏
expect(routing.style.display).toBe('none')
})
it('淡入淡出过渡平滑无闪烁v-show 保留 DOM无重挂载', async () => {
mounted = mountAssistantText(makeMessage())
const routingBefore = getRouting(mounted.container)
hover(mounted.root)
await nextTick()
const routingHovered = getRouting(mounted.container)
// hover 前后同一节点 → 未重挂载
expect(routingHovered).toBe(routingBefore)
leave(mounted.root)
await nextTick()
const routingLeft = getRouting(mounted.container)
expect(routingLeft).toBe(routingBefore)
})
})