Files
waveform-analysis/src/components/waveformChartCases/interactionAndAnnotationCreation.test.ts
李启源 c5aa3c662a
All checks were successful
Package component / package (push) Successful in 4m2s
fix(chart): refine axis number formatting
2026-08-04 11:28:42 +08:00

319 lines
10 KiB
TypeScript

import { flushPromises } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import { flushAnimationFrames, pendingAnimationFrameCount } from '../../test/setup'
import { mountSizedChart } from '../../test/waveformChart'
describe('WaveformChart', () => {
it('zooms only the active independent track and resets when the mode changes', async () => {
const wrapper = await mountSizedChart({
kind: 'series',
series: [
{
name: 'A',
data: {
kind: 'points',
points: [
{ x: 0, y: 0 },
{ x: 2, y: 1 },
],
},
},
{
name: 'B',
data: {
kind: 'points',
points: [
{ x: 0, y: 2 },
{ x: 2, y: 3 },
],
},
},
],
})
const endpoints = () =>
wrapper.findAll('.waveform-chart__axis-endpoint--end').map((item) => item.text())
const initialEndpoints = endpoints()
const firstOverlay = wrapper.findAll('.waveform-chart__overlay--independent')[0]
Object.defineProperty(firstOverlay.element, 'getBoundingClientRect', {
value: () => ({ left: 0, top: 0, width: 712, height: 130 }),
})
firstOverlay.element.dispatchEvent(
new WheelEvent('wheel', {
deltaY: -4000,
clientX: 356,
clientY: 65,
bubbles: true,
cancelable: true,
}),
)
flushAnimationFrames()
await flushPromises()
expect(endpoints()[0]).not.toBe(initialEndpoints[0])
expect(endpoints()[1]).toBe(initialEndpoints[1])
await wrapper.setProps({ displayMode: 'separated' })
await flushPromises()
expect(wrapper.findAll('.waveform-chart__axis--x')).toHaveLength(1)
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2000')
})
it('updates rendering props and disables zoom interaction', async () => {
const wrapper = await mountSizedChart(
{
kind: 'points',
points: [
{ x: 0, y: 1 },
{ x: 1, y: 2 },
],
},
{ zoomable: true, lineColor: '#ff0000' },
)
expect(wrapper.get('.waveform-chart__line').attributes('stroke')).toBe('#ff0000')
expect(wrapper.get('.waveform-chart__overlay').classes()).toContain('is-zoomable')
await wrapper.setProps({ zoomable: false, lineColor: '#00aa00' })
await flushPromises()
expect(wrapper.get('.waveform-chart__line').attributes('stroke')).toBe('#00aa00')
expect(wrapper.get('.waveform-chart__overlay').classes()).not.toContain('is-zoomable')
})
it('binds zoom only while zooming is enabled', async () => {
const wrapper = await mountSizedChart({
kind: 'points',
points: [
{ x: 0, y: 0 },
{ x: 2, y: 1 },
],
})
const overlay = wrapper.get('.waveform-chart__overlay')
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
value: () => ({ left: 0, top: 0, width: 712, height: 290 }),
})
const initialZoomEventCount = wrapper.emitted('zoom-change')?.length ?? 0
overlay.element.dispatchEvent(
new WheelEvent('wheel', {
deltaY: -4000,
clientX: 356,
clientY: 145,
bubbles: true,
cancelable: true,
}),
)
overlay.element.dispatchEvent(
new WheelEvent('wheel', {
deltaY: -200,
clientX: 356,
clientY: 145,
bubbles: true,
cancelable: true,
}),
)
expect(pendingAnimationFrameCount()).toBe(1)
expect(wrapper.emitted('zoom-change')?.length ?? 0).toBe(initialZoomEventCount)
flushAnimationFrames()
await flushPromises()
expect(wrapper.emitted('zoom-change')?.length ?? 0).toBe(initialZoomEventCount + 1)
await wrapper.setProps({ zoomable: false })
await flushPromises()
const zoomEventCount = wrapper.emitted('zoom-change')?.length ?? 0
overlay.element.dispatchEvent(
new WheelEvent('wheel', {
deltaY: -4000,
clientX: 356,
clientY: 145,
bubbles: true,
cancelable: true,
}),
)
flushAnimationFrames()
await flushPromises()
expect(wrapper.emitted('zoom-change')?.length ?? 0).toBe(zoomEventCount)
expect(overlay.classes()).not.toContain('is-zoomable')
})
it('creates a controlled annotation from externally selected annotation mode', async () => {
const wrapper = await mountSizedChart(
{
kind: 'points',
points: [
{ x: 0, y: 0 },
{ x: 1, y: 5 },
],
},
{ interactionMode: 'annotation' },
)
const overlay = wrapper.get('.waveform-chart__overlay')
const overlayWidth = Number(overlay.attributes('width'))
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
})
const path = wrapper.get('.waveform-chart__line').attributes('d') ?? ''
const endpoint = path.match(/L([\d.-]+),([\d.-]+)$/)
expect(endpoint).not.toBeNull()
expect(wrapper.attributes('data-interaction-mode')).toBe('annotation')
overlay.element.dispatchEvent(
new MouseEvent('click', {
clientX: Number(endpoint?.[1]),
clientY: Number(endpoint?.[2]),
bubbles: true,
cancelable: true,
}),
)
await flushPromises()
expect(wrapper.find('.waveform-annotation-editor').exists()).toBe(true)
await wrapper.get('textarea[aria-label="标注文本"]').setValue('峰值点')
await wrapper.get('.waveform-annotation-editor button.is-primary').trigger('click')
const annotations = wrapper.emitted('update:annotations')?.at(-1)?.[0] as
Array<{ seriesId: string; x: number; y: number; text: string }> | undefined
expect(annotations).toHaveLength(1)
expect(annotations?.[0]).toMatchObject({ seriesId: 'series-0', x: 1, y: 5, text: '峰值点' })
expect(wrapper.emitted('annotation-create')).toHaveLength(1)
expect(wrapper.attributes('data-interaction-mode')).toBe('annotation')
})
it('supports right-click creation anywhere in the plot without drawing an anchor or guide line', async () => {
const wrapper = await mountSizedChart(
{
kind: 'points',
points: [
{ x: 0, y: 0 },
{ x: 1, y: 5 },
],
},
{ grid: { rowCount: 1, columnCount: 1 } },
)
const overlay = wrapper.get('.waveform-chart__overlay')
const overlayWidth = Number(overlay.attributes('width'))
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
})
overlay.element.dispatchEvent(
new MouseEvent('contextmenu', {
clientX: overlayWidth / 2,
clientY: 145,
bubbles: true,
}),
)
await flushPromises()
expect(wrapper.find('.waveform-annotation-editor').exists()).toBe(true)
expect(wrapper.get('.waveform-annotation-editor').attributes('aria-modal')).toBe('true')
expect(wrapper.find('.waveform-annotation-editor__panel').exists()).toBe(true)
const textarea = wrapper.get('textarea[aria-label="标注文本"]')
const textareaContextMenu = new MouseEvent('contextmenu', {
bubbles: true,
cancelable: true,
})
expect(textarea.element.dispatchEvent(textareaContextMenu)).toBe(true)
expect(textareaContextMenu.defaultPrevented).toBe(false)
await textarea.setValue('右键标注')
await wrapper.get('.waveform-annotation-editor button.is-primary').trigger('click')
// Annotation snaps to nearest sample point (x=1, y=5)
expect(wrapper.emitted('update:annotations')?.at(-1)?.[0]).toMatchObject([
{ seriesId: 'series-0', x: 1, y: 5 },
])
await wrapper.setProps({
annotations: [{ id: 'right-click', seriesId: 'series-0', x: 1, y: 5, text: '右键标注' }],
})
expect(wrapper.find('.waveform-annotation__vertical-line').exists()).toBe(false)
expect(wrapper.find('.waveform-annotation__anchor').exists()).toBe(false)
expect(wrapper.get('.waveform-annotation__arrow').attributes('x2')).not.toBe(
String(overlayWidth / 2),
)
})
it('limits modal channel options to the graph frame under the pointer', async () => {
const wrapper = await mountSizedChart(
{
kind: 'series',
series: [
{
id: 'channel-a',
name: '通道 A',
data: {
kind: 'points',
points: [
{ x: 0, y: 0 },
{ x: 1, y: 1 },
],
},
},
{
id: 'channel-b',
name: '通道 B',
data: {
kind: 'points',
points: [
{ x: 0, y: 10 },
{ x: 1, y: 11 },
],
},
},
],
},
{ displayMode: 'separated' },
)
const overlays = wrapper.findAll('.waveform-chart__overlay--shared')
expect(overlays.length).toBeGreaterThan(0)
Object.defineProperty(overlays[0].element, 'getBoundingClientRect', {
value: () => ({ left: 0, top: 0, width: 712, height: 290 }),
})
overlays[0].element.dispatchEvent(
new MouseEvent('contextmenu', { clientX: 356, clientY: 30, bubbles: true }),
)
await flushPromises()
expect(
wrapper
.find('select[aria-label="选择标注波形"]')
.findAll('option')
.map((item) => item.text()),
).toEqual(['通道 A'])
await wrapper.get('button[aria-label="关闭标注编辑器"]').trigger('click')
overlays[0].element.dispatchEvent(
new MouseEvent('contextmenu', { clientX: 356, clientY: 230, bubbles: true }),
)
await flushPromises()
expect(
wrapper
.find('select[aria-label="选择标注波形"]')
.findAll('option')
.map((item) => item.text()),
).toEqual(['通道 B'])
})
it('intelligently chooses placement to avoid clipping at boundaries', async () => {
const wrapper = await mountSizedChart(
{
kind: 'points',
points: [
{ x: 0, y: 0 },
{ x: 1, y: 5 },
],
},
{ annotations: [{ id: 'top-edge', seriesId: 'series-0', x: 0.5, y: 5, text: '顶部标注' }] },
)
// Smart placement chooses 'bottom' when annotation is near top boundary
expect(wrapper.get('.waveform-annotation').attributes('data-placement')).toBe('bottom')
expect(wrapper.get('.waveform-annotation__arrow').attributes('x1')).toBe(
wrapper.get('.waveform-annotation__arrow').attributes('x2'),
)
})
})