useLocalStorageQuery.test.ts113 lines · main
1import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
2import { act, renderHook, waitFor } from '@testing-library/react'
3import React from 'react'
4import { afterEach, beforeEach, describe, expect, it } from 'vitest'
5
6import { useLocalStorageQuery } from '../useLocalStorage'
7import { customRenderHook } from '@/tests/lib/custom-render'
8
9function makeQueryClient() {
10 return new QueryClient({ defaultOptions: { queries: { retry: false } } })
11}
12
13function makeWrapper(queryClient: QueryClient) {
14 return ({ children }: { children: React.ReactNode }) =>
15 React.createElement(QueryClientProvider, { client: queryClient }, children)
16}
17
18describe('useLocalStorageQuery', () => {
19 beforeEach(() => {
20 window.localStorage.clear()
21 })
22
23 afterEach(() => {
24 window.localStorage.clear()
25 })
26
27 it('returns initialValue when localStorage has no entry for the key', async () => {
28 const { result } = customRenderHook(() => useLocalStorageQuery('test-key', 'default'))
29
30 await waitFor(() => expect(result.current[2].isSuccess).toBe(true))
31 expect(result.current[0]).toBe('default')
32 })
33
34 it('returns the stored value from localStorage on mount', async () => {
35 window.localStorage.setItem('test-key', JSON.stringify('persisted'))
36
37 const { result } = customRenderHook(() => useLocalStorageQuery('test-key', 'default'))
38
39 await waitFor(() => expect(result.current[0]).toBe('persisted'))
40 })
41
42 it('updates the value in state and localStorage when setValue is called', async () => {
43 const { result } = customRenderHook(() => useLocalStorageQuery('test-key', 'default'))
44 await waitFor(() => expect(result.current[2].isSuccess).toBe(true))
45
46 act(() => {
47 result.current[1]('updated')
48 })
49
50 await waitFor(() => expect(result.current[0]).toBe('updated'))
51 expect(window.localStorage.getItem('test-key')).toBe(JSON.stringify('updated'))
52 })
53
54 it('supports a function updater that receives the current value', async () => {
55 window.localStorage.setItem('count', JSON.stringify(5))
56
57 const { result } = customRenderHook(() => useLocalStorageQuery('count', 0))
58 await waitFor(() => expect(result.current[0]).toBe(5))
59
60 act(() => {
61 result.current[1]((prev: number) => prev + 1)
62 })
63
64 await waitFor(() => expect(result.current[0]).toBe(6))
65 expect(window.localStorage.getItem('count')).toBe(JSON.stringify(6))
66 })
67
68 it('works with object values', async () => {
69 const stored = { count: 3, label: 'hello' }
70 window.localStorage.setItem('obj-key', JSON.stringify(stored))
71
72 const { result } = customRenderHook(() =>
73 useLocalStorageQuery('obj-key', { count: 0, label: '' })
74 )
75
76 await waitFor(() => expect(result.current[0]).toEqual(stored))
77 })
78
79 it('syncs two hooks sharing the same key via the same QueryClient', async () => {
80 const queryClient = makeQueryClient()
81
82 const { result } = renderHook(
83 () => ({
84 a: useLocalStorageQuery('shared-key', 'initial'),
85 b: useLocalStorageQuery('shared-key', 'initial'),
86 }),
87 { wrapper: makeWrapper(queryClient) }
88 )
89
90 await waitFor(() => expect(result.current.a[2].isSuccess).toBe(true))
91
92 act(() => {
93 result.current.a[1]('from-a')
94 })
95
96 await waitFor(() => {
97 expect(result.current.a[0]).toBe('from-a')
98 expect(result.current.b[0]).toBe('from-a')
99 })
100 })
101
102 it('uses the initialValue as fallback when the function updater runs before any stored value', async () => {
103 const { result } = customRenderHook(() => useLocalStorageQuery('new-key', 10))
104 await waitFor(() => expect(result.current[2].isSuccess).toBe(true))
105
106 act(() => {
107 result.current[1]((prev: number) => prev * 2)
108 })
109
110 await waitFor(() => expect(result.current[0]).toBe(20))
111 expect(window.localStorage.getItem('new-key')).toBe(JSON.stringify(20))
112 })
113})