141 lines
7.9 KiB
TypeScript
141 lines
7.9 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { expect, it, vi } from 'vitest'
|
|
import { flushPromises, mount } from '@vue/test-utils'
|
|
import DiagramInteractions from './DiagramInteractions.vue'
|
|
import { appendDiagramControls } from '@/utils/diagramControls'
|
|
|
|
it.each(['markdown-mermaid', 'editor-mermaid-preview'])('handles copied SVG controls in %s', async className => {
|
|
const container = document.createElement('div')
|
|
container.className = className
|
|
container.innerHTML = '<svg viewBox="0 0 400 200"><text>Diagram</text></svg>'
|
|
appendDiagramControls(container)
|
|
const wrapper = mount(DiagramInteractions, { slots: { default: container.outerHTML }, attachTo: document.body })
|
|
const svg = wrapper.get('svg').element as SVGSVGElement
|
|
await wrapper.get('[data-diagram-action="in"]').trigger('click')
|
|
expect(svg.style.width).toBe('480px')
|
|
await wrapper.get('[data-diagram-action="reset"]').trigger('click')
|
|
expect(svg.style.maxWidth).toBe('')
|
|
const dialog = document.querySelector('dialog')!
|
|
const show = vi.fn()
|
|
dialog.showModal = show
|
|
await wrapper.get('[data-diagram-action="view"]').trigger('click')
|
|
await flushPromises()
|
|
expect(show).toHaveBeenCalledOnce()
|
|
expect(dialog.textContent).toContain('Diagram')
|
|
wrapper.unmount()
|
|
})
|
|
|
|
|
|
it('arms wheel zoom with middle click and releases page scrolling after mouse movement', async () => {
|
|
const wrapper = mount(DiagramInteractions, { slots: { default: '<div class="markdown-mermaid"><svg viewBox="0 0 400 200"><text>Chart</text></svg></div>' }, attachTo: document.body })
|
|
const svg = wrapper.get('svg').element as SVGSVGElement
|
|
const scroll = () => { const event = new WheelEvent('wheel', { deltaY: -100, bubbles: true, cancelable: true }); svg.dispatchEvent(event); return event }
|
|
expect(scroll().defaultPrevented).toBe(false)
|
|
await wrapper.get('svg').trigger('mousedown', { button: 1, clientX: 10, clientY: 20 })
|
|
expect(scroll().defaultPrevented).toBe(true)
|
|
expect(parseFloat(svg.style.width)).toBeGreaterThan(400)
|
|
const width = svg.style.width
|
|
document.dispatchEvent(new MouseEvent('mousemove', { clientX: 11, clientY: 20 }))
|
|
expect(scroll().defaultPrevented).toBe(false)
|
|
expect(svg.style.width).toBe(width)
|
|
await wrapper.get('svg').trigger('mousedown', { button: 1 })
|
|
window.dispatchEvent(new Event('blur'))
|
|
expect(scroll().defaultPrevented).toBe(false)
|
|
wrapper.unmount()
|
|
})
|
|
|
|
|
|
it('preserves Mermaid HTML node and edge labels in the viewer while removing active HTML', async () => {
|
|
const container = document.createElement('div')
|
|
container.className = 'markdown-mermaid'
|
|
container.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 200"><g class="nodeLabel"><foreignObject width="100" height="30"><div xmlns="http://www.w3.org/1999/xhtml"><span onclick="alert(1)">系统验证</span></div></foreignObject></g><g class="edgeLabel"><foreignObject width="100" height="30"><div xmlns="http://www.w3.org/1999/xhtml">验证通过<img src="x" onerror="alert(1)" /></div></foreignObject></g><text>结束</text></svg>`
|
|
appendDiagramControls(container)
|
|
const wrapper = mount(DiagramInteractions, { slots: { default: '<div class="markdown-mermaid"></div>' }, attachTo: document.body })
|
|
wrapper.get('.markdown-mermaid').element.innerHTML = container.innerHTML
|
|
const dialog = document.querySelector('dialog')!
|
|
dialog.showModal = vi.fn()
|
|
await wrapper.get('[data-diagram-action="view"]').trigger('click')
|
|
await flushPromises()
|
|
expect(dialog.querySelectorAll('foreignObject')).toHaveLength(2)
|
|
expect(dialog.textContent).toContain('系统验证')
|
|
expect(dialog.textContent).toContain('验证通过')
|
|
expect(dialog.textContent).toContain('结束')
|
|
expect(dialog.querySelector('[onclick], [onerror], script')).toBeNull()
|
|
wrapper.unmount()
|
|
})
|
|
|
|
|
|
it('zooms directly in the viewer with bounded speed even for a large wheel delta', async () => {
|
|
const container = document.createElement('div')
|
|
container.className = 'markdown-mermaid'
|
|
container.innerHTML = '<svg viewBox="0 0 400 200"><text>Chart</text></svg>'
|
|
appendDiagramControls(container)
|
|
const wrapper = mount(DiagramInteractions, { slots: { default: container.outerHTML }, attachTo: document.body })
|
|
const dialog = document.querySelector('dialog')!
|
|
dialog.showModal = vi.fn()
|
|
await wrapper.get('[data-diagram-action="view"]').trigger('click')
|
|
const event = new WheelEvent('wheel', { deltaY: -10000, bubbles: true, cancelable: true })
|
|
dialog.querySelector('.diagram-viewer-scroll')!.dispatchEvent(event)
|
|
await flushPromises()
|
|
expect(event.defaultPrevented).toBe(true)
|
|
expect(Number(dialog.querySelector('output')!.textContent!.replace('%', ''))).toBeGreaterThan(100)
|
|
expect(Number(dialog.querySelector('output')!.textContent!.replace('%', ''))).toBeLessThanOrEqual(105)
|
|
wrapper.unmount()
|
|
})
|
|
|
|
|
|
it('starts wheel zoom from the fitted width instead of the intrinsic SVG width', async () => {
|
|
const wrapper = mount(DiagramInteractions, { slots: { default: '<div class="markdown-mermaid"><svg viewBox="0 0 4000 2000"></svg></div>' }, attachTo: document.body })
|
|
const svg = wrapper.get('svg').element as SVGSVGElement
|
|
vi.spyOn(svg, 'getBoundingClientRect').mockReturnValue({ width: 400, height: 200, left: 0, top: 0 } as DOMRect)
|
|
await wrapper.get('svg').trigger('mousedown', { button: 1 })
|
|
svg.dispatchEvent(new WheelEvent('wheel', { deltaY: -100, bubbles: true, cancelable: true }))
|
|
expect(parseFloat(svg.style.width)).toBeGreaterThan(400)
|
|
expect(parseFloat(svg.style.width)).toBeLessThanOrEqual(420)
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('keeps the cursor point fixed by adjusting the scroll container during zoom', async () => {
|
|
let frame: FrameRequestCallback | undefined
|
|
const raf = vi.spyOn(window, 'requestAnimationFrame').mockImplementation(callback => { frame = callback; return 1 })
|
|
const cancel = vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(() => {})
|
|
const wrapper = mount(DiagramInteractions, { slots: { default: '<div class="markdown-mermaid" style="overflow-x:auto;overflow-y:auto"><svg viewBox="0 0 400 200"></svg></div>' }, attachTo: document.body })
|
|
try {
|
|
const container = wrapper.get('.markdown-mermaid').element as HTMLElement
|
|
const svg = wrapper.get('svg').element as SVGSVGElement
|
|
vi.spyOn(svg, 'getBoundingClientRect').mockImplementation(() => {
|
|
const width = parseFloat(svg.style.width) || 400
|
|
return { width, height: width / 2, left: -container.scrollLeft, top: -container.scrollTop } as DOMRect
|
|
})
|
|
await wrapper.get('svg').trigger('mousedown', { button: 1, clientX: 100, clientY: 50 })
|
|
svg.dispatchEvent(new WheelEvent('wheel', { clientX: 100, clientY: 50, deltaY: -100, bubbles: true, cancelable: true }))
|
|
frame?.(performance.now())
|
|
const rect = svg.getBoundingClientRect()
|
|
expect(rect.left + rect.width * .25).toBeCloseTo(100)
|
|
expect(rect.top + rect.height * .25).toBeCloseTo(50)
|
|
expect(container.scrollLeft).toBeGreaterThan(0)
|
|
} finally {
|
|
wrapper.unmount()
|
|
raf.mockRestore(); cancel.mockRestore()
|
|
}
|
|
})
|
|
|
|
|
|
it('fits the full chart on open and clears the previous viewport scroll', async () => {
|
|
const wrapper = mount(DiagramInteractions, { slots: { default: '<div class="markdown-mermaid"><svg viewBox="0 0 2400 200"><text>Final task</text></svg><button data-diagram-action="view">View</button></div>' }, attachTo: document.body })
|
|
const dialog = document.querySelector('dialog')!
|
|
dialog.showModal = vi.fn()
|
|
const viewport = dialog.querySelector('.diagram-viewer-scroll') as HTMLElement
|
|
Object.defineProperty(viewport, 'clientWidth', { value: 1000 })
|
|
Object.defineProperty(viewport, 'clientHeight', { value: 600 })
|
|
viewport.scrollLeft = 900
|
|
viewport.scrollTop = 30
|
|
await wrapper.get('[data-diagram-action="view"]').trigger('click')
|
|
await flushPromises()
|
|
expect((dialog.querySelector('.diagram-viewer-image') as HTMLElement).style.width).toBe('1000px')
|
|
expect(viewport.scrollLeft).toBe(0)
|
|
expect(viewport.scrollTop).toBe(0)
|
|
expect(dialog.textContent).toContain('Final task')
|
|
wrapper.unmount()
|
|
})
|