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') }) 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') }) 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) }) })