163 lines
5.5 KiB
TypeScript
163 lines
5.5 KiB
TypeScript
/**
|
||
* AssistantText 路由标签悬停显示单元测试 (U5)。
|
||
*
|
||
* 验证场景:
|
||
* - 默认状态路由 tag 不可见
|
||
* - 鼠标悬停助手消息时 tag 淡入显示
|
||
* - 鼠标移开时 tag 淡出
|
||
* - 无 matched_skill 的消息悬停时也不显示 tag
|
||
* - 淡入淡出过渡平滑无闪烁(v-show 保留 DOM,无重挂载)
|
||
*
|
||
* 说明:happy-dom 不计算 CSS transition,因此"淡入/淡出"通过
|
||
* `assistant-text__routing--visible` class 的增删来断言;"平滑无闪烁"
|
||
* 通过断言 DOM 节点恒定(v-show 而非 v-if,hover 前后同一节点)验证。
|
||
*/
|
||
|
||
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)
|
||
})
|
||
})
|