@363045841yyt/klinechart-core 0.7.3 → 0.7.5
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 +201 -201
- package/README.zh-CN.md +201 -201
- package/dist/controllers/index.d.ts +1 -0
- package/dist/controllers/index.d.ts.map +1 -1
- package/dist/controllers/index.js +1 -0
- package/dist/controllers/index.js.map +1 -1
- package/dist/engine/chart.d.ts +11 -19
- package/dist/engine/chart.d.ts.map +1 -1
- package/dist/engine/chart.js +92 -109
- package/dist/engine/chart.js.map +1 -1
- package/dist/engine/renderers/Indicator/indicatorData.d.ts +1 -0
- package/dist/engine/renderers/Indicator/indicatorData.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/indicatorData.js +1 -1
- package/dist/engine/renderers/Indicator/indicatorData.js.map +1 -1
- package/dist/engine/renderers/webgl/candleSurface.js +47 -47
- package/dist/engine/subPaneManager.d.ts +4 -0
- package/dist/engine/subPaneManager.d.ts.map +1 -1
- package/dist/engine/subPaneManager.js +13 -0
- package/dist/engine/subPaneManager.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -2
- package/dist/version.js.map +1 -1
- package/package.json +129 -122
- package/src/__tests__/signal.test.ts +124 -124
- package/src/config/chartSettings.ts +66 -66
- package/src/controllers/__tests__/drawing.test.ts +214 -214
- package/src/controllers/__tests__/indicatorSelector.test.ts +481 -481
- package/src/controllers/__tests__/toolbar.test.ts +225 -225
- package/src/controllers/createChartController.ts +665 -665
- package/src/controllers/createDrawingController.ts +96 -96
- package/src/controllers/createIndicatorSelectorController.ts +307 -307
- package/src/controllers/createToolbarController.ts +146 -146
- package/src/controllers/index.ts +20 -19
- package/src/controllers/types.ts +284 -284
- package/src/engine/__tests__/chart.dpr.test.ts +401 -401
- package/src/engine/__tests__/paneRenderer.resize.test.ts +92 -92
- package/src/engine/chart-store.ts +121 -121
- package/src/engine/chart.d.ts +617 -617
- package/src/engine/chart.ts +2803 -2815
- package/src/engine/controller/__tests__/interaction.dpr.test.ts +259 -259
- package/src/engine/controller/interaction.ts +722 -722
- package/src/engine/controller/markerInteraction.ts +130 -130
- package/src/engine/controller/pinchTracker.ts +82 -82
- package/src/engine/controller/tooltipPosition.ts +48 -48
- package/src/engine/draw/__tests__/pixelAlign.spec.ts +176 -176
- package/src/engine/draw/pixelAlign.ts +259 -259
- package/src/engine/drawing/index.ts +655 -655
- package/src/engine/drawing/interaction.ts +842 -842
- package/src/engine/drawing/plugin.ts +343 -343
- package/src/engine/indicators/__tests__/__fixtures__/golden/atr.json +38 -38
- package/src/engine/indicators/__tests__/__fixtures__/golden/dema.json +14 -14
- package/src/engine/indicators/__tests__/__fixtures__/golden/hma.json +14 -14
- package/src/engine/indicators/__tests__/__fixtures__/golden/index.ts +55 -55
- package/src/engine/indicators/__tests__/__fixtures__/golden/kama.json +14 -14
- package/src/engine/indicators/__tests__/__fixtures__/golden/tema.json +14 -14
- package/src/engine/indicators/__tests__/__fixtures__/golden/wma.json +40 -40
- package/src/engine/indicators/__tests__/__fixtures__/synthetic.ts +65 -65
- package/src/engine/indicators/__tests__/_propertyAssertions.ts +76 -76
- package/src/engine/indicators/__tests__/atr.test.ts +153 -153
- package/src/engine/indicators/__tests__/calculators.test.ts +614 -614
- package/src/engine/indicators/__tests__/cmf-mfi.test.ts +100 -100
- package/src/engine/indicators/__tests__/dema.test.ts +73 -73
- package/src/engine/indicators/__tests__/donchian.test.ts +70 -70
- package/src/engine/indicators/__tests__/hma.test.ts +73 -73
- package/src/engine/indicators/__tests__/ichimoku.test.ts +105 -105
- package/src/engine/indicators/__tests__/kama.test.ts +80 -80
- package/src/engine/indicators/__tests__/keltner.test.ts +65 -65
- package/src/engine/indicators/__tests__/pivot-fib.test.ts +110 -110
- package/src/engine/indicators/__tests__/roc.test.ts +68 -68
- package/src/engine/indicators/__tests__/sar.test.ts +86 -86
- package/src/engine/indicators/__tests__/scheduler.test.ts +831 -831
- package/src/engine/indicators/__tests__/soa.test.ts +533 -533
- package/src/engine/indicators/__tests__/structure.test.ts +110 -110
- package/src/engine/indicators/__tests__/supertrend.test.ts +65 -65
- package/src/engine/indicators/__tests__/tema.test.ts +68 -68
- package/src/engine/indicators/__tests__/trix.test.ts +70 -70
- package/src/engine/indicators/__tests__/volatility.test.ts +117 -117
- package/src/engine/indicators/__tests__/volume.test.ts +115 -115
- package/src/engine/indicators/__tests__/volumeProfile.test.ts +74 -74
- package/src/engine/indicators/__tests__/vwap.test.ts +69 -69
- package/src/engine/indicators/__tests__/wma.test.ts +112 -112
- package/src/engine/indicators/__tests__/zones.test.ts +95 -95
- package/src/engine/indicators/atrState.ts +27 -27
- package/src/engine/indicators/bollState.ts +51 -51
- package/src/engine/indicators/calculators.ts +2593 -2593
- package/src/engine/indicators/cciState.ts +25 -25
- package/src/engine/indicators/chaikinVolState.ts +32 -32
- package/src/engine/indicators/cmfState.ts +27 -27
- package/src/engine/indicators/demaState.ts +27 -27
- package/src/engine/indicators/donchianState.ts +43 -43
- package/src/engine/indicators/eneState.ts +43 -43
- package/src/engine/indicators/expmaState.ts +43 -43
- package/src/engine/indicators/fastkState.ts +25 -25
- package/src/engine/indicators/fibState.ts +41 -41
- package/src/engine/indicators/hmaState.ts +27 -27
- package/src/engine/indicators/hvState.ts +28 -28
- package/src/engine/indicators/ichimokuState.ts +70 -70
- package/src/engine/indicators/indicator.worker.ts +169 -169
- package/src/engine/indicators/indicatorDefinitionRegistry.ts +62 -62
- package/src/engine/indicators/indicatorMetadata.ts +110 -110
- package/src/engine/indicators/indicatorRegistry.ts +106 -106
- package/src/engine/indicators/indicatorRuntime.ts +1548 -1548
- package/src/engine/indicators/kamaState.ts +34 -34
- package/src/engine/indicators/keltnerState.ts +49 -49
- package/src/engine/indicators/kstState.ts +42 -42
- package/src/engine/indicators/maState.ts +36 -36
- package/src/engine/indicators/macdState.ts +76 -76
- package/src/engine/indicators/mfiState.ts +27 -27
- package/src/engine/indicators/momState.ts +25 -25
- package/src/engine/indicators/obvState.ts +25 -25
- package/src/engine/indicators/parkinsonState.ts +28 -28
- package/src/engine/indicators/pivotState.ts +51 -51
- package/src/engine/indicators/pvtState.ts +25 -25
- package/src/engine/indicators/rocState.ts +27 -27
- package/src/engine/indicators/rsiState.ts +65 -65
- package/src/engine/indicators/sarState.ts +41 -41
- package/src/engine/indicators/scheduler.ts +1205 -1205
- package/src/engine/indicators/soa.ts +352 -352
- package/src/engine/indicators/stateComposer.ts +1262 -1262
- package/src/engine/indicators/stochState.ts +26 -26
- package/src/engine/indicators/structureState.ts +69 -69
- package/src/engine/indicators/supertrendState.ts +37 -37
- package/src/engine/indicators/temaState.ts +27 -27
- package/src/engine/indicators/trixState.ts +35 -35
- package/src/engine/indicators/vmaState.ts +27 -27
- package/src/engine/indicators/volumeProfileState.ts +63 -63
- package/src/engine/indicators/vwapState.ts +29 -29
- package/src/engine/indicators/wmaState.ts +27 -27
- package/src/engine/indicators/wmsrState.ts +25 -25
- package/src/engine/indicators/workerProtocol.ts +613 -613
- package/src/engine/indicators/zonesState.ts +47 -47
- package/src/engine/layout/pane.ts +161 -161
- package/src/engine/marker/registry.ts +265 -265
- package/src/engine/paneRenderer.ts +169 -169
- package/src/engine/renderers/Indicator/atr.ts +237 -237
- package/src/engine/renderers/Indicator/boll.ts +317 -317
- package/src/engine/renderers/Indicator/cci.ts +275 -275
- package/src/engine/renderers/Indicator/chaikinVol.ts +138 -138
- package/src/engine/renderers/Indicator/cmf.ts +137 -137
- package/src/engine/renderers/Indicator/dema.ts +136 -136
- package/src/engine/renderers/Indicator/donchian.ts +137 -137
- package/src/engine/renderers/Indicator/ene.ts +271 -271
- package/src/engine/renderers/Indicator/expma.ts +197 -197
- package/src/engine/renderers/Indicator/fastk.ts +316 -316
- package/src/engine/renderers/Indicator/fib.ts +141 -141
- package/src/engine/renderers/Indicator/hma.ts +136 -136
- package/src/engine/renderers/Indicator/hv.ts +124 -124
- package/src/engine/renderers/Indicator/ichimoku.ts +181 -181
- package/src/engine/renderers/Indicator/index.ts +241 -241
- package/src/engine/renderers/Indicator/indicatorData.ts +650 -650
- package/src/engine/renderers/Indicator/kama.ts +136 -136
- package/src/engine/renderers/Indicator/keltner.ts +137 -137
- package/src/engine/renderers/Indicator/kst.ts +302 -302
- package/src/engine/renderers/Indicator/ma.ts +200 -200
- package/src/engine/renderers/Indicator/macd.ts +477 -477
- package/src/engine/renderers/Indicator/macdLegend.ts +141 -141
- package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +272 -272
- package/src/engine/renderers/Indicator/mfi.ts +142 -142
- package/src/engine/renderers/Indicator/mom.ts +311 -311
- package/src/engine/renderers/Indicator/obv.ts +123 -123
- package/src/engine/renderers/Indicator/parkinson.ts +124 -124
- package/src/engine/renderers/Indicator/pivot.ts +131 -131
- package/src/engine/renderers/Indicator/pvt.ts +123 -123
- package/src/engine/renderers/Indicator/roc.ts +143 -143
- package/src/engine/renderers/Indicator/rsi.ts +390 -390
- package/src/engine/renderers/Indicator/sar.ts +113 -113
- package/src/engine/renderers/Indicator/scale/atr_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/cci_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/fastk_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/indicator_scale.ts +204 -204
- package/src/engine/renderers/Indicator/scale/kst_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/macd_scale.ts +22 -22
- package/src/engine/renderers/Indicator/scale/mom_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/rsi_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/stoch_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/volume_scale.ts +26 -26
- package/src/engine/renderers/Indicator/scale/wmsr_scale.ts +19 -19
- package/src/engine/renderers/Indicator/stoch.ts +359 -359
- package/src/engine/renderers/Indicator/structure.ts +126 -126
- package/src/engine/renderers/Indicator/subPaneConfig.ts +265 -265
- package/src/engine/renderers/Indicator/supertrend.ts +115 -115
- package/src/engine/renderers/Indicator/tema.ts +136 -136
- package/src/engine/renderers/Indicator/trix.ts +158 -158
- package/src/engine/renderers/Indicator/vma.ts +124 -124
- package/src/engine/renderers/Indicator/volumeProfile.ts +125 -125
- package/src/engine/renderers/Indicator/vwap.ts +123 -123
- package/src/engine/renderers/Indicator/wma.ts +136 -136
- package/src/engine/renderers/Indicator/wmsr.ts +328 -328
- package/src/engine/renderers/Indicator/zones.ts +104 -104
- package/src/engine/renderers/__tests__/boll.renderer.test.ts +314 -314
- package/src/engine/renderers/__tests__/ene.renderer.test.ts +305 -305
- package/src/engine/renderers/__tests__/expma.renderer.test.ts +279 -279
- package/src/engine/renderers/__tests__/ma.renderer.test.ts +426 -426
- package/src/engine/renderers/__tests__/mainIndicatorLegend.renderer.test.ts +502 -502
- package/src/engine/renderers/__tests__/yAxis.renderer.test.ts +173 -173
- package/src/engine/renderers/candle.ts +459 -459
- package/src/engine/renderers/crosshair.ts +69 -69
- package/src/engine/renderers/customMarkers.ts +162 -162
- package/src/engine/renderers/extremaMarkers.ts +246 -246
- package/src/engine/renderers/gridLines.ts +90 -90
- package/src/engine/renderers/lastPrice.ts +97 -97
- package/src/engine/renderers/paneTitle.ts +136 -136
- package/src/engine/renderers/subVolume.ts +236 -236
- package/src/engine/renderers/timeAxis.ts +121 -121
- package/src/engine/renderers/webgl/candleSurface.ts +955 -955
- package/src/engine/renderers/webgl/sharedWebGLSurface.ts +146 -146
- package/src/engine/renderers/yAxis.ts +105 -105
- package/src/engine/scale/__tests__/logFormula.spec.ts +148 -148
- package/src/engine/scale/logFormula.ts +130 -130
- package/src/engine/scale/price.ts +39 -39
- package/src/engine/scale/priceScale.ts +264 -264
- package/src/engine/subPaneManager.ts +442 -427
- package/src/engine/theme/colors.ts +642 -642
- package/src/engine/theme/fonts.ts +20 -20
- package/src/engine/utils/klineConfig.ts +49 -49
- package/src/engine/utils/tickCount.ts +11 -11
- package/src/engine/utils/tickPosition.ts +214 -214
- package/src/engine/utils/zoom.ts +83 -83
- package/src/engine/viewport/viewport.ts +67 -67
- package/src/index.ts +3 -3
- package/src/plugin/ConfigManager.ts +93 -93
- package/src/plugin/EventBus.ts +77 -77
- package/src/plugin/HookSystem.ts +106 -106
- package/src/plugin/PluginHost.ts +243 -243
- package/src/plugin/PluginRegistry.ts +92 -92
- package/src/plugin/StateStore.ts +73 -73
- package/src/plugin/index.ts +19 -19
- package/src/plugin/rendererPluginManager.ts +368 -368
- package/src/plugin/stateKeys.ts +8 -8
- package/src/plugin/types.ts +526 -526
- package/src/reactivity/index.ts +2 -2
- package/src/reactivity/signal.ts +119 -119
- package/src/semantic/controller.ts +251 -251
- package/src/semantic/drawShape.ts +260 -260
- package/src/semantic/index.ts +28 -28
- package/src/semantic/schema.json +256 -256
- package/src/semantic/types.ts +251 -251
- package/src/semantic/validator.ts +349 -349
- package/src/types/kLine.ts +13 -13
- package/src/types/price.ts +56 -56
- package/src/types/volumePrice.ts +33 -33
- package/src/utils/dateFormat.ts +208 -208
- package/src/utils/kLineDraw/axis.ts +562 -562
- package/src/utils/priceToY.ts +34 -34
- package/src/utils/volumePrice.ts +202 -202
- package/src/version.ts +1 -1
|
@@ -1,426 +1,426 @@
|
|
|
1
|
-
// @ts-nocheck - Test file with intentional type relaxations for mocking
|
|
2
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
3
|
-
import { createMARendererPlugin } from '../Indicator/ma'
|
|
4
|
-
import { MA_STATE_KEY, type MARenderState } from '@/core/indicators/maState'
|
|
5
|
-
import { MA_COLORS } from '@/core/theme/colors'
|
|
6
|
-
import type { PluginHost, RenderContext, RendererPluginWithHost } from '@/plugin'
|
|
7
|
-
import type { KLineData } from '@/types/price'
|
|
8
|
-
import type { Pane } from '@/core/layout/pane'
|
|
9
|
-
|
|
10
|
-
// Type helper for tests - we know these methods exist on the implementation
|
|
11
|
-
interface TestableMARenderer extends RendererPluginWithHost {
|
|
12
|
-
draw: (context: RenderContext) => void
|
|
13
|
-
getConfig: () => Record<string, unknown>
|
|
14
|
-
setConfig: (config: Record<string, unknown>) => void
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* 创建 mock canvas context
|
|
19
|
-
*/
|
|
20
|
-
function createMockCanvasContext(): CanvasRenderingContext2D {
|
|
21
|
-
return {
|
|
22
|
-
save: vi.fn(),
|
|
23
|
-
restore: vi.fn(),
|
|
24
|
-
translate: vi.fn(),
|
|
25
|
-
beginPath: vi.fn(),
|
|
26
|
-
moveTo: vi.fn(),
|
|
27
|
-
lineTo: vi.fn(),
|
|
28
|
-
stroke: vi.fn(),
|
|
29
|
-
strokeStyle: '',
|
|
30
|
-
lineWidth: 0,
|
|
31
|
-
lineJoin: '',
|
|
32
|
-
lineCap: '',
|
|
33
|
-
} as unknown as CanvasRenderingContext2D
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* 创建 mock PluginHost
|
|
38
|
-
*/
|
|
39
|
-
function createMockPluginHost(state?: MARenderState): PluginHost {
|
|
40
|
-
return {
|
|
41
|
-
setSharedState: vi.fn(),
|
|
42
|
-
getSharedState: vi.fn(<T>(key: string): T | undefined => {
|
|
43
|
-
if (key === MA_STATE_KEY) {
|
|
44
|
-
return state as T
|
|
45
|
-
}
|
|
46
|
-
return undefined
|
|
47
|
-
}),
|
|
48
|
-
clearByOwner: vi.fn(),
|
|
49
|
-
registerService: vi.fn(),
|
|
50
|
-
getService: vi.fn(<T>(name: string) => {
|
|
51
|
-
if (name === 'indicatorScheduler') {
|
|
52
|
-
return {
|
|
53
|
-
getIndicatorMetadata: (indicatorName: string) => {
|
|
54
|
-
if (indicatorName === 'ma') {
|
|
55
|
-
return { name: 'ma', stateKey: MA_STATE_KEY }
|
|
56
|
-
}
|
|
57
|
-
return undefined
|
|
58
|
-
},
|
|
59
|
-
getAllIndicators: () => [],
|
|
60
|
-
} as T
|
|
61
|
-
}
|
|
62
|
-
return undefined
|
|
63
|
-
}),
|
|
64
|
-
getCanvas: vi.fn(),
|
|
65
|
-
getMainPane: vi.fn(),
|
|
66
|
-
getSubPane: vi.fn(),
|
|
67
|
-
getAllSubPanes: vi.fn(),
|
|
68
|
-
getTheme: vi.fn(),
|
|
69
|
-
getStyles: vi.fn(),
|
|
70
|
-
getBarStyles: vi.fn(),
|
|
71
|
-
getConfig: vi.fn(),
|
|
72
|
-
setConfig: vi.fn(),
|
|
73
|
-
on: vi.fn(),
|
|
74
|
-
off: vi.fn(),
|
|
75
|
-
once: vi.fn(),
|
|
76
|
-
emit: vi.fn(),
|
|
77
|
-
} as unknown as PluginHost
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* 创建 mock RenderContext
|
|
82
|
-
*/
|
|
83
|
-
function createMockRenderContext(
|
|
84
|
-
ctx: CanvasRenderingContext2D,
|
|
85
|
-
overrides: Partial<RenderContext> = {}
|
|
86
|
-
): RenderContext {
|
|
87
|
-
const mockPane = {
|
|
88
|
-
height: 200,
|
|
89
|
-
yAxis: {
|
|
90
|
-
priceToY: (price: number) => price * 10,
|
|
91
|
-
getDisplayRange: () => ({ minPrice: 0, maxPrice: 200 }),
|
|
92
|
-
getPriceOffset: () => 0,
|
|
93
|
-
getScaleType: () => 'linear',
|
|
94
|
-
},
|
|
95
|
-
} as unknown as Pane
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
ctx,
|
|
99
|
-
data: [] as KLineData[],
|
|
100
|
-
range: { start: 0, end: 10 },
|
|
101
|
-
visibleRange: { start: 0, end: 10 },
|
|
102
|
-
crosshair: null,
|
|
103
|
-
crosshairIndex: null,
|
|
104
|
-
dpr: 1,
|
|
105
|
-
scrollLeft: 0,
|
|
106
|
-
pane: mockPane,
|
|
107
|
-
kLineCenters: Array.from({ length: 10 }, (_, i) => i * 10 + 5),
|
|
108
|
-
...overrides,
|
|
109
|
-
} as RenderContext
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* 创建测试用的 MARenderState
|
|
114
|
-
*/
|
|
115
|
-
function createTestMARenderState(
|
|
116
|
-
overrides: Partial<MARenderState> = {}
|
|
117
|
-
): MARenderState {
|
|
118
|
-
return {
|
|
119
|
-
timestamp: Date.now(),
|
|
120
|
-
series: {
|
|
121
|
-
5: [undefined, undefined, undefined, undefined, 12, 13, 14, 15, 16, 17],
|
|
122
|
-
10: [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, 14.5],
|
|
123
|
-
},
|
|
124
|
-
enabledPeriods: [5, 10],
|
|
125
|
-
visibleMin: 12,
|
|
126
|
-
visibleMax: 17,
|
|
127
|
-
...overrides,
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
describe('createMARendererPlugin', () => {
|
|
132
|
-
it('should create a renderer plugin with correct metadata', () => {
|
|
133
|
-
const plugin = createMARendererPlugin()
|
|
134
|
-
|
|
135
|
-
expect(plugin.name).toBe('ma')
|
|
136
|
-
expect(plugin.version).toBe('2.1.0')
|
|
137
|
-
expect(plugin.description).toBe('MA均线渲染器')
|
|
138
|
-
expect(plugin.debugName).toBe('MA均线')
|
|
139
|
-
expect(plugin.paneId).toBe('main')
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
it('should have onInstall method', () => {
|
|
143
|
-
const plugin = createMARendererPlugin()
|
|
144
|
-
expect(typeof plugin.onInstall).toBe('function')
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('should declare MA_STATE_KEY namespace', () => {
|
|
148
|
-
const plugin = createMARendererPlugin()
|
|
149
|
-
plugin.onInstall(createMockPluginHost())
|
|
150
|
-
expect(plugin.getDeclaredNamespaces()).toEqual([MA_STATE_KEY])
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
it('should accept PluginHost via onInstall', () => {
|
|
154
|
-
const plugin = createMARendererPlugin()
|
|
155
|
-
const mockHost = createMockPluginHost()
|
|
156
|
-
|
|
157
|
-
expect(() => plugin.onInstall(mockHost)).not.toThrow()
|
|
158
|
-
})
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
describe('MA renderer draw', () => {
|
|
162
|
-
let ctx: CanvasRenderingContext2D
|
|
163
|
-
let plugin: TestableMARenderer
|
|
164
|
-
|
|
165
|
-
beforeEach(() => {
|
|
166
|
-
ctx = createMockCanvasContext()
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
it('should not draw when StateStore has no MA state', () => {
|
|
170
|
-
const mockHost = createMockPluginHost(undefined)
|
|
171
|
-
plugin = createMARendererPlugin() as TestableMARenderer
|
|
172
|
-
plugin.onInstall(mockHost)
|
|
173
|
-
|
|
174
|
-
const context = createMockRenderContext(ctx)
|
|
175
|
-
plugin.draw(context)
|
|
176
|
-
|
|
177
|
-
// Should not call any drawing methods
|
|
178
|
-
expect(ctx.beginPath).not.toHaveBeenCalled()
|
|
179
|
-
expect(ctx.stroke).not.toHaveBeenCalled()
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
it('should not draw when state has no valid data (visibleMin > visibleMax)', () => {
|
|
183
|
-
const state = createTestMARenderState({
|
|
184
|
-
visibleMin: Infinity,
|
|
185
|
-
visibleMax: -Infinity,
|
|
186
|
-
enabledPeriods: [],
|
|
187
|
-
})
|
|
188
|
-
const mockHost = createMockPluginHost(state)
|
|
189
|
-
plugin = createMARendererPlugin() as TestableMARenderer
|
|
190
|
-
plugin.onInstall(mockHost)
|
|
191
|
-
|
|
192
|
-
const context = createMockRenderContext(ctx)
|
|
193
|
-
plugin.draw(context)
|
|
194
|
-
|
|
195
|
-
expect(ctx.beginPath).not.toHaveBeenCalled()
|
|
196
|
-
expect(ctx.stroke).not.toHaveBeenCalled()
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
it('should not draw when no periods are enabled', () => {
|
|
200
|
-
const state = createTestMARenderState({
|
|
201
|
-
enabledPeriods: [],
|
|
202
|
-
})
|
|
203
|
-
const mockHost = createMockPluginHost(state)
|
|
204
|
-
plugin = createMARendererPlugin() as TestableMARenderer
|
|
205
|
-
plugin.onInstall(mockHost)
|
|
206
|
-
|
|
207
|
-
const context = createMockRenderContext(ctx)
|
|
208
|
-
plugin.draw(context)
|
|
209
|
-
|
|
210
|
-
expect(ctx.beginPath).not.toHaveBeenCalled()
|
|
211
|
-
expect(ctx.stroke).not.toHaveBeenCalled()
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
it('should save and restore context', () => {
|
|
215
|
-
const state = createTestMARenderState()
|
|
216
|
-
const mockHost = createMockPluginHost(state)
|
|
217
|
-
plugin = createMARendererPlugin() as TestableMARenderer
|
|
218
|
-
plugin.onInstall(mockHost)
|
|
219
|
-
|
|
220
|
-
const context = createMockRenderContext(ctx)
|
|
221
|
-
plugin.draw(context)
|
|
222
|
-
|
|
223
|
-
expect(ctx.save).toHaveBeenCalledTimes(1)
|
|
224
|
-
expect(ctx.restore).toHaveBeenCalledTimes(1)
|
|
225
|
-
expect(ctx.restore).toHaveBeenCalledAfter(ctx.save as ReturnType<typeof vi.fn>)
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
it('should translate context by -scrollLeft', () => {
|
|
229
|
-
const state = createTestMARenderState()
|
|
230
|
-
const mockHost = createMockPluginHost(state)
|
|
231
|
-
plugin = createMARendererPlugin() as TestableMARenderer
|
|
232
|
-
plugin.onInstall(mockHost)
|
|
233
|
-
|
|
234
|
-
const context = createMockRenderContext(ctx, { scrollLeft: 100 })
|
|
235
|
-
plugin.draw(context)
|
|
236
|
-
|
|
237
|
-
expect(ctx.translate).toHaveBeenCalledWith(-100, 0)
|
|
238
|
-
})
|
|
239
|
-
|
|
240
|
-
it('should set correct stroke style and line properties', () => {
|
|
241
|
-
const state = createTestMARenderState()
|
|
242
|
-
const mockHost = createMockPluginHost(state)
|
|
243
|
-
plugin = createMARendererPlugin() as TestableMARenderer
|
|
244
|
-
plugin.onInstall(mockHost)
|
|
245
|
-
|
|
246
|
-
const context = createMockRenderContext(ctx)
|
|
247
|
-
plugin.draw(context)
|
|
248
|
-
|
|
249
|
-
// Check that stroke was called (meaning line properties were set)
|
|
250
|
-
expect(ctx.stroke).toHaveBeenCalled()
|
|
251
|
-
expect(ctx.lineWidth).toBe(1)
|
|
252
|
-
expect(ctx.lineJoin).toBe('round')
|
|
253
|
-
expect(ctx.lineCap).toBe('round')
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
it('should draw lines for enabled periods', () => {
|
|
257
|
-
const state = createTestMARenderState({
|
|
258
|
-
series: {
|
|
259
|
-
5: [10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
|
|
260
|
-
},
|
|
261
|
-
enabledPeriods: [5],
|
|
262
|
-
visibleMin: 10,
|
|
263
|
-
visibleMax: 19,
|
|
264
|
-
})
|
|
265
|
-
const mockHost = createMockPluginHost(state)
|
|
266
|
-
plugin = createMARendererPlugin() as TestableMARenderer
|
|
267
|
-
plugin.onInstall(mockHost)
|
|
268
|
-
|
|
269
|
-
const context = createMockRenderContext(ctx, {
|
|
270
|
-
range: { start: 0, end: 10 },
|
|
271
|
-
kLineCenters: Array.from({ length: 10 }, (_, i) => i * 10 + 5),
|
|
272
|
-
})
|
|
273
|
-
plugin.draw(context)
|
|
274
|
-
|
|
275
|
-
expect(ctx.beginPath).toHaveBeenCalled()
|
|
276
|
-
expect(ctx.moveTo).toHaveBeenCalled()
|
|
277
|
-
expect(ctx.lineTo).toHaveBeenCalled()
|
|
278
|
-
expect(ctx.stroke).toHaveBeenCalled()
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
it('should skip undefined values in series', () => {
|
|
282
|
-
const state = createTestMARenderState({
|
|
283
|
-
series: {
|
|
284
|
-
5: [undefined, undefined, 12, 13, 14, 15, 16, 17, 18, 19],
|
|
285
|
-
},
|
|
286
|
-
enabledPeriods: [5],
|
|
287
|
-
})
|
|
288
|
-
const mockHost = createMockPluginHost(state)
|
|
289
|
-
plugin = createMARendererPlugin() as TestableMARenderer
|
|
290
|
-
plugin.onInstall(mockHost)
|
|
291
|
-
|
|
292
|
-
const context = createMockRenderContext(ctx, {
|
|
293
|
-
range: { start: 0, end: 10 },
|
|
294
|
-
kLineCenters: Array.from({ length: 10 }, (_, i) => i * 10 + 5),
|
|
295
|
-
})
|
|
296
|
-
plugin.draw(context)
|
|
297
|
-
|
|
298
|
-
// First valid value is at index 2 (value 12)
|
|
299
|
-
expect(ctx.moveTo).toHaveBeenCalled()
|
|
300
|
-
// moveTo should be called once for the first valid point
|
|
301
|
-
expect(ctx.moveTo).toHaveBeenCalledTimes(1)
|
|
302
|
-
})
|
|
303
|
-
|
|
304
|
-
it('should use correct colors for each period', () => {
|
|
305
|
-
const state = createTestMARenderState({
|
|
306
|
-
series: {
|
|
307
|
-
5: [10, 10, 10, 10, 10, 10, 10, 10, 10, 10],
|
|
308
|
-
10: [20, 20, 20, 20, 20, 20, 20, 20, 20, 20],
|
|
309
|
-
20: [30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
310
|
-
30: [40, 40, 40, 40, 40, 40, 40, 40, 40, 40],
|
|
311
|
-
60: [50, 50, 50, 50, 50, 50, 50, 50, 50, 50],
|
|
312
|
-
},
|
|
313
|
-
enabledPeriods: [5, 10, 20, 30, 60],
|
|
314
|
-
})
|
|
315
|
-
const mockHost = createMockPluginHost(state)
|
|
316
|
-
plugin = createMARendererPlugin() as TestableMARenderer
|
|
317
|
-
plugin.onInstall(mockHost)
|
|
318
|
-
|
|
319
|
-
const context = createMockRenderContext(ctx)
|
|
320
|
-
plugin.draw(context)
|
|
321
|
-
|
|
322
|
-
// Should have drawn 5 separate lines (one per period)
|
|
323
|
-
expect(ctx.stroke).toHaveBeenCalledTimes(5)
|
|
324
|
-
})
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
describe('MA renderer getConfig/setConfig', () => {
|
|
328
|
-
let plugin: ReturnType<typeof createMARendererPlugin>
|
|
329
|
-
|
|
330
|
-
it('getConfig should return enabled periods from StateStore', () => {
|
|
331
|
-
const state = createTestMARenderState({
|
|
332
|
-
enabledPeriods: [5, 20, 60],
|
|
333
|
-
})
|
|
334
|
-
const mockHost = createMockPluginHost(state)
|
|
335
|
-
plugin = createMARendererPlugin() as TestableMARenderer
|
|
336
|
-
plugin.onInstall(mockHost)
|
|
337
|
-
|
|
338
|
-
const config = plugin.getConfig()
|
|
339
|
-
|
|
340
|
-
expect(config).toEqual({
|
|
341
|
-
ma5: true,
|
|
342
|
-
ma20: true,
|
|
343
|
-
ma60: true,
|
|
344
|
-
})
|
|
345
|
-
expect(config.ma10).toBeUndefined()
|
|
346
|
-
expect(config.ma30).toBeUndefined()
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
it('getConfig should return empty object when no state', () => {
|
|
350
|
-
const mockHost = createMockPluginHost(undefined)
|
|
351
|
-
plugin = createMARendererPlugin() as TestableMARenderer
|
|
352
|
-
plugin.onInstall(mockHost)
|
|
353
|
-
|
|
354
|
-
const config = plugin.getConfig()
|
|
355
|
-
|
|
356
|
-
expect(config).toEqual({})
|
|
357
|
-
})
|
|
358
|
-
|
|
359
|
-
it('setConfig should not store config locally (stateless design)', () => {
|
|
360
|
-
const state = createTestMARenderState({
|
|
361
|
-
enabledPeriods: [5],
|
|
362
|
-
})
|
|
363
|
-
const mockHost = createMockPluginHost(state)
|
|
364
|
-
plugin = createMARendererPlugin() as TestableMARenderer
|
|
365
|
-
plugin.onInstall(mockHost)
|
|
366
|
-
|
|
367
|
-
// setConfig should be a no-op
|
|
368
|
-
expect(() => plugin.setConfig({ ma5: false, ma10: true })).not.toThrow()
|
|
369
|
-
|
|
370
|
-
// Config should still reflect StateStore state, not what we just set
|
|
371
|
-
const config = plugin.getConfig()
|
|
372
|
-
expect(config).toEqual({ ma5: true })
|
|
373
|
-
})
|
|
374
|
-
})
|
|
375
|
-
|
|
376
|
-
describe('MA renderer stateless design verification', () => {
|
|
377
|
-
it('should not have any internal caching', () => {
|
|
378
|
-
const plugin = createMARendererPlugin()
|
|
379
|
-
|
|
380
|
-
// Plugin should not expose any cache-related methods
|
|
381
|
-
expect('maCache' in plugin).toBe(false)
|
|
382
|
-
expect('cachedData' in plugin).toBe(false)
|
|
383
|
-
expect('getMAData' in plugin).toBe(false)
|
|
384
|
-
expect('onDataUpdate' in plugin).toBe(false)
|
|
385
|
-
})
|
|
386
|
-
|
|
387
|
-
it('should read fresh state on each draw call', () => {
|
|
388
|
-
const mockGetSharedState = vi.fn()
|
|
389
|
-
const mockHost = {
|
|
390
|
-
setSharedState: vi.fn(),
|
|
391
|
-
getSharedState: mockGetSharedState,
|
|
392
|
-
clearByOwner: vi.fn(),
|
|
393
|
-
registerService: vi.fn(),
|
|
394
|
-
getService: vi.fn(<T>(name: string) => {
|
|
395
|
-
if (name === 'indicatorScheduler') {
|
|
396
|
-
return {
|
|
397
|
-
getIndicatorMetadata: (indicatorName: string) => {
|
|
398
|
-
if (indicatorName === 'ma') {
|
|
399
|
-
return { name: 'ma', stateKey: MA_STATE_KEY }
|
|
400
|
-
}
|
|
401
|
-
return undefined
|
|
402
|
-
},
|
|
403
|
-
getAllIndicators: () => [],
|
|
404
|
-
} as T
|
|
405
|
-
}
|
|
406
|
-
return undefined
|
|
407
|
-
}),
|
|
408
|
-
} as unknown as PluginHost
|
|
409
|
-
|
|
410
|
-
mockGetSharedState.mockReturnValue(createTestMARenderState())
|
|
411
|
-
|
|
412
|
-
const plugin = createMARendererPlugin()
|
|
413
|
-
plugin.onInstall(mockHost)
|
|
414
|
-
|
|
415
|
-
const ctx = createMockCanvasContext()
|
|
416
|
-
const context = createMockRenderContext(ctx)
|
|
417
|
-
|
|
418
|
-
// First draw
|
|
419
|
-
plugin.draw(context)
|
|
420
|
-
expect(mockGetSharedState).toHaveBeenCalledTimes(1)
|
|
421
|
-
|
|
422
|
-
// Second draw - should read state again (not cached)
|
|
423
|
-
plugin.draw(context)
|
|
424
|
-
expect(mockGetSharedState).toHaveBeenCalledTimes(2)
|
|
425
|
-
})
|
|
426
|
-
})
|
|
1
|
+
// @ts-nocheck - Test file with intentional type relaxations for mocking
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
3
|
+
import { createMARendererPlugin } from '../Indicator/ma'
|
|
4
|
+
import { MA_STATE_KEY, type MARenderState } from '@/core/indicators/maState'
|
|
5
|
+
import { MA_COLORS } from '@/core/theme/colors'
|
|
6
|
+
import type { PluginHost, RenderContext, RendererPluginWithHost } from '@/plugin'
|
|
7
|
+
import type { KLineData } from '@/types/price'
|
|
8
|
+
import type { Pane } from '@/core/layout/pane'
|
|
9
|
+
|
|
10
|
+
// Type helper for tests - we know these methods exist on the implementation
|
|
11
|
+
interface TestableMARenderer extends RendererPluginWithHost {
|
|
12
|
+
draw: (context: RenderContext) => void
|
|
13
|
+
getConfig: () => Record<string, unknown>
|
|
14
|
+
setConfig: (config: Record<string, unknown>) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 创建 mock canvas context
|
|
19
|
+
*/
|
|
20
|
+
function createMockCanvasContext(): CanvasRenderingContext2D {
|
|
21
|
+
return {
|
|
22
|
+
save: vi.fn(),
|
|
23
|
+
restore: vi.fn(),
|
|
24
|
+
translate: vi.fn(),
|
|
25
|
+
beginPath: vi.fn(),
|
|
26
|
+
moveTo: vi.fn(),
|
|
27
|
+
lineTo: vi.fn(),
|
|
28
|
+
stroke: vi.fn(),
|
|
29
|
+
strokeStyle: '',
|
|
30
|
+
lineWidth: 0,
|
|
31
|
+
lineJoin: '',
|
|
32
|
+
lineCap: '',
|
|
33
|
+
} as unknown as CanvasRenderingContext2D
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 创建 mock PluginHost
|
|
38
|
+
*/
|
|
39
|
+
function createMockPluginHost(state?: MARenderState): PluginHost {
|
|
40
|
+
return {
|
|
41
|
+
setSharedState: vi.fn(),
|
|
42
|
+
getSharedState: vi.fn(<T>(key: string): T | undefined => {
|
|
43
|
+
if (key === MA_STATE_KEY) {
|
|
44
|
+
return state as T
|
|
45
|
+
}
|
|
46
|
+
return undefined
|
|
47
|
+
}),
|
|
48
|
+
clearByOwner: vi.fn(),
|
|
49
|
+
registerService: vi.fn(),
|
|
50
|
+
getService: vi.fn(<T>(name: string) => {
|
|
51
|
+
if (name === 'indicatorScheduler') {
|
|
52
|
+
return {
|
|
53
|
+
getIndicatorMetadata: (indicatorName: string) => {
|
|
54
|
+
if (indicatorName === 'ma') {
|
|
55
|
+
return { name: 'ma', stateKey: MA_STATE_KEY }
|
|
56
|
+
}
|
|
57
|
+
return undefined
|
|
58
|
+
},
|
|
59
|
+
getAllIndicators: () => [],
|
|
60
|
+
} as T
|
|
61
|
+
}
|
|
62
|
+
return undefined
|
|
63
|
+
}),
|
|
64
|
+
getCanvas: vi.fn(),
|
|
65
|
+
getMainPane: vi.fn(),
|
|
66
|
+
getSubPane: vi.fn(),
|
|
67
|
+
getAllSubPanes: vi.fn(),
|
|
68
|
+
getTheme: vi.fn(),
|
|
69
|
+
getStyles: vi.fn(),
|
|
70
|
+
getBarStyles: vi.fn(),
|
|
71
|
+
getConfig: vi.fn(),
|
|
72
|
+
setConfig: vi.fn(),
|
|
73
|
+
on: vi.fn(),
|
|
74
|
+
off: vi.fn(),
|
|
75
|
+
once: vi.fn(),
|
|
76
|
+
emit: vi.fn(),
|
|
77
|
+
} as unknown as PluginHost
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 创建 mock RenderContext
|
|
82
|
+
*/
|
|
83
|
+
function createMockRenderContext(
|
|
84
|
+
ctx: CanvasRenderingContext2D,
|
|
85
|
+
overrides: Partial<RenderContext> = {}
|
|
86
|
+
): RenderContext {
|
|
87
|
+
const mockPane = {
|
|
88
|
+
height: 200,
|
|
89
|
+
yAxis: {
|
|
90
|
+
priceToY: (price: number) => price * 10,
|
|
91
|
+
getDisplayRange: () => ({ minPrice: 0, maxPrice: 200 }),
|
|
92
|
+
getPriceOffset: () => 0,
|
|
93
|
+
getScaleType: () => 'linear',
|
|
94
|
+
},
|
|
95
|
+
} as unknown as Pane
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
ctx,
|
|
99
|
+
data: [] as KLineData[],
|
|
100
|
+
range: { start: 0, end: 10 },
|
|
101
|
+
visibleRange: { start: 0, end: 10 },
|
|
102
|
+
crosshair: null,
|
|
103
|
+
crosshairIndex: null,
|
|
104
|
+
dpr: 1,
|
|
105
|
+
scrollLeft: 0,
|
|
106
|
+
pane: mockPane,
|
|
107
|
+
kLineCenters: Array.from({ length: 10 }, (_, i) => i * 10 + 5),
|
|
108
|
+
...overrides,
|
|
109
|
+
} as RenderContext
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 创建测试用的 MARenderState
|
|
114
|
+
*/
|
|
115
|
+
function createTestMARenderState(
|
|
116
|
+
overrides: Partial<MARenderState> = {}
|
|
117
|
+
): MARenderState {
|
|
118
|
+
return {
|
|
119
|
+
timestamp: Date.now(),
|
|
120
|
+
series: {
|
|
121
|
+
5: [undefined, undefined, undefined, undefined, 12, 13, 14, 15, 16, 17],
|
|
122
|
+
10: [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, 14.5],
|
|
123
|
+
},
|
|
124
|
+
enabledPeriods: [5, 10],
|
|
125
|
+
visibleMin: 12,
|
|
126
|
+
visibleMax: 17,
|
|
127
|
+
...overrides,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
describe('createMARendererPlugin', () => {
|
|
132
|
+
it('should create a renderer plugin with correct metadata', () => {
|
|
133
|
+
const plugin = createMARendererPlugin()
|
|
134
|
+
|
|
135
|
+
expect(plugin.name).toBe('ma')
|
|
136
|
+
expect(plugin.version).toBe('2.1.0')
|
|
137
|
+
expect(plugin.description).toBe('MA均线渲染器')
|
|
138
|
+
expect(plugin.debugName).toBe('MA均线')
|
|
139
|
+
expect(plugin.paneId).toBe('main')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('should have onInstall method', () => {
|
|
143
|
+
const plugin = createMARendererPlugin()
|
|
144
|
+
expect(typeof plugin.onInstall).toBe('function')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('should declare MA_STATE_KEY namespace', () => {
|
|
148
|
+
const plugin = createMARendererPlugin()
|
|
149
|
+
plugin.onInstall(createMockPluginHost())
|
|
150
|
+
expect(plugin.getDeclaredNamespaces()).toEqual([MA_STATE_KEY])
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('should accept PluginHost via onInstall', () => {
|
|
154
|
+
const plugin = createMARendererPlugin()
|
|
155
|
+
const mockHost = createMockPluginHost()
|
|
156
|
+
|
|
157
|
+
expect(() => plugin.onInstall(mockHost)).not.toThrow()
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe('MA renderer draw', () => {
|
|
162
|
+
let ctx: CanvasRenderingContext2D
|
|
163
|
+
let plugin: TestableMARenderer
|
|
164
|
+
|
|
165
|
+
beforeEach(() => {
|
|
166
|
+
ctx = createMockCanvasContext()
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('should not draw when StateStore has no MA state', () => {
|
|
170
|
+
const mockHost = createMockPluginHost(undefined)
|
|
171
|
+
plugin = createMARendererPlugin() as TestableMARenderer
|
|
172
|
+
plugin.onInstall(mockHost)
|
|
173
|
+
|
|
174
|
+
const context = createMockRenderContext(ctx)
|
|
175
|
+
plugin.draw(context)
|
|
176
|
+
|
|
177
|
+
// Should not call any drawing methods
|
|
178
|
+
expect(ctx.beginPath).not.toHaveBeenCalled()
|
|
179
|
+
expect(ctx.stroke).not.toHaveBeenCalled()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('should not draw when state has no valid data (visibleMin > visibleMax)', () => {
|
|
183
|
+
const state = createTestMARenderState({
|
|
184
|
+
visibleMin: Infinity,
|
|
185
|
+
visibleMax: -Infinity,
|
|
186
|
+
enabledPeriods: [],
|
|
187
|
+
})
|
|
188
|
+
const mockHost = createMockPluginHost(state)
|
|
189
|
+
plugin = createMARendererPlugin() as TestableMARenderer
|
|
190
|
+
plugin.onInstall(mockHost)
|
|
191
|
+
|
|
192
|
+
const context = createMockRenderContext(ctx)
|
|
193
|
+
plugin.draw(context)
|
|
194
|
+
|
|
195
|
+
expect(ctx.beginPath).not.toHaveBeenCalled()
|
|
196
|
+
expect(ctx.stroke).not.toHaveBeenCalled()
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('should not draw when no periods are enabled', () => {
|
|
200
|
+
const state = createTestMARenderState({
|
|
201
|
+
enabledPeriods: [],
|
|
202
|
+
})
|
|
203
|
+
const mockHost = createMockPluginHost(state)
|
|
204
|
+
plugin = createMARendererPlugin() as TestableMARenderer
|
|
205
|
+
plugin.onInstall(mockHost)
|
|
206
|
+
|
|
207
|
+
const context = createMockRenderContext(ctx)
|
|
208
|
+
plugin.draw(context)
|
|
209
|
+
|
|
210
|
+
expect(ctx.beginPath).not.toHaveBeenCalled()
|
|
211
|
+
expect(ctx.stroke).not.toHaveBeenCalled()
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('should save and restore context', () => {
|
|
215
|
+
const state = createTestMARenderState()
|
|
216
|
+
const mockHost = createMockPluginHost(state)
|
|
217
|
+
plugin = createMARendererPlugin() as TestableMARenderer
|
|
218
|
+
plugin.onInstall(mockHost)
|
|
219
|
+
|
|
220
|
+
const context = createMockRenderContext(ctx)
|
|
221
|
+
plugin.draw(context)
|
|
222
|
+
|
|
223
|
+
expect(ctx.save).toHaveBeenCalledTimes(1)
|
|
224
|
+
expect(ctx.restore).toHaveBeenCalledTimes(1)
|
|
225
|
+
expect(ctx.restore).toHaveBeenCalledAfter(ctx.save as ReturnType<typeof vi.fn>)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('should translate context by -scrollLeft', () => {
|
|
229
|
+
const state = createTestMARenderState()
|
|
230
|
+
const mockHost = createMockPluginHost(state)
|
|
231
|
+
plugin = createMARendererPlugin() as TestableMARenderer
|
|
232
|
+
plugin.onInstall(mockHost)
|
|
233
|
+
|
|
234
|
+
const context = createMockRenderContext(ctx, { scrollLeft: 100 })
|
|
235
|
+
plugin.draw(context)
|
|
236
|
+
|
|
237
|
+
expect(ctx.translate).toHaveBeenCalledWith(-100, 0)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('should set correct stroke style and line properties', () => {
|
|
241
|
+
const state = createTestMARenderState()
|
|
242
|
+
const mockHost = createMockPluginHost(state)
|
|
243
|
+
plugin = createMARendererPlugin() as TestableMARenderer
|
|
244
|
+
plugin.onInstall(mockHost)
|
|
245
|
+
|
|
246
|
+
const context = createMockRenderContext(ctx)
|
|
247
|
+
plugin.draw(context)
|
|
248
|
+
|
|
249
|
+
// Check that stroke was called (meaning line properties were set)
|
|
250
|
+
expect(ctx.stroke).toHaveBeenCalled()
|
|
251
|
+
expect(ctx.lineWidth).toBe(1)
|
|
252
|
+
expect(ctx.lineJoin).toBe('round')
|
|
253
|
+
expect(ctx.lineCap).toBe('round')
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('should draw lines for enabled periods', () => {
|
|
257
|
+
const state = createTestMARenderState({
|
|
258
|
+
series: {
|
|
259
|
+
5: [10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
|
|
260
|
+
},
|
|
261
|
+
enabledPeriods: [5],
|
|
262
|
+
visibleMin: 10,
|
|
263
|
+
visibleMax: 19,
|
|
264
|
+
})
|
|
265
|
+
const mockHost = createMockPluginHost(state)
|
|
266
|
+
plugin = createMARendererPlugin() as TestableMARenderer
|
|
267
|
+
plugin.onInstall(mockHost)
|
|
268
|
+
|
|
269
|
+
const context = createMockRenderContext(ctx, {
|
|
270
|
+
range: { start: 0, end: 10 },
|
|
271
|
+
kLineCenters: Array.from({ length: 10 }, (_, i) => i * 10 + 5),
|
|
272
|
+
})
|
|
273
|
+
plugin.draw(context)
|
|
274
|
+
|
|
275
|
+
expect(ctx.beginPath).toHaveBeenCalled()
|
|
276
|
+
expect(ctx.moveTo).toHaveBeenCalled()
|
|
277
|
+
expect(ctx.lineTo).toHaveBeenCalled()
|
|
278
|
+
expect(ctx.stroke).toHaveBeenCalled()
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('should skip undefined values in series', () => {
|
|
282
|
+
const state = createTestMARenderState({
|
|
283
|
+
series: {
|
|
284
|
+
5: [undefined, undefined, 12, 13, 14, 15, 16, 17, 18, 19],
|
|
285
|
+
},
|
|
286
|
+
enabledPeriods: [5],
|
|
287
|
+
})
|
|
288
|
+
const mockHost = createMockPluginHost(state)
|
|
289
|
+
plugin = createMARendererPlugin() as TestableMARenderer
|
|
290
|
+
plugin.onInstall(mockHost)
|
|
291
|
+
|
|
292
|
+
const context = createMockRenderContext(ctx, {
|
|
293
|
+
range: { start: 0, end: 10 },
|
|
294
|
+
kLineCenters: Array.from({ length: 10 }, (_, i) => i * 10 + 5),
|
|
295
|
+
})
|
|
296
|
+
plugin.draw(context)
|
|
297
|
+
|
|
298
|
+
// First valid value is at index 2 (value 12)
|
|
299
|
+
expect(ctx.moveTo).toHaveBeenCalled()
|
|
300
|
+
// moveTo should be called once for the first valid point
|
|
301
|
+
expect(ctx.moveTo).toHaveBeenCalledTimes(1)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('should use correct colors for each period', () => {
|
|
305
|
+
const state = createTestMARenderState({
|
|
306
|
+
series: {
|
|
307
|
+
5: [10, 10, 10, 10, 10, 10, 10, 10, 10, 10],
|
|
308
|
+
10: [20, 20, 20, 20, 20, 20, 20, 20, 20, 20],
|
|
309
|
+
20: [30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
310
|
+
30: [40, 40, 40, 40, 40, 40, 40, 40, 40, 40],
|
|
311
|
+
60: [50, 50, 50, 50, 50, 50, 50, 50, 50, 50],
|
|
312
|
+
},
|
|
313
|
+
enabledPeriods: [5, 10, 20, 30, 60],
|
|
314
|
+
})
|
|
315
|
+
const mockHost = createMockPluginHost(state)
|
|
316
|
+
plugin = createMARendererPlugin() as TestableMARenderer
|
|
317
|
+
plugin.onInstall(mockHost)
|
|
318
|
+
|
|
319
|
+
const context = createMockRenderContext(ctx)
|
|
320
|
+
plugin.draw(context)
|
|
321
|
+
|
|
322
|
+
// Should have drawn 5 separate lines (one per period)
|
|
323
|
+
expect(ctx.stroke).toHaveBeenCalledTimes(5)
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
describe('MA renderer getConfig/setConfig', () => {
|
|
328
|
+
let plugin: ReturnType<typeof createMARendererPlugin>
|
|
329
|
+
|
|
330
|
+
it('getConfig should return enabled periods from StateStore', () => {
|
|
331
|
+
const state = createTestMARenderState({
|
|
332
|
+
enabledPeriods: [5, 20, 60],
|
|
333
|
+
})
|
|
334
|
+
const mockHost = createMockPluginHost(state)
|
|
335
|
+
plugin = createMARendererPlugin() as TestableMARenderer
|
|
336
|
+
plugin.onInstall(mockHost)
|
|
337
|
+
|
|
338
|
+
const config = plugin.getConfig()
|
|
339
|
+
|
|
340
|
+
expect(config).toEqual({
|
|
341
|
+
ma5: true,
|
|
342
|
+
ma20: true,
|
|
343
|
+
ma60: true,
|
|
344
|
+
})
|
|
345
|
+
expect(config.ma10).toBeUndefined()
|
|
346
|
+
expect(config.ma30).toBeUndefined()
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('getConfig should return empty object when no state', () => {
|
|
350
|
+
const mockHost = createMockPluginHost(undefined)
|
|
351
|
+
plugin = createMARendererPlugin() as TestableMARenderer
|
|
352
|
+
plugin.onInstall(mockHost)
|
|
353
|
+
|
|
354
|
+
const config = plugin.getConfig()
|
|
355
|
+
|
|
356
|
+
expect(config).toEqual({})
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it('setConfig should not store config locally (stateless design)', () => {
|
|
360
|
+
const state = createTestMARenderState({
|
|
361
|
+
enabledPeriods: [5],
|
|
362
|
+
})
|
|
363
|
+
const mockHost = createMockPluginHost(state)
|
|
364
|
+
plugin = createMARendererPlugin() as TestableMARenderer
|
|
365
|
+
plugin.onInstall(mockHost)
|
|
366
|
+
|
|
367
|
+
// setConfig should be a no-op
|
|
368
|
+
expect(() => plugin.setConfig({ ma5: false, ma10: true })).not.toThrow()
|
|
369
|
+
|
|
370
|
+
// Config should still reflect StateStore state, not what we just set
|
|
371
|
+
const config = plugin.getConfig()
|
|
372
|
+
expect(config).toEqual({ ma5: true })
|
|
373
|
+
})
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
describe('MA renderer stateless design verification', () => {
|
|
377
|
+
it('should not have any internal caching', () => {
|
|
378
|
+
const plugin = createMARendererPlugin()
|
|
379
|
+
|
|
380
|
+
// Plugin should not expose any cache-related methods
|
|
381
|
+
expect('maCache' in plugin).toBe(false)
|
|
382
|
+
expect('cachedData' in plugin).toBe(false)
|
|
383
|
+
expect('getMAData' in plugin).toBe(false)
|
|
384
|
+
expect('onDataUpdate' in plugin).toBe(false)
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('should read fresh state on each draw call', () => {
|
|
388
|
+
const mockGetSharedState = vi.fn()
|
|
389
|
+
const mockHost = {
|
|
390
|
+
setSharedState: vi.fn(),
|
|
391
|
+
getSharedState: mockGetSharedState,
|
|
392
|
+
clearByOwner: vi.fn(),
|
|
393
|
+
registerService: vi.fn(),
|
|
394
|
+
getService: vi.fn(<T>(name: string) => {
|
|
395
|
+
if (name === 'indicatorScheduler') {
|
|
396
|
+
return {
|
|
397
|
+
getIndicatorMetadata: (indicatorName: string) => {
|
|
398
|
+
if (indicatorName === 'ma') {
|
|
399
|
+
return { name: 'ma', stateKey: MA_STATE_KEY }
|
|
400
|
+
}
|
|
401
|
+
return undefined
|
|
402
|
+
},
|
|
403
|
+
getAllIndicators: () => [],
|
|
404
|
+
} as T
|
|
405
|
+
}
|
|
406
|
+
return undefined
|
|
407
|
+
}),
|
|
408
|
+
} as unknown as PluginHost
|
|
409
|
+
|
|
410
|
+
mockGetSharedState.mockReturnValue(createTestMARenderState())
|
|
411
|
+
|
|
412
|
+
const plugin = createMARendererPlugin()
|
|
413
|
+
plugin.onInstall(mockHost)
|
|
414
|
+
|
|
415
|
+
const ctx = createMockCanvasContext()
|
|
416
|
+
const context = createMockRenderContext(ctx)
|
|
417
|
+
|
|
418
|
+
// First draw
|
|
419
|
+
plugin.draw(context)
|
|
420
|
+
expect(mockGetSharedState).toHaveBeenCalledTimes(1)
|
|
421
|
+
|
|
422
|
+
// Second draw - should read state again (not cached)
|
|
423
|
+
plugin.draw(context)
|
|
424
|
+
expect(mockGetSharedState).toHaveBeenCalledTimes(2)
|
|
425
|
+
})
|
|
426
|
+
})
|