319 lines
10 KiB
TypeScript
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'),
|
|
)
|
|
})
|
|
})
|