useChartHoverState.test.tsx 11 KB

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