refactor(chart): enforce 400-line source limit
Some checks failed
Package component / package (push) Failing after 4m19s
Some checks failed
Package component / package (push) Failing after 4m19s
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
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('2.00')
|
||||
})
|
||||
|
||||
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'),
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user