waveform-analysis 0.1.63 → 0.1.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -5
- package/dist/index.cjs +1 -1
- package/dist/index.js +986 -964
- package/dist/style.css +1 -1
- package/dist/types/components/core/constants.d.ts +1 -1
- package/dist/types/components/core/trackLayoutBuilder.d.ts +1 -0
- package/dist/types/components/core/yAxisConstants.d.ts +1 -1
- package/dist/types/types/chart.d.ts +6 -1
- package/package.json +1 -1
- package/src/App.vue +2 -2
- package/src/components/WaveformChart.css +4 -0
- package/src/components/WaveformChart.vue +2 -2
- package/src/components/core/constants.ts +3 -3
- package/src/components/core/layout.ts +1 -1
- package/src/components/core/trackLayoutBuilder.ts +42 -1
- package/src/components/core/useWaveformLayout.ts +2 -1
- package/src/components/core/useWaveformPresentation.ts +1 -1
- package/src/components/core/xDomain.ts +26 -2
- package/src/components/core/yAxisConstants.ts +1 -1
- package/src/components/rendering/WaveformLegend.vue +1 -1
- package/src/components/rendering/WaveformSeriesLayer.vue +1 -1
- package/src/components/rendering/WaveformTrack.css +5 -2
- package/src/components/rendering/WaveformTrack.vue +7 -1
- package/src/components/rendering/WaveformTrackAxes.vue +8 -2
- package/src/demo/DemoChartHost.vue +1 -0
- package/src/types/chart.ts +6 -1
- package/src/App.test.ts +0 -400
- package/src/components/annotation/components.test.ts +0 -372
- package/src/components/annotation/markup.test.ts +0 -371
- package/src/components/annotation/serialization.test.ts +0 -114
- package/src/components/core/constants.test.ts +0 -29
- package/src/components/core/fixedYDomainLayout.test.ts +0 -204
- package/src/components/core/grid.test.ts +0 -197
- package/src/components/core/latestTaskScheduler.test.ts +0 -51
- package/src/components/core/layout.test.ts +0 -373
- package/src/components/core/title.test.ts +0 -76
- package/src/components/core/useWaveformData.test.ts +0 -95
- package/src/components/core/useWaveformRenderSampling.test.ts +0 -17
- package/src/components/core/xDomain.test.ts +0 -28
- package/src/components/core/yAxisNice.test.ts +0 -174
- package/src/components/core/yAxisSlotLayout.test.ts +0 -165
- package/src/components/core/yDomain.test.ts +0 -56
- package/src/components/interaction/WaveformTooltip.test.ts +0 -276
- package/src/components/interaction/viewportInteractionState.test.ts +0 -101
- package/src/components/utils/useAnimationFrameThrottle.test.ts +0 -53
- package/src/components/waveformChartCases/annotationEditingAndReprojection.test.ts +0 -257
- package/src/components/waveformChartCases/compactAndSharedHover.test.ts +0 -358
- package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +0 -358
- package/src/components/waveformChartCases/dataLifecycle.test.ts +0 -310
- package/src/components/waveformChartCases/frameGridAndFormatting.test.ts +0 -389
- package/src/components/waveformChartCases/gridFill.test.ts +0 -95
- package/src/components/waveformChartCases/incompleteDataHover.test.ts +0 -155
- package/src/components/waveformChartCases/interactionAndAnnotationCreation.test.ts +0 -328
- package/src/components/waveformChartCases/layoutAndDomains.test.ts +0 -371
- package/src/components/waveformChartCases/legendVisibility.test.ts +0 -350
- package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +0 -400
- package/src/components/waveformChartCases/multiAxisLabelAlignment.test.ts +0 -61
- package/src/components/waveformChartCases/presentationAndAxes.test.ts +0 -345
- package/src/components/waveformChartCases/presentationMode.test.ts +0 -327
- package/src/components/waveformChartCases/renderingStyles.test.ts +0 -283
- package/src/components/waveformChartCases/sizingAndTitles.test.ts +0 -370
- package/src/components/waveformChartCases/trackOrder.test.ts +0 -323
- package/src/components/waveformChartCases/viewportBoxAndPan.test.ts +0 -365
- package/src/components/waveformChartCases/viewportConstraintsAndReset.test.ts +0 -311
- package/src/components/waveformChartCases/viewportLifecycle.test.ts +0 -132
- package/src/components/waveformChartCases/viewportReset.test.ts +0 -251
- package/src/components/waveformChartCases/wheelZoomOut.test.ts +0 -114
- package/src/components/waveformChartCases/workerSamplingIntegration.test.ts +0 -117
- package/src/components/waveformChartCases/xDomainStrategy.test.ts +0 -168
- package/src/components/waveformChartCases/yAxisLayout.test.ts +0 -103
- package/src/components/waveformChartCases/zoomIntentAndDataRebind.test.ts +0 -213
- package/src/components/waveformChartCases/zoomPointConstraints.test.ts +0 -308
- package/src/components/waveformLegendGeometry.test.ts +0 -21
- package/src/components/waveformNormalization.test.ts +0 -170
- package/src/core/data.test.ts +0 -241
- package/src/core/rendering.test.ts +0 -267
- package/src/core/renderingStrategies.test.ts +0 -90
- package/src/core/samplingReference.test.ts +0 -124
- package/src/core/wasmSampling.test.ts +0 -205
- package/src/core/workerSampling/multiresolution.test.ts +0 -69
- package/src/core/workerSampling/protocol.test.ts +0 -34
- package/src/core/workerSampling/repository.test.ts +0 -400
- package/src/core/workerSampling/repositoryCache.test.ts +0 -163
- package/src/core/workerSampling/repositorySampling.test.ts +0 -106
- package/src/core/workerSampling/repositoryTypedDataset.test.ts +0 -89
- package/src/core/workerSampling/repositoryWasmDataset.test.ts +0 -70
- package/src/data/simulatedWaveforms.test.ts +0 -60
- package/src/demo/plotMarginControls.test.ts +0 -29
- package/src/demo/xAxisLabelControls.test.ts +0 -86
- package/src/test/setup.ts +0 -82
- package/src/test/waveformChart.ts +0 -75
- package/src/utils/domain.test.ts +0 -28
- package/src/utils/formatters.test.ts +0 -131
- package/src/utils/sampling.test.ts +0 -226
- package/src/views/FixedYDomainDemo.test.ts +0 -44
- package/src/views/WasmSamplingDemo.test.ts +0 -13
- package/src/views/wasmSamplingDemoSampling.test.ts +0 -27
|
@@ -1,358 +0,0 @@
|
|
|
1
|
-
import { flushPromises, mount } from '@vue/test-utils'
|
|
2
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
-
|
|
4
|
-
import { flushAnimationFrames, pendingAnimationFrameCount } from '../../test/setup'
|
|
5
|
-
import WaveformChart from '../WaveformChart.vue'
|
|
6
|
-
|
|
7
|
-
import { gridSeries, mountSizedChart } from '../../test/waveformChart'
|
|
8
|
-
|
|
9
|
-
describe('WaveformChart', () => {
|
|
10
|
-
it('suppresses native context menus across the waveform component', async () => {
|
|
11
|
-
const wrapper = await mountSizedChart(gridSeries(2), {
|
|
12
|
-
title: { text: '波形标题' },
|
|
13
|
-
grid: { rowCount: 1, columnCount: 1, showPagination: true },
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
for (const selector of [
|
|
17
|
-
'.waveform-chart__title-area',
|
|
18
|
-
'.waveform-chart__grid',
|
|
19
|
-
'.waveform-chart__overlay',
|
|
20
|
-
'.waveform-chart__pagination',
|
|
21
|
-
]) {
|
|
22
|
-
const event = new MouseEvent('contextmenu', { bubbles: true, cancelable: true })
|
|
23
|
-
const dispatched = wrapper.get(selector).element.dispatchEvent(event)
|
|
24
|
-
|
|
25
|
-
expect(dispatched).toBe(false)
|
|
26
|
-
expect(event.defaultPrevented).toBe(true)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const sharedWrapper = await mountSizedChart(
|
|
30
|
-
{
|
|
31
|
-
kind: 'points',
|
|
32
|
-
points: [
|
|
33
|
-
{ x: 0, y: 0 },
|
|
34
|
-
{ x: 1, y: 1 },
|
|
35
|
-
],
|
|
36
|
-
},
|
|
37
|
-
{ displayMode: 'separated' },
|
|
38
|
-
)
|
|
39
|
-
const sharedEvent = new MouseEvent('contextmenu', { bubbles: true, cancelable: true })
|
|
40
|
-
const sharedDispatched = sharedWrapper
|
|
41
|
-
.get('.waveform-chart__overlay--shared')
|
|
42
|
-
.element.dispatchEvent(sharedEvent)
|
|
43
|
-
|
|
44
|
-
expect(sharedDispatched).toBe(false)
|
|
45
|
-
expect(sharedEvent.defaultPrevented).toBe(true)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('preserves native context menus for editable controls', async () => {
|
|
49
|
-
const wrapper = await mountSizedChart({ kind: 'samples', values: [0, 1], sampleRate: 1 })
|
|
50
|
-
const editableElements = [
|
|
51
|
-
document.createElement('input'),
|
|
52
|
-
document.createElement('textarea'),
|
|
53
|
-
document.createElement('div'),
|
|
54
|
-
]
|
|
55
|
-
editableElements[2]?.setAttribute('contenteditable', 'true')
|
|
56
|
-
|
|
57
|
-
for (const element of editableElements) {
|
|
58
|
-
wrapper.element.appendChild(element)
|
|
59
|
-
const event = new MouseEvent('contextmenu', { bubbles: true, cancelable: true })
|
|
60
|
-
const dispatched = element.dispatchEvent(event)
|
|
61
|
-
|
|
62
|
-
expect(dispatched).toBe(true)
|
|
63
|
-
expect(event.defaultPrevented).toBe(false)
|
|
64
|
-
}
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('applies size fallbacks for minimum, negative, and non-finite values', async () => {
|
|
68
|
-
const minimumWrapper = mount(WaveformChart, {
|
|
69
|
-
props: {
|
|
70
|
-
data: { kind: 'samples', values: [0, 1], sampleRate: 1 },
|
|
71
|
-
width: -20,
|
|
72
|
-
height: -20,
|
|
73
|
-
},
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
expect(minimumWrapper.attributes('style')).toContain('width: 0px')
|
|
77
|
-
expect(minimumWrapper.attributes('style')).toContain('height: 180px')
|
|
78
|
-
expect(minimumWrapper.get('.waveform-chart__svg').attributes('height')).toBe('180')
|
|
79
|
-
|
|
80
|
-
const adaptiveWrapper = mount(WaveformChart, {
|
|
81
|
-
props: {
|
|
82
|
-
data: { kind: 'samples', values: [0, 1], sampleRate: 1 },
|
|
83
|
-
width: Number.POSITIVE_INFINITY,
|
|
84
|
-
height: Number.NaN,
|
|
85
|
-
},
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
expect(adaptiveWrapper.attributes('style')).toContain('width: 100%')
|
|
89
|
-
expect(adaptiveWrapper.attributes('style')).toContain('height: 100%')
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it('keeps a 100k-point SVG path bounded by the plot width', async () => {
|
|
93
|
-
const sourcePoints = Array.from({ length: 100_000 }, (_, index) => ({
|
|
94
|
-
x: index / 1_000,
|
|
95
|
-
y: index === 50_001 ? 100 : Math.sin(index / 50),
|
|
96
|
-
}))
|
|
97
|
-
const wrapper = await mountSizedChart(
|
|
98
|
-
{ kind: 'points', points: sourcePoints },
|
|
99
|
-
{ rendering: { downsampleThreshold: 1_000, maxPointsPerPixel: 4 } },
|
|
100
|
-
)
|
|
101
|
-
const overlayWidth = Number(wrapper.get('.waveform-chart__overlay').attributes('width'))
|
|
102
|
-
await vi.waitFor(
|
|
103
|
-
() => {
|
|
104
|
-
expect(wrapper.emitted('sampling-complete')?.length).toBeGreaterThan(0)
|
|
105
|
-
const path = wrapper.get('.waveform-chart__line').attributes('d') ?? ''
|
|
106
|
-
const renderedPointCount = path.match(/[ML]/g)?.length ?? 0
|
|
107
|
-
expect(renderedPointCount).toBeGreaterThan(0)
|
|
108
|
-
expect(renderedPointCount).toBeLessThanOrEqual(Math.floor(overlayWidth * 4) + 2)
|
|
109
|
-
expect(path).toContain(',0')
|
|
110
|
-
},
|
|
111
|
-
{ timeout: 5_000 },
|
|
112
|
-
)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('bounds dense decorations by pixel spacing while keeping one SVG path per series', async () => {
|
|
116
|
-
const sourcePoints = Array.from({ length: 1_000 }, (_, index) => ({
|
|
117
|
-
x: index,
|
|
118
|
-
y: Math.sin(index / 20),
|
|
119
|
-
error: 0.1,
|
|
120
|
-
}))
|
|
121
|
-
const wrapper = await mountSizedChart(
|
|
122
|
-
{
|
|
123
|
-
kind: 'series',
|
|
124
|
-
series: [
|
|
125
|
-
{
|
|
126
|
-
id: 'dense-decorations',
|
|
127
|
-
name: '密集标记',
|
|
128
|
-
pointType: 'triangle',
|
|
129
|
-
errorBar: { visible: true },
|
|
130
|
-
data: { kind: 'points', points: sourcePoints },
|
|
131
|
-
},
|
|
132
|
-
],
|
|
133
|
-
},
|
|
134
|
-
{ rendering: { pointMinSpacing: 10, errorBarMinSpacing: 12 } },
|
|
135
|
-
)
|
|
136
|
-
const overlayWidth = Number(wrapper.get('.waveform-chart__overlay').attributes('width'))
|
|
137
|
-
const pointPaths = wrapper.findAll('.waveform-chart__point')
|
|
138
|
-
const errorBarPaths = wrapper.findAll('.waveform-chart__error-bar')
|
|
139
|
-
const pointCount = pointPaths[0]?.attributes('d')?.match(/M/g)?.length ?? 0
|
|
140
|
-
const errorBarCount = (errorBarPaths[0]?.attributes('d')?.match(/M/g)?.length ?? 0) / 3
|
|
141
|
-
|
|
142
|
-
expect(pointPaths).toHaveLength(1)
|
|
143
|
-
expect(errorBarPaths).toHaveLength(1)
|
|
144
|
-
expect(pointCount).toBeLessThanOrEqual(Math.ceil(overlayWidth / 10) + 2)
|
|
145
|
-
expect(errorBarCount).toBeLessThanOrEqual(Math.ceil(overlayWidth / 12) + 2)
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
it('renders explicit points and supports a single point', async () => {
|
|
149
|
-
const wrapper = await mountSizedChart({ kind: 'points', points: [{ x: 3, y: 8 }] })
|
|
150
|
-
|
|
151
|
-
expect(wrapper.get('.waveform-chart__line').attributes('d')).toContain('M')
|
|
152
|
-
expect(wrapper.find('.waveform-chart__empty').exists()).toBe(false)
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
it('shows an empty state for empty and invalid data', async () => {
|
|
156
|
-
const wrapper = await mountSizedChart({ kind: 'samples', values: [1], sampleRate: -1 })
|
|
157
|
-
|
|
158
|
-
expect(wrapper.get('.waveform-chart__empty').text()).toContain('暂无有效波形数据')
|
|
159
|
-
expect(wrapper.find('.waveform-chart__line').exists()).toBe(false)
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
it('emits the nearest point on hover and clears it on leave', async () => {
|
|
163
|
-
const wrapper = await mountSizedChart(
|
|
164
|
-
{
|
|
165
|
-
kind: 'points',
|
|
166
|
-
points: [
|
|
167
|
-
{ x: 0, y: 0 },
|
|
168
|
-
{ x: 1, y: 5 },
|
|
169
|
-
],
|
|
170
|
-
},
|
|
171
|
-
{ grid: { rowCount: 1, columnCount: 1 } },
|
|
172
|
-
)
|
|
173
|
-
const overlay = wrapper.get('.waveform-chart__overlay')
|
|
174
|
-
const overlayWidth = Number(overlay.attributes('width'))
|
|
175
|
-
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
|
|
176
|
-
value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
overlay.element.dispatchEvent(
|
|
180
|
-
new MouseEvent('pointermove', { clientX: 700, clientY: 120, bubbles: true }),
|
|
181
|
-
)
|
|
182
|
-
flushAnimationFrames()
|
|
183
|
-
await flushPromises()
|
|
184
|
-
expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([{ x: 1, y: 5 }])
|
|
185
|
-
expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(true)
|
|
186
|
-
expect(wrapper.get('.waveform-tooltip__series-label').text()).toContain('未配置炮号:')
|
|
187
|
-
const crosshairLines = wrapper.findAll('.waveform-chart__crosshair line')
|
|
188
|
-
expect(crosshairLines).toHaveLength(1)
|
|
189
|
-
expect(crosshairLines[0].attributes('x1')).toBe(crosshairLines[0].attributes('x2'))
|
|
190
|
-
expect(crosshairLines[0].attributes('y1')).toBe('0')
|
|
191
|
-
expect(wrapper.find('.waveform-chart__crosshair circle').exists()).toBe(false)
|
|
192
|
-
|
|
193
|
-
await overlay.trigger('pointerleave')
|
|
194
|
-
expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([null])
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
it('clears hover when the pointer leaves the chart and restores it only after a new plot move', async () => {
|
|
198
|
-
const wrapper = await mountSizedChart(
|
|
199
|
-
{
|
|
200
|
-
kind: 'points',
|
|
201
|
-
points: [
|
|
202
|
-
{ x: 0, y: 0 },
|
|
203
|
-
{ x: 1, y: 5 },
|
|
204
|
-
],
|
|
205
|
-
},
|
|
206
|
-
{ grid: { rowCount: 1, columnCount: 1 } },
|
|
207
|
-
)
|
|
208
|
-
const overlay = wrapper.get('.waveform-chart__overlay')
|
|
209
|
-
const overlayWidth = Number(overlay.attributes('width'))
|
|
210
|
-
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
|
|
211
|
-
value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
overlay.element.dispatchEvent(
|
|
215
|
-
new MouseEvent('pointermove', { clientX: overlayWidth, clientY: 120, bubbles: true }),
|
|
216
|
-
)
|
|
217
|
-
flushAnimationFrames()
|
|
218
|
-
await flushPromises()
|
|
219
|
-
expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(true)
|
|
220
|
-
|
|
221
|
-
await wrapper.trigger('pointerleave')
|
|
222
|
-
expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(false)
|
|
223
|
-
expect(wrapper.find('.waveform-chart__crosshair').exists()).toBe(false)
|
|
224
|
-
expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([null])
|
|
225
|
-
|
|
226
|
-
await wrapper.trigger('pointerenter')
|
|
227
|
-
expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(false)
|
|
228
|
-
|
|
229
|
-
overlay.element.dispatchEvent(
|
|
230
|
-
new MouseEvent('pointermove', { clientX: 0, clientY: 120, bubbles: true }),
|
|
231
|
-
)
|
|
232
|
-
flushAnimationFrames()
|
|
233
|
-
await flushPromises()
|
|
234
|
-
expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(true)
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
it('hides the numeric tooltip and crosshair when showTooltip is disabled', async () => {
|
|
238
|
-
const wrapper = await mountSizedChart(
|
|
239
|
-
{
|
|
240
|
-
kind: 'points',
|
|
241
|
-
points: [
|
|
242
|
-
{ x: 0, y: 0 },
|
|
243
|
-
{ x: 1, y: 5 },
|
|
244
|
-
],
|
|
245
|
-
},
|
|
246
|
-
{ grid: { rowCount: 1, columnCount: 1 }, showTooltip: false },
|
|
247
|
-
)
|
|
248
|
-
const overlay = wrapper.get('.waveform-chart__overlay')
|
|
249
|
-
const overlayWidth = Number(overlay.attributes('width'))
|
|
250
|
-
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
|
|
251
|
-
value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
overlay.element.dispatchEvent(
|
|
255
|
-
new MouseEvent('pointermove', { clientX: 700, clientY: 120, bubbles: true }),
|
|
256
|
-
)
|
|
257
|
-
flushAnimationFrames()
|
|
258
|
-
await flushPromises()
|
|
259
|
-
|
|
260
|
-
expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(false)
|
|
261
|
-
expect(wrapper.find('.waveform-chart__crosshair').exists()).toBe(false)
|
|
262
|
-
|
|
263
|
-
await wrapper.setProps({ showTooltip: true })
|
|
264
|
-
await flushPromises()
|
|
265
|
-
expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(true)
|
|
266
|
-
expect(wrapper.find('.waveform-chart__crosshair').exists()).toBe(true)
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
it('coalesces pointer moves per frame and cancels pending hover work', async () => {
|
|
270
|
-
const wrapper = await mountSizedChart(
|
|
271
|
-
{
|
|
272
|
-
kind: 'points',
|
|
273
|
-
points: [
|
|
274
|
-
{ x: 0, y: 0 },
|
|
275
|
-
{ x: 1, y: 5 },
|
|
276
|
-
],
|
|
277
|
-
},
|
|
278
|
-
{ grid: { rowCount: 1, columnCount: 1 } },
|
|
279
|
-
)
|
|
280
|
-
const overlay = wrapper.get('.waveform-chart__overlay')
|
|
281
|
-
const overlayWidth = Number(overlay.attributes('width'))
|
|
282
|
-
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
|
|
283
|
-
value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
|
|
284
|
-
})
|
|
285
|
-
const emittedBeforeMove = wrapper.emitted('point-hover')?.length ?? 0
|
|
286
|
-
|
|
287
|
-
overlay.element.dispatchEvent(
|
|
288
|
-
new MouseEvent('pointermove', { clientX: 0, clientY: 100, bubbles: true }),
|
|
289
|
-
)
|
|
290
|
-
overlay.element.dispatchEvent(
|
|
291
|
-
new MouseEvent('pointermove', { clientX: overlayWidth, clientY: 120, bubbles: true }),
|
|
292
|
-
)
|
|
293
|
-
|
|
294
|
-
expect(pendingAnimationFrameCount()).toBe(1)
|
|
295
|
-
expect(wrapper.emitted('point-hover')?.length ?? 0).toBe(emittedBeforeMove)
|
|
296
|
-
flushAnimationFrames()
|
|
297
|
-
await flushPromises()
|
|
298
|
-
expect(wrapper.emitted('point-hover')).toHaveLength(emittedBeforeMove + 1)
|
|
299
|
-
expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([{ x: 1, y: 5 }])
|
|
300
|
-
|
|
301
|
-
overlay.element.dispatchEvent(
|
|
302
|
-
new MouseEvent('pointermove', { clientX: 0, clientY: 100, bubbles: true }),
|
|
303
|
-
)
|
|
304
|
-
expect(pendingAnimationFrameCount()).toBe(1)
|
|
305
|
-
await overlay.trigger('pointerleave')
|
|
306
|
-
const emittedAfterLeave = wrapper.emitted('point-hover')?.length ?? 0
|
|
307
|
-
expect(pendingAnimationFrameCount()).toBe(0)
|
|
308
|
-
flushAnimationFrames()
|
|
309
|
-
await flushPromises()
|
|
310
|
-
expect(wrapper.emitted('point-hover')).toHaveLength(emittedAfterLeave)
|
|
311
|
-
expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([null])
|
|
312
|
-
|
|
313
|
-
overlay.element.dispatchEvent(
|
|
314
|
-
new MouseEvent('pointermove', { clientX: overlayWidth, clientY: 100, bubbles: true }),
|
|
315
|
-
)
|
|
316
|
-
expect(pendingAnimationFrameCount()).toBe(1)
|
|
317
|
-
wrapper.unmount()
|
|
318
|
-
expect(pendingAnimationFrameCount()).toBe(0)
|
|
319
|
-
})
|
|
320
|
-
|
|
321
|
-
it('isolates hover rendering from the chart, track, and waveform path subtrees', async () => {
|
|
322
|
-
const wrapper = await mountSizedChart(
|
|
323
|
-
{
|
|
324
|
-
kind: 'points',
|
|
325
|
-
points: [
|
|
326
|
-
{ x: 0, y: 0 },
|
|
327
|
-
{ x: 1, y: 5 },
|
|
328
|
-
],
|
|
329
|
-
},
|
|
330
|
-
{ grid: { rowCount: 1, columnCount: 1 } },
|
|
331
|
-
)
|
|
332
|
-
const overlay = wrapper.get('.waveform-chart__overlay')
|
|
333
|
-
const overlayWidth = Number(overlay.attributes('width'))
|
|
334
|
-
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
|
|
335
|
-
value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
const track = wrapper.getComponent({ name: 'WaveformTrack' })
|
|
339
|
-
const seriesLayer = wrapper.getComponent({ name: 'WaveformSeriesLayer' })
|
|
340
|
-
const chartUpdate = vi.spyOn(wrapper.vm.$, 'update')
|
|
341
|
-
const trackUpdate = vi.spyOn(track.vm.$, 'update')
|
|
342
|
-
const seriesLayerUpdate = vi.spyOn(seriesLayer.vm.$, 'update')
|
|
343
|
-
const pathBeforeHover = wrapper.get('.waveform-chart__line').element
|
|
344
|
-
|
|
345
|
-
overlay.element.dispatchEvent(
|
|
346
|
-
new MouseEvent('pointermove', { clientX: overlayWidth, clientY: 120, bubbles: true }),
|
|
347
|
-
)
|
|
348
|
-
flushAnimationFrames()
|
|
349
|
-
await flushPromises()
|
|
350
|
-
|
|
351
|
-
expect(wrapper.get('.waveform-tooltip__series-label').text()).toContain('未配置炮号:')
|
|
352
|
-
expect(wrapper.findAll('.waveform-chart__crosshair line')).toHaveLength(1)
|
|
353
|
-
expect(wrapper.get('.waveform-chart__line').element).toBe(pathBeforeHover)
|
|
354
|
-
expect(chartUpdate).not.toHaveBeenCalled()
|
|
355
|
-
expect(trackUpdate).not.toHaveBeenCalled()
|
|
356
|
-
expect(seriesLayerUpdate).not.toHaveBeenCalled()
|
|
357
|
-
})
|
|
358
|
-
})
|
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
import { flushPromises } from '@vue/test-utils'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
|
|
4
|
-
import { type WaveformData } from '../waveform'
|
|
5
|
-
|
|
6
|
-
import { gridSeries, mountSizedChart } from '../../test/waveformChart'
|
|
7
|
-
|
|
8
|
-
describe('WaveformChart', () => {
|
|
9
|
-
it('keeps other independent tracks unchanged when one data window is replaced', async () => {
|
|
10
|
-
const wrapper = await mountSizedChart(gridSeries(2), {
|
|
11
|
-
displayMode: 'independent',
|
|
12
|
-
grid: { rowCount: 1, columnCount: 2 },
|
|
13
|
-
})
|
|
14
|
-
const originalEndpoints = wrapper
|
|
15
|
-
.findAll('.waveform-chart__axis-endpoint--end')
|
|
16
|
-
.map((endpoint) => endpoint.text())
|
|
17
|
-
|
|
18
|
-
await wrapper.setProps({
|
|
19
|
-
data: {
|
|
20
|
-
kind: 'series',
|
|
21
|
-
series: [
|
|
22
|
-
{
|
|
23
|
-
id: 'channel-0',
|
|
24
|
-
name: '通道 1',
|
|
25
|
-
data: {
|
|
26
|
-
kind: 'points',
|
|
27
|
-
points: [
|
|
28
|
-
{ x: 0.25, y: 0 },
|
|
29
|
-
{ x: 0.75, y: 1 },
|
|
30
|
-
],
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
id: 'channel-1',
|
|
35
|
-
name: '通道 2',
|
|
36
|
-
data: {
|
|
37
|
-
kind: 'points',
|
|
38
|
-
points: [
|
|
39
|
-
{ x: 0, y: 1 },
|
|
40
|
-
{ x: 1, y: 2 },
|
|
41
|
-
],
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
],
|
|
45
|
-
},
|
|
46
|
-
})
|
|
47
|
-
await flushPromises()
|
|
48
|
-
|
|
49
|
-
const nextEndpoints = wrapper
|
|
50
|
-
.findAll('.waveform-chart__axis-endpoint--end')
|
|
51
|
-
.map((endpoint) => endpoint.text())
|
|
52
|
-
expect(nextEndpoints[0]).not.toBe(originalEndpoints[0])
|
|
53
|
-
expect(nextEndpoints[1]).toBe(originalEndpoints[1])
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('rebuilds cached domains only when the data reference changes', async () => {
|
|
57
|
-
const firstData: WaveformData = {
|
|
58
|
-
kind: 'points',
|
|
59
|
-
points: [
|
|
60
|
-
{ x: 0, y: 0 },
|
|
61
|
-
{ x: 1, y: 1 },
|
|
62
|
-
],
|
|
63
|
-
}
|
|
64
|
-
const wrapper = await mountSizedChart(firstData)
|
|
65
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
|
|
66
|
-
|
|
67
|
-
firstData.points.push({ x: 2, y: 2 })
|
|
68
|
-
await flushPromises()
|
|
69
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
|
|
70
|
-
|
|
71
|
-
await wrapper.setProps({ data: { ...firstData, points: [...firstData.points] } })
|
|
72
|
-
await flushPromises()
|
|
73
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2000')
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('keeps controlled annotations when replacing the loaded data window', async () => {
|
|
77
|
-
const annotations = [
|
|
78
|
-
{ id: 'window-note', seriesId: 'series-0', x: 0.5, y: 0.5, text: '窗口标注' },
|
|
79
|
-
]
|
|
80
|
-
const wrapper = await mountSizedChart(
|
|
81
|
-
{
|
|
82
|
-
kind: 'points',
|
|
83
|
-
points: [
|
|
84
|
-
{ x: 0, y: 0 },
|
|
85
|
-
{ x: 1, y: 1 },
|
|
86
|
-
],
|
|
87
|
-
},
|
|
88
|
-
{ annotations },
|
|
89
|
-
)
|
|
90
|
-
expect(wrapper.find('[data-annotation-id="window-note"]').exists()).toBe(true)
|
|
91
|
-
|
|
92
|
-
await wrapper.setProps({
|
|
93
|
-
data: {
|
|
94
|
-
kind: 'points',
|
|
95
|
-
points: [
|
|
96
|
-
{ x: 2, y: 0 },
|
|
97
|
-
{ x: 3, y: 1 },
|
|
98
|
-
],
|
|
99
|
-
},
|
|
100
|
-
})
|
|
101
|
-
await flushPromises()
|
|
102
|
-
|
|
103
|
-
expect(wrapper.find('[data-annotation-id="window-note"]').exists()).toBe(false)
|
|
104
|
-
expect(annotations).toEqual([
|
|
105
|
-
{ id: 'window-note', seriesId: 'series-0', x: 0.5, y: 0.5, text: '窗口标注' },
|
|
106
|
-
])
|
|
107
|
-
|
|
108
|
-
await wrapper.setProps({
|
|
109
|
-
data: {
|
|
110
|
-
kind: 'points',
|
|
111
|
-
points: [
|
|
112
|
-
{ x: 0, y: 0 },
|
|
113
|
-
{ x: 1, y: 1 },
|
|
114
|
-
],
|
|
115
|
-
},
|
|
116
|
-
})
|
|
117
|
-
await flushPromises()
|
|
118
|
-
expect(wrapper.find('[data-annotation-id="window-note"]').exists()).toBe(true)
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('renders named multi-channel paths as independent tracks by default', async () => {
|
|
122
|
-
const wrapper = await mountSizedChart({
|
|
123
|
-
kind: 'series',
|
|
124
|
-
series: [
|
|
125
|
-
{
|
|
126
|
-
name: 'BT2_2M',
|
|
127
|
-
unit: 'T',
|
|
128
|
-
data: {
|
|
129
|
-
kind: 'points',
|
|
130
|
-
points: [
|
|
131
|
-
{ x: 0, y: 0 },
|
|
132
|
-
{ x: 1, y: 2 },
|
|
133
|
-
],
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
{
|
|
137
|
-
name: 'BT1_2M',
|
|
138
|
-
unit: 'T',
|
|
139
|
-
data: {
|
|
140
|
-
kind: 'points',
|
|
141
|
-
points: [
|
|
142
|
-
{ x: 0, y: -1 },
|
|
143
|
-
{ x: 2, y: 3 },
|
|
144
|
-
],
|
|
145
|
-
},
|
|
146
|
-
},
|
|
147
|
-
],
|
|
148
|
-
})
|
|
149
|
-
const paths = wrapper.findAll('.waveform-chart__line')
|
|
150
|
-
|
|
151
|
-
expect(paths).toHaveLength(2)
|
|
152
|
-
expect(wrapper.findAll('.waveform-chart__svg')).toHaveLength(1)
|
|
153
|
-
expect(wrapper.findAll('.waveform-chart__track')).toHaveLength(2)
|
|
154
|
-
expect(wrapper.findAll('.waveform-chart__axis--x')).toHaveLength(2)
|
|
155
|
-
expect(wrapper.findAll('.waveform-chart__overlay--independent')).toHaveLength(2)
|
|
156
|
-
expect(paths[0].attributes('stroke')).toBe('#0960bd')
|
|
157
|
-
expect(paths[1].attributes('stroke')).toBe('#ff7f0e')
|
|
158
|
-
expect(paths[0].attributes('data-series-name')).toBe('BT2_2M')
|
|
159
|
-
const yAxisLabels = wrapper.findAll('.waveform-chart__y-axis-label')
|
|
160
|
-
expect(yAxisLabels.map((label) => label.text())).toEqual(['BT2_2M', 'BT1_2M'])
|
|
161
|
-
expect(
|
|
162
|
-
wrapper
|
|
163
|
-
.findAll('.waveform-chart__axis--y .tick text')
|
|
164
|
-
.map((label) => label.text())
|
|
165
|
-
.some((label) => label.includes('(T)')),
|
|
166
|
-
).toBe(true)
|
|
167
|
-
expect(yAxisLabels.map((label) => label.attributes('fill'))).toEqual(['#0960bd', '#ff7f0e'])
|
|
168
|
-
const labelX = Number(
|
|
169
|
-
yAxisLabels[0].attributes('transform')?.match(/^translate\(([-\d.]+),/)?.[1],
|
|
170
|
-
)
|
|
171
|
-
expect(labelX).toBeLessThan(-46)
|
|
172
|
-
expect(Number(wrapper.get('.waveform-chart__y-axis-label-bg').attributes('x'))).toBe(labelX - 6)
|
|
173
|
-
expect(yAxisLabels.every((label) => !label.text().includes('(T)'))).toBe(true)
|
|
174
|
-
expect(wrapper.findAll('.waveform-chart__track-label')).toHaveLength(0)
|
|
175
|
-
expect(
|
|
176
|
-
wrapper.findAll('.waveform-chart__axis-endpoint--end').map((item) => item.text()),
|
|
177
|
-
).toEqual(['1000', '2000'])
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
it('keeps the zero Y-axis label on upper compact tracks', async () => {
|
|
181
|
-
const wrapper = await mountSizedChart(
|
|
182
|
-
{
|
|
183
|
-
kind: 'series',
|
|
184
|
-
series: [
|
|
185
|
-
{
|
|
186
|
-
name: 'first',
|
|
187
|
-
data: {
|
|
188
|
-
kind: 'points',
|
|
189
|
-
points: [
|
|
190
|
-
{ x: 0, y: 0 },
|
|
191
|
-
{ x: 1, y: 1 },
|
|
192
|
-
],
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
{
|
|
196
|
-
name: 'second',
|
|
197
|
-
data: {
|
|
198
|
-
kind: 'points',
|
|
199
|
-
points: [
|
|
200
|
-
{ x: 0, y: -1 },
|
|
201
|
-
{ x: 1, y: 2 },
|
|
202
|
-
],
|
|
203
|
-
},
|
|
204
|
-
},
|
|
205
|
-
],
|
|
206
|
-
},
|
|
207
|
-
{ displayMode: 'compact' },
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
const tracks = wrapper.findAll('.waveform-chart__track')
|
|
211
|
-
const firstAxisTicks = tracks[0].findAll('.waveform-chart__axis--y .tick')
|
|
212
|
-
const zeroTicks = firstAxisTicks.filter((tick) => Number(tick.text()) === 0)
|
|
213
|
-
const firstTrackHeight = Number(tracks[0].attributes('data-track-height'))
|
|
214
|
-
const zeroTickY = Number(
|
|
215
|
-
zeroTicks[0].attributes('transform')?.match(/translate\(0,\s*([\d.]+)\)/)?.[1],
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
expect(zeroTicks).toHaveLength(1)
|
|
219
|
-
expect(Math.abs(zeroTickY - firstTrackHeight)).toBeLessThanOrEqual(1)
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
it.each(['independent', 'separated', 'compact'] as const)(
|
|
223
|
-
'shows a non-zero Y-axis start value once in %s mode',
|
|
224
|
-
async (displayMode) => {
|
|
225
|
-
const wrapper = await mountSizedChart(
|
|
226
|
-
{
|
|
227
|
-
kind: 'series',
|
|
228
|
-
series: [
|
|
229
|
-
{
|
|
230
|
-
name: 'first',
|
|
231
|
-
data: {
|
|
232
|
-
kind: 'points',
|
|
233
|
-
points: [
|
|
234
|
-
{ x: 0, y: 0.11 },
|
|
235
|
-
{ x: 1, y: 0.89 },
|
|
236
|
-
],
|
|
237
|
-
},
|
|
238
|
-
},
|
|
239
|
-
{
|
|
240
|
-
name: 'second',
|
|
241
|
-
data: {
|
|
242
|
-
kind: 'points',
|
|
243
|
-
points: [
|
|
244
|
-
{ x: 0, y: 4 },
|
|
245
|
-
{ x: 1, y: 5 },
|
|
246
|
-
],
|
|
247
|
-
},
|
|
248
|
-
},
|
|
249
|
-
],
|
|
250
|
-
},
|
|
251
|
-
{ displayMode },
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
const firstTrack = wrapper.findAll('.waveform-chart__track')[0]
|
|
255
|
-
const firstTrackHeight = Number(firstTrack.attributes('data-track-height'))
|
|
256
|
-
const startTicks = firstTrack.findAll('.waveform-chart__axis--y .tick').filter((tick) => {
|
|
257
|
-
const match = tick.attributes('transform')?.match(/translate\(0,\s*([\d.]+)\)/)
|
|
258
|
-
return match ? Math.abs(Number(match[1]) - firstTrackHeight) <= 1 : false
|
|
259
|
-
})
|
|
260
|
-
|
|
261
|
-
expect(startTicks).toHaveLength(1)
|
|
262
|
-
expect(Number(startTicks[0].text())).toBe(0)
|
|
263
|
-
},
|
|
264
|
-
)
|
|
265
|
-
|
|
266
|
-
it.each(['independent', 'separated'] as const)(
|
|
267
|
-
'shows the Y-axis end value once on every track in %s mode',
|
|
268
|
-
async (displayMode) => {
|
|
269
|
-
const wrapper = await mountSizedChart(
|
|
270
|
-
{
|
|
271
|
-
kind: 'series',
|
|
272
|
-
series: [
|
|
273
|
-
{
|
|
274
|
-
name: 'first',
|
|
275
|
-
data: {
|
|
276
|
-
kind: 'points',
|
|
277
|
-
points: [
|
|
278
|
-
{ x: 0, y: 0.11 },
|
|
279
|
-
{ x: 1, y: 0.89 },
|
|
280
|
-
],
|
|
281
|
-
},
|
|
282
|
-
},
|
|
283
|
-
{
|
|
284
|
-
name: 'second',
|
|
285
|
-
data: {
|
|
286
|
-
kind: 'points',
|
|
287
|
-
points: [
|
|
288
|
-
{ x: 0, y: 4.11 },
|
|
289
|
-
{ x: 1, y: 4.89 },
|
|
290
|
-
],
|
|
291
|
-
},
|
|
292
|
-
},
|
|
293
|
-
],
|
|
294
|
-
},
|
|
295
|
-
{ displayMode },
|
|
296
|
-
)
|
|
297
|
-
|
|
298
|
-
const expectedEndValues = [1, 5]
|
|
299
|
-
wrapper.findAll('.waveform-chart__track').forEach((track, index) => {
|
|
300
|
-
const endTicks = track.findAll('.waveform-chart__axis--y .tick').filter((tick) => {
|
|
301
|
-
const match = tick.attributes('transform')?.match(/translate\(0,\s*([\d.]+)\)/)
|
|
302
|
-
return match ? Math.abs(Number(match[1])) <= 1 : false
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
expect(endTicks).toHaveLength(1)
|
|
306
|
-
expect(Number(endTicks[0].text())).toBe(expectedEndValues[index])
|
|
307
|
-
})
|
|
308
|
-
},
|
|
309
|
-
)
|
|
310
|
-
})
|