2026-07-27 15:05:13 +08:00
|
|
|
import { flushPromises } from '@vue/test-utils'
|
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
|
|
|
|
|
|
import { resizeObservers } from '../../test/setup'
|
|
|
|
|
|
|
|
|
|
import { gridSeries, mountSizedChart } from '../../test/waveformChart'
|
|
|
|
|
|
|
|
|
|
describe('WaveformChart', () => {
|
|
|
|
|
it('expands the Y-axis label gutter for signed values and long exponents', async () => {
|
|
|
|
|
const wrapper = await mountSizedChart(
|
|
|
|
|
{
|
|
|
|
|
kind: 'series',
|
|
|
|
|
series: Array.from({ length: 4 }, (_, index) => ({
|
|
|
|
|
id: `wide-axis-${index}`,
|
|
|
|
|
name: `宽范围 ${index + 1}`,
|
|
|
|
|
data: {
|
|
|
|
|
kind: 'points',
|
|
|
|
|
points: [
|
|
|
|
|
{ x: 0, y: -1e120 },
|
|
|
|
|
{ x: 0.5, y: 0 },
|
|
|
|
|
{ x: 1, y: 1e120 },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
})),
|
|
|
|
|
},
|
|
|
|
|
{ displayMode: 'compact', grid: { rowCount: 2, columnCount: 2 } },
|
|
|
|
|
)
|
|
|
|
|
const tracks = wrapper.findAll('.waveform-chart__track')
|
|
|
|
|
const labelX = Number(tracks[0].attributes('data-y-axis-label-x'))
|
|
|
|
|
const firstWidth = Number(tracks[0].attributes('data-track-width'))
|
|
|
|
|
const secondLeft = Number(tracks[1].attributes('data-track-left'))
|
|
|
|
|
const labelBackgroundX = Number(
|
|
|
|
|
tracks[0].get('.waveform-chart__y-axis-label-bg').attributes('x'),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
expect(labelX).toBe(-103)
|
|
|
|
|
expect(labelBackgroundX).toBe(labelX - 12)
|
|
|
|
|
expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(119)
|
|
|
|
|
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(119)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('keeps a tick-only gutter when channel labels are empty', async () => {
|
|
|
|
|
const wrapper = await mountSizedChart(
|
|
|
|
|
{
|
|
|
|
|
kind: 'series',
|
|
|
|
|
series: Array.from({ length: 2 }, (_, index) => ({
|
|
|
|
|
id: `unnamed-${index}`,
|
|
|
|
|
name: '',
|
|
|
|
|
data: {
|
|
|
|
|
kind: 'points',
|
|
|
|
|
points: [
|
|
|
|
|
{ x: 0, y: -1e120 },
|
|
|
|
|
{ x: 1, y: 1e120 },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
})),
|
|
|
|
|
},
|
|
|
|
|
{ yLabel: '', grid: { rowCount: 1, columnCount: 2 } },
|
|
|
|
|
)
|
|
|
|
|
const tracks = wrapper.findAll('.waveform-chart__track')
|
|
|
|
|
const firstWidth = Number(tracks[0].attributes('data-track-width'))
|
|
|
|
|
const secondLeft = Number(tracks[1].attributes('data-track-left'))
|
|
|
|
|
|
|
|
|
|
expect(wrapper.findAll('.waveform-chart__y-axis-label')).toHaveLength(0)
|
|
|
|
|
expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(89)
|
|
|
|
|
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(89)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('keeps the Y-axis label gutter stable while paging between value ranges', async () => {
|
|
|
|
|
const wrapper = await mountSizedChart(
|
|
|
|
|
{
|
|
|
|
|
kind: 'series',
|
|
|
|
|
series: [
|
|
|
|
|
{
|
|
|
|
|
id: 'short-axis',
|
|
|
|
|
name: '短范围',
|
|
|
|
|
data: {
|
|
|
|
|
kind: 'points',
|
|
|
|
|
points: [
|
|
|
|
|
{ x: 0, y: 0 },
|
|
|
|
|
{ x: 1, y: 1 },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'wide-axis',
|
|
|
|
|
name: '宽范围',
|
|
|
|
|
data: {
|
|
|
|
|
kind: 'points',
|
|
|
|
|
points: [
|
|
|
|
|
{ x: 0, y: -1e120 },
|
|
|
|
|
{ x: 1, y: 1e120 },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{ grid: { rowCount: 1, columnCount: 1 } },
|
|
|
|
|
)
|
|
|
|
|
const initialMargin = wrapper.attributes('data-chart-left-margin')
|
|
|
|
|
const initialLabelX = wrapper.get('.waveform-chart__track').attributes('data-y-axis-label-x')
|
|
|
|
|
|
|
|
|
|
await wrapper.get('.ant-pagination-next button').trigger('click')
|
|
|
|
|
|
|
|
|
|
expect(wrapper.attributes('data-chart-left-margin')).toBe(initialMargin)
|
|
|
|
|
expect(wrapper.get('.waveform-chart__track').attributes('data-y-axis-label-x')).toBe(
|
|
|
|
|
initialLabelX,
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('hides only secondary-column Y-axis labels when the grid is too narrow', async () => {
|
|
|
|
|
const wrapper = await mountSizedChart(gridSeries(4), {
|
|
|
|
|
displayMode: 'independent',
|
|
|
|
|
grid: { rowCount: 2, columnCount: 2 },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
resizeObservers.at(-1)?.resize(120, 360)
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
expect(wrapper.findAll('.waveform-chart__y-axis-label')).toHaveLength(2)
|
|
|
|
|
expect(wrapper.findAll('.waveform-chart__axis--y')).toHaveLength(4)
|
|
|
|
|
const tracks = wrapper.findAll('.waveform-chart__track')
|
|
|
|
|
const firstWidth = Number(tracks[0].attributes('data-track-width'))
|
|
|
|
|
const secondLeft = Number(tracks[1].attributes('data-track-left'))
|
|
|
|
|
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(39)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('uses one shared overlay and bottom-row x axes for separated and compact grids', async () => {
|
|
|
|
|
for (const displayMode of ['separated', 'compact'] as const) {
|
|
|
|
|
const wrapper = await mountSizedChart(gridSeries(4), {
|
|
|
|
|
displayMode,
|
|
|
|
|
grid: { rowCount: 2, columnCount: 2 },
|
|
|
|
|
})
|
|
|
|
|
expect(wrapper.findAll('.waveform-chart__overlay--shared')).toHaveLength(1)
|
|
|
|
|
expect(wrapper.findAll('.waveform-chart__axis--x')).toHaveLength(2)
|
|
|
|
|
const tracks = wrapper.findAll('.waveform-chart__track')
|
|
|
|
|
const firstTop = Number(tracks[0].attributes('data-track-top'))
|
|
|
|
|
const secondRowTop = Number(tracks[2].attributes('data-track-top'))
|
|
|
|
|
const firstHeight = Number(tracks[0].attributes('data-track-height'))
|
|
|
|
|
if (displayMode === 'compact') expect(secondRowTop).toBe(firstTop + firstHeight)
|
|
|
|
|
else expect(secondRowTop).toBeGreaterThan(firstTop + firstHeight)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('keeps unused compact cells visually empty while preserving bottom X axes', async () => {
|
|
|
|
|
const data = gridSeries(4)
|
|
|
|
|
if (data.kind === 'series') {
|
|
|
|
|
data.series.forEach((series, index) => {
|
|
|
|
|
series.data = {
|
|
|
|
|
kind: 'points',
|
|
|
|
|
points: [
|
|
|
|
|
{ x: 100, y: index },
|
|
|
|
|
{ x: 200, y: index + 1 },
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
const wrapper = await mountSizedChart(data, {
|
|
|
|
|
displayMode: 'compact',
|
|
|
|
|
grid: { rowCount: 2, columnCount: 3 },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const tracks = wrapper.findAll('.waveform-chart__track')
|
|
|
|
|
const emptyTracks = wrapper.findAll('.waveform-chart__track--empty')
|
|
|
|
|
const firstTrackTop = Number(tracks[0].attributes('data-track-top'))
|
|
|
|
|
const firstTrackHeight = Number(tracks[0].attributes('data-track-height'))
|
|
|
|
|
const secondRowFirstTrackTop = Number(tracks[3].attributes('data-track-top'))
|
|
|
|
|
|
|
|
|
|
expect(tracks).toHaveLength(6)
|
|
|
|
|
expect(emptyTracks).toHaveLength(2)
|
|
|
|
|
expect(wrapper.find('.waveform-chart__grid-slot-placeholder').exists()).toBe(false)
|
|
|
|
|
expect(secondRowFirstTrackTop).toBe(firstTrackTop + firstTrackHeight)
|
|
|
|
|
expect(wrapper.findAll('.waveform-chart__axis--x')).toHaveLength(3)
|
|
|
|
|
expect(wrapper.findAll('.waveform-chart__line')).toHaveLength(4)
|
|
|
|
|
expect(emptyTracks[0].findAll('.waveform-chart__grid')).toHaveLength(0)
|
|
|
|
|
expect(emptyTracks[0].find('.waveform-chart__plot-background').exists()).toBe(false)
|
|
|
|
|
expect(emptyTracks[0].find('.waveform-chart__plot-frame').exists()).toBe(false)
|
|
|
|
|
expect(emptyTracks[0].find('.waveform-chart__axis--y').exists()).toBe(false)
|
|
|
|
|
expect(emptyTracks[0].find('.waveform-chart__line').exists()).toBe(false)
|
|
|
|
|
expect(emptyTracks[0].find('.waveform-chart__y-axis-label').exists()).toBe(false)
|
|
|
|
|
expect(emptyTracks[0].find('.waveform-chart__watermark').exists()).toBe(false)
|
|
|
|
|
|
|
|
|
|
const populatedBottomTrack = tracks[3]
|
|
|
|
|
expect(emptyTracks[0].get('.waveform-chart__axis-endpoint--start').text()).toBe(
|
|
|
|
|
populatedBottomTrack.get('.waveform-chart__axis-endpoint--start').text(),
|
|
|
|
|
)
|
|
|
|
|
expect(emptyTracks[0].get('.waveform-chart__axis-endpoint--end').text()).toBe(
|
|
|
|
|
populatedBottomTrack.get('.waveform-chart__axis-endpoint--end').text(),
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('shows the global empty state when compact data has no valid channels', async () => {
|
|
|
|
|
const wrapper = await mountSizedChart(
|
|
|
|
|
{ kind: 'samples', values: [1], sampleRate: -1 },
|
|
|
|
|
{ displayMode: 'compact', grid: { rowCount: 2, columnCount: 3 } },
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
expect(wrapper.get('.waveform-chart__empty').text()).toContain('暂无有效波形数据')
|
|
|
|
|
expect(wrapper.findAll('.waveform-chart__track')).toHaveLength(0)
|
|
|
|
|
expect(wrapper.findAll('.waveform-chart__axis--x')).toHaveLength(0)
|
|
|
|
|
expect(wrapper.findAll('.waveform-chart__line')).toHaveLength(0)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('resets the page when the grid configuration changes', async () => {
|
|
|
|
|
const wrapper = await mountSizedChart(gridSeries(5), {
|
|
|
|
|
grid: { rowCount: 1, columnCount: 1 },
|
|
|
|
|
})
|
|
|
|
|
await wrapper.get('.ant-pagination-next button').trigger('click')
|
|
|
|
|
expect(wrapper.get('.ant-pagination-item-2').classes()).toContain('ant-pagination-item-active')
|
|
|
|
|
|
|
|
|
|
await wrapper.setProps({ grid: { rowCount: 2, columnCount: 1 } })
|
|
|
|
|
await flushPromises()
|
|
|
|
|
expect(wrapper.get('.ant-pagination-item-1').classes()).toContain('ant-pagination-item-active')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('keeps the shared x domain stable while paging separated and compact grids', async () => {
|
|
|
|
|
for (const displayMode of ['separated', 'compact'] as const) {
|
|
|
|
|
const wrapper = await mountSizedChart(gridSeries(3), {
|
|
|
|
|
displayMode,
|
|
|
|
|
grid: { rowCount: 1, columnCount: 1 },
|
|
|
|
|
})
|
|
|
|
|
const initialEnd = wrapper.get('.waveform-chart__axis-endpoint--end').text()
|
|
|
|
|
await wrapper.get('.ant-pagination-next button').trigger('click')
|
|
|
|
|
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe(initialEnd)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('uses each track x domain in independent mode instead of the shared initial domain', async () => {
|
|
|
|
|
const wrapper = await mountSizedChart(
|
|
|
|
|
{
|
|
|
|
|
kind: 'series',
|
|
|
|
|
series: [
|
|
|
|
|
{
|
|
|
|
|
id: 'narrow-track',
|
|
|
|
|
name: '窄范围',
|
|
|
|
|
data: {
|
|
|
|
|
kind: 'points',
|
|
|
|
|
points: [
|
|
|
|
|
{ x: 0, y: 0 },
|
|
|
|
|
{ x: 0.006, y: 1 },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'wide-track',
|
|
|
|
|
name: '宽范围',
|
|
|
|
|
data: {
|
|
|
|
|
kind: 'points',
|
|
|
|
|
points: [
|
|
|
|
|
{ x: -8, y: 0 },
|
|
|
|
|
{ x: 5, y: 1 },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
displayMode: 'independent',
|
|
|
|
|
grid: { rowCount: 1, columnCount: 2 },
|
|
|
|
|
initialXDomain: [-8, 5],
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const tracks = wrapper.findAll('.waveform-chart__track')
|
|
|
|
|
expect(tracks[0]?.get('.waveform-chart__axis-endpoint--end').text()).toBe('6.00')
|
|
|
|
|
expect(tracks[1]?.get('.waveform-chart__axis-endpoint--end').text()).toBe('5.00')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('uses an explicit initial x domain override for an independent track', async () => {
|
|
|
|
|
const wrapper = await mountSizedChart(
|
|
|
|
|
{
|
|
|
|
|
kind: 'series',
|
|
|
|
|
series: [
|
|
|
|
|
{
|
|
|
|
|
id: 'narrow-track',
|
|
|
|
|
name: '窄范围',
|
|
|
|
|
data: {
|
|
|
|
|
kind: 'points',
|
|
|
|
|
points: [
|
|
|
|
|
{ x: 0, y: 0 },
|
|
|
|
|
{ x: 0.006, y: 1 },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
displayMode: 'independent',
|
|
|
|
|
initialXDomain: [-8, 5],
|
|
|
|
|
initialXDomains: { 'narrow-track': [0, 1] },
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1.00')
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-29 15:50:25 +08:00
|
|
|
it('reacts to exact fixed Y-domain props and returns to automatic bounds', async () => {
|
|
|
|
|
const wrapper = await mountSizedChart({
|
|
|
|
|
kind: 'points',
|
|
|
|
|
points: [
|
|
|
|
|
{ x: 0, y: 0 },
|
|
|
|
|
{ x: 1, y: 100 },
|
|
|
|
|
],
|
|
|
|
|
})
|
|
|
|
|
const yTickLabels = () =>
|
|
|
|
|
wrapper
|
|
|
|
|
.get('.waveform-chart__axis--y')
|
|
|
|
|
.findAll('.tick text')
|
|
|
|
|
.map((tick) => tick.text())
|
|
|
|
|
|
|
|
|
|
await wrapper.setProps({ yDomain: [3, 97] })
|
|
|
|
|
await flushPromises()
|
|
|
|
|
expect(yTickLabels()).toContain('3.00')
|
|
|
|
|
expect(yTickLabels()).toContain('97.00')
|
|
|
|
|
|
|
|
|
|
await wrapper.setProps({ yDomain: undefined })
|
|
|
|
|
await flushPromises()
|
|
|
|
|
expect(yTickLabels()).not.toContain('3.00')
|
|
|
|
|
expect(yTickLabels()).not.toContain('97.00')
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-27 15:05:13 +08:00
|
|
|
it('keeps annotations bound to their channel while paging', async () => {
|
|
|
|
|
const wrapper = await mountSizedChart(gridSeries(3), {
|
|
|
|
|
grid: { rowCount: 1, columnCount: 1 },
|
|
|
|
|
annotations: [
|
|
|
|
|
{ id: 'channel-2-note', seriesId: 'channel-1', x: 1, y: 2, text: '当前页标注' },
|
|
|
|
|
],
|
|
|
|
|
})
|
|
|
|
|
expect(wrapper.find('[data-annotation-id="channel-2-note"]').exists()).toBe(false)
|
|
|
|
|
await wrapper.get('.ant-pagination-next button').trigger('click')
|
|
|
|
|
expect(wrapper.find('[data-annotation-id="channel-2-note"]').exists()).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('renders a path for sample data and responds to width changes', async () => {
|
|
|
|
|
const wrapper = await mountSizedChart({
|
|
|
|
|
kind: 'samples',
|
|
|
|
|
values: [0, 1, -1, 0.5],
|
|
|
|
|
sampleRate: 4,
|
|
|
|
|
})
|
|
|
|
|
const initialPath = wrapper.get('.waveform-chart__line').attributes('d')
|
|
|
|
|
|
|
|
|
|
expect(initialPath).toContain('L')
|
|
|
|
|
expect(wrapper.get('.waveform-chart__svg').attributes('width')).toBe('800')
|
|
|
|
|
expect(wrapper.attributes('data-interaction-mode')).toBeUndefined()
|
|
|
|
|
|
|
|
|
|
resizeObservers.at(-1)?.resize(500, 360)
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
expect(wrapper.get('.waveform-chart__svg').attributes('width')).toBe('500')
|
|
|
|
|
expect(wrapper.get('.waveform-chart__line').attributes('d')).not.toBe(initialPath)
|
|
|
|
|
})
|
|
|
|
|
})
|