useChartHoverState.test.tsx350 lines · main
1import { act, renderHook } from '@testing-library/react'
2import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3
4// Mock localStorage
5const localStorageMock = (() => {
6 let store: Record<string, string> = {}
7
8 return {
9 getItem: vi.fn((key: string) => store[key] || null),
10 setItem: vi.fn((key: string, value: string) => {
11 store[key] = value
12 }),
13 clear: vi.fn(() => {
14 store = {}
15 }),
16 }
17})()
18
19Object.defineProperty(window, 'localStorage', {
20 value: localStorageMock,
21})
22
23const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
24
25describe('useChartHoverState', () => {
26 let useChartHoverState: any
27
28 beforeEach(async () => {
29 localStorageMock.clear()
30 consoleWarnSpy.mockClear()
31
32 vi.resetModules()
33 const { useChartHoverState: importedHook } = await import('./useChartHoverState')
34 useChartHoverState = importedHook
35 })
36
37 afterEach(() => {
38 localStorageMock.clear()
39 })
40
41 describe('initialization', () => {
42 it('should initialize with default state', () => {
43 const { result } = renderHook(() => useChartHoverState('chart1'))
44
45 expect(result.current.hoveredIndex).toBe(null)
46 expect(result.current.syncHover).toBe(true)
47 expect(result.current.syncTooltip).toBe(true)
48 expect(result.current.hoveredChart).toBe(null)
49 expect(result.current.isHovered).toBe(false)
50 expect(result.current.isCurrentChart).toBe(false)
51 })
52
53 it('should load sync settings from localStorage on initialization', async () => {
54 localStorageMock.setItem('briven-chart-hover-sync-enabled', 'true')
55 localStorageMock.setItem('briven-chart-tooltip-sync-enabled', 'true')
56
57 vi.resetModules()
58 const { useChartHoverState: useChartHoverStateWithStorage } =
59 await import('./useChartHoverState')
60
61 const { result } = renderHook(() => useChartHoverStateWithStorage('chart1'))
62
63 expect(result.current.syncHover).toBe(true)
64 expect(result.current.syncTooltip).toBe(true)
65 })
66
67 it('should handle corrupted localStorage data gracefully', async () => {
68 localStorageMock.setItem('briven-chart-hover-sync-enabled', 'invalid-json')
69 localStorageMock.setItem('briven-chart-tooltip-sync-enabled', 'invalid-json')
70
71 vi.resetModules()
72 const { useChartHoverState: useChartHoverStateWithCorrupted } =
73 await import('./useChartHoverState')
74
75 const { result } = renderHook(() => useChartHoverStateWithCorrupted('chart1'))
76
77 expect(result.current.syncHover).toBe(true)
78 expect(result.current.syncTooltip).toBe(true)
79 expect(consoleWarnSpy).toHaveBeenCalled()
80 })
81 })
82
83 describe('hover functionality', () => {
84 it('should set hover state locally when sync is disabled', () => {
85 const { result } = renderHook(() => useChartHoverState('chart1'))
86
87 act(() => {
88 result.current.setHover(5)
89 })
90
91 expect(result.current.hoveredIndex).toBe(5)
92 expect(result.current.hoveredChart).toBe('chart1')
93 expect(result.current.isHovered).toBe(true)
94 expect(result.current.isCurrentChart).toBe(true)
95 })
96
97 it('should clear hover state locally when sync is disabled', () => {
98 const { result } = renderHook(() => useChartHoverState('chart1'))
99
100 act(() => {
101 result.current.setHover(5)
102 })
103
104 act(() => {
105 result.current.clearHover()
106 })
107
108 expect(result.current.hoveredIndex).toBe(null)
109 expect(result.current.hoveredChart).toBe(null)
110 expect(result.current.isHovered).toBe(false)
111 expect(result.current.isCurrentChart).toBe(false)
112 })
113
114 it('should sync hover state globally when sync is enabled', () => {
115 const { result: result1 } = renderHook(() => useChartHoverState('chart1'))
116 const { result: result2 } = renderHook(() => useChartHoverState('chart2'))
117
118 act(() => {
119 result1.current.setSyncHover(true)
120 })
121 act(() => {
122 result1.current.setHover(3)
123 })
124
125 expect(result1.current.hoveredIndex).toBe(3)
126 expect(result1.current.hoveredChart).toBe('chart1')
127 expect(result1.current.isCurrentChart).toBe(true)
128
129 expect(result2.current.hoveredIndex).toBe(3)
130 expect(result2.current.hoveredChart).toBe('chart1')
131 expect(result2.current.isCurrentChart).toBe(false)
132 })
133
134 it('should clear synced hover state globally', () => {
135 const { result: result1 } = renderHook(() => useChartHoverState('chart1'))
136 const { result: result2 } = renderHook(() => useChartHoverState('chart2'))
137
138 act(() => {
139 result1.current.setSyncHover(true)
140 result1.current.setHover(3)
141 })
142 act(() => {
143 result2.current.clearHover()
144 })
145
146 expect(result1.current.hoveredIndex).toBe(null)
147 expect(result1.current.hoveredChart).toBe(null)
148 expect(result2.current.hoveredIndex).toBe(null)
149 expect(result2.current.hoveredChart).toBe(null)
150 })
151 })
152
153 describe('sync settings', () => {
154 it('should enable hover sync and save to localStorage', () => {
155 const { result } = renderHook(() => useChartHoverState('chart1'))
156
157 act(() => {
158 result.current.setSyncHover(true)
159 })
160
161 expect(result.current.syncHover).toBe(true)
162 expect(localStorageMock.setItem).toHaveBeenCalledWith(
163 'briven-chart-hover-sync-enabled',
164 'true'
165 )
166 })
167
168 it('should disable hover sync and automatically disable tooltip sync', () => {
169 const { result } = renderHook(() => useChartHoverState('chart1'))
170
171 act(() => {
172 result.current.setSyncTooltip(true)
173 })
174
175 expect(result.current.syncHover).toBe(true)
176 expect(result.current.syncTooltip).toBe(true)
177 act(() => {
178 result.current.setSyncHover(false)
179 })
180
181 expect(result.current.syncHover).toBe(false)
182 expect(result.current.syncTooltip).toBe(false)
183 })
184
185 it('should enable tooltip sync and automatically enable hover sync', () => {
186 const { result } = renderHook(() => useChartHoverState('chart1'))
187
188 act(() => {
189 result.current.setSyncTooltip(true)
190 })
191
192 expect(result.current.syncHover).toBe(true)
193 expect(result.current.syncTooltip).toBe(true)
194 expect(localStorageMock.setItem).toHaveBeenCalledWith(
195 'briven-chart-hover-sync-enabled',
196 'true'
197 )
198 expect(localStorageMock.setItem).toHaveBeenCalledWith(
199 'briven-chart-tooltip-sync-enabled',
200 'true'
201 )
202 })
203
204 it('should handle localStorage errors gracefully when saving sync settings', () => {
205 const { result } = renderHook(() => useChartHoverState('chart1'))
206
207 localStorageMock.setItem.mockImplementationOnce(() => {
208 throw new Error('Storage quota exceeded')
209 })
210
211 act(() => {
212 result.current.setSyncHover(true)
213 })
214
215 expect(result.current.syncHover).toBe(true)
216 expect(consoleWarnSpy).toHaveBeenCalledWith(
217 'Failed to save chart hover sync setting to localStorage:',
218 expect.any(Error)
219 )
220 })
221 })
222
223 describe('state synchronization between multiple hooks', () => {
224 it('should synchronize state changes between multiple hook instances', () => {
225 const { result: result1 } = renderHook(() => useChartHoverState('chart1'))
226 const { result: result2 } = renderHook(() => useChartHoverState('chart2'))
227 const { result: result3 } = renderHook(() => useChartHoverState('chart3'))
228
229 act(() => {
230 result1.current.setSyncHover(true)
231 })
232
233 expect(result1.current.syncHover).toBe(true)
234 expect(result2.current.syncHover).toBe(true)
235 expect(result3.current.syncHover).toBe(true)
236
237 act(() => {
238 result2.current.setHover(7)
239 })
240 expect(result1.current.hoveredIndex).toBe(7)
241 expect(result1.current.hoveredChart).toBe('chart2')
242 expect(result2.current.hoveredIndex).toBe(7)
243 expect(result2.current.hoveredChart).toBe('chart2')
244 expect(result3.current.hoveredIndex).toBe(7)
245 expect(result3.current.hoveredChart).toBe('chart2')
246 })
247
248 it('should correctly identify current chart vs synced charts', () => {
249 const { result: result1 } = renderHook(() => useChartHoverState('chart1'))
250 const { result: result2 } = renderHook(() => useChartHoverState('chart2'))
251
252 act(() => {
253 result1.current.setSyncHover(true)
254 result1.current.setHover(5)
255 })
256
257 expect(result1.current.isCurrentChart).toBe(true)
258 expect(result2.current.isCurrentChart).toBe(false)
259 expect(result1.current.isHovered).toBe(true)
260 expect(result2.current.isHovered).toBe(true)
261 })
262 })
263
264 describe('edge cases', () => {
265 it('should handle setting hover to null', () => {
266 const { result } = renderHook(() => useChartHoverState('chart1'))
267
268 act(() => {
269 result.current.setHover(5)
270 })
271
272 act(() => {
273 result.current.setHover(null)
274 })
275
276 expect(result.current.hoveredIndex).toBe(null)
277 expect(result.current.hoveredChart).toBe(null)
278 })
279
280 it('should handle rapid state changes', () => {
281 const { result } = renderHook(() => useChartHoverState('chart1'))
282
283 act(() => {
284 result.current.setSyncHover(true)
285 result.current.setHover(1)
286 result.current.setHover(2)
287 result.current.setHover(3)
288 result.current.clearHover()
289 result.current.setHover(4)
290 })
291
292 expect(result.current.hoveredIndex).toBe(4)
293 expect(result.current.hoveredChart).toBe('chart1')
294 })
295
296 it('should not trigger unnecessary state updates when state is the same', () => {
297 const { result } = renderHook(() => useChartHoverState('chart1'))
298
299 const initialSyncHover = result.current.syncHover
300
301 act(() => {
302 result.current.setSyncHover(initialSyncHover)
303 })
304
305 expect(result.current.syncHover).toBe(initialSyncHover)
306 })
307 })
308
309 describe('helper functions', () => {
310 it('should correctly calculate isHovered for current chart', () => {
311 const { result } = renderHook(() => useChartHoverState('chart1'))
312
313 expect(result.current.hoveredIndex).toBe(null)
314 expect(result.current.isHovered).toBe(false)
315
316 act(() => {
317 result.current.setHover(5)
318 })
319
320 expect(result.current.hoveredIndex).toBe(5)
321 expect(result.current.isHovered).toBe(true)
322 })
323
324 it('should correctly calculate isHovered for synced charts', () => {
325 const { result: result1 } = renderHook(() => useChartHoverState('chart1'))
326 const { result: result2 } = renderHook(() => useChartHoverState('chart2'))
327
328 act(() => {
329 result1.current.setSyncHover(true)
330 result1.current.setHover(5)
331 })
332
333 expect(result1.current.isHovered).toBe(true) // Current chart
334 expect(result2.current.isHovered).toBe(true) // Synced chart
335 })
336
337 it('should correctly identify current chart', () => {
338 const { result: result1 } = renderHook(() => useChartHoverState('chart1'))
339 const { result: result2 } = renderHook(() => useChartHoverState('chart2'))
340
341 act(() => {
342 result1.current.setSyncHover(true)
343 result1.current.setHover(5)
344 })
345
346 expect(result1.current.isCurrentChart).toBe(true)
347 expect(result2.current.isCurrentChart).toBe(false)
348 })
349 })
350})