Skip to content

Commit 670845b

Browse files
authored
fix(tests): more coverage (#1232)
* fix(tests): more coverage * fix * fix 2
1 parent 1815112 commit 670845b

15 files changed

Lines changed: 1839 additions & 17 deletions

tests/react/actions.test.tsx

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import { act, fireEvent, render, screen } from '@testing-library/react'
2+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3+
import { proxy, useSnapshot } from 'valtio'
4+
import { deepClone } from 'valtio/utils'
5+
6+
// Behavior described in docs/how-tos/how-to-organize-actions.mdx
7+
describe('organizing actions', () => {
8+
beforeEach(() => {
9+
vi.useFakeTimers()
10+
})
11+
12+
afterEach(() => {
13+
vi.useRealTimers()
14+
})
15+
16+
const renderCounter = (state: { count: number }, inc: () => void) => {
17+
const Component = () => {
18+
const snap = useSnapshot(state)
19+
return (
20+
<>
21+
<div>count: {snap.count}</div>
22+
<button onClick={inc}>button</button>
23+
</>
24+
)
25+
}
26+
render(<Component />)
27+
}
28+
29+
const clickButton = () => fireEvent.click(screen.getByText('button'))
30+
31+
it('should support action functions defined in a module', async () => {
32+
const state = proxy({ count: 0, name: 'foo' })
33+
const inc = () => {
34+
++state.count
35+
}
36+
37+
renderCounter(state, inc)
38+
clickButton()
39+
await act(() => vi.advanceTimersByTimeAsync(0))
40+
expect(screen.getByText('count: 1')).toBeInTheDocument()
41+
})
42+
43+
it('should support an action object defined in a module', async () => {
44+
const state = proxy({ count: 0, name: 'foo' })
45+
const actions = {
46+
inc: () => {
47+
++state.count
48+
},
49+
setName: (name: string) => {
50+
state.name = name
51+
},
52+
}
53+
54+
renderCounter(state, actions.inc)
55+
clickButton()
56+
await act(() => vi.advanceTimersByTimeAsync(0))
57+
expect(screen.getByText('count: 1')).toBeInTheDocument()
58+
59+
actions.setName('bar')
60+
expect(state.name).toBe('bar')
61+
})
62+
63+
it('should support action methods stored in the state', async () => {
64+
const state: {
65+
count: number
66+
name: string
67+
inc: () => void
68+
setName: (name: string) => void
69+
} = proxy({
70+
count: 0,
71+
name: 'foo',
72+
inc: () => {
73+
++state.count
74+
},
75+
setName: (name: string) => {
76+
state.name = name
77+
},
78+
})
79+
80+
renderCounter(state, () => state.inc())
81+
clickButton()
82+
await act(() => vi.advanceTimersByTimeAsync(0))
83+
expect(screen.getByText('count: 1')).toBeInTheDocument()
84+
})
85+
86+
it('should support action methods using this', async () => {
87+
const state = proxy({
88+
count: 0,
89+
name: 'foo',
90+
inc() {
91+
++this.count
92+
},
93+
setName(name: string) {
94+
this.name = name
95+
},
96+
})
97+
98+
renderCounter(state, () => state.inc())
99+
clickButton()
100+
await act(() => vi.advanceTimersByTimeAsync(0))
101+
expect(screen.getByText('count: 1')).toBeInTheDocument()
102+
103+
state.setName('bar')
104+
expect(state.name).toBe('bar')
105+
})
106+
107+
it('should support a class instance', async () => {
108+
class State {
109+
count = 0
110+
name = 'foo'
111+
inc() {
112+
++this.count
113+
}
114+
setName(name: string) {
115+
this.name = name
116+
}
117+
}
118+
const state = proxy(new State())
119+
120+
renderCounter(state, () => state.inc())
121+
clickButton()
122+
await act(() => vi.advanceTimersByTimeAsync(0))
123+
expect(screen.getByText('count: 1')).toBeInTheDocument()
124+
125+
state.setName('bar')
126+
expect(state.name).toBe('bar')
127+
})
128+
})
129+
130+
// Behavior described in docs/how-tos/how-to-reset-state.mdx
131+
describe('resetting state', () => {
132+
beforeEach(() => {
133+
vi.useFakeTimers()
134+
})
135+
136+
afterEach(() => {
137+
vi.useRealTimers()
138+
})
139+
140+
const initialObj = {
141+
text: 'hello',
142+
arr: [1, 2, 3],
143+
obj: { a: 'b' },
144+
}
145+
146+
it('should reset by reassigning each key from a fresh clone', async () => {
147+
const state = proxy(deepClone(initialObj))
148+
const reset = () => {
149+
const resetObj = deepClone(initialObj)
150+
Object.keys(resetObj).forEach((key) => {
151+
state[key as keyof typeof resetObj] = resetObj[
152+
key as keyof typeof resetObj
153+
] as never
154+
})
155+
}
156+
157+
const Component = () => {
158+
const snap = useSnapshot(state)
159+
return (
160+
<>
161+
<div>text: {snap.text}</div>
162+
<div>arr: {snap.arr.join(',')}</div>
163+
<button onClick={reset}>reset</button>
164+
</>
165+
)
166+
}
167+
168+
render(<Component />)
169+
170+
state.text = 'changed'
171+
state.arr.push(4)
172+
state.obj.a = 'c'
173+
await act(() => vi.advanceTimersByTimeAsync(0))
174+
expect(screen.getByText('text: changed')).toBeInTheDocument()
175+
expect(screen.getByText('arr: 1,2,3,4')).toBeInTheDocument()
176+
177+
fireEvent.click(screen.getByText('reset'))
178+
await act(() => vi.advanceTimersByTimeAsync(0))
179+
expect(screen.getByText('text: hello')).toBeInTheDocument()
180+
expect(screen.getByText('arr: 1,2,3')).toBeInTheDocument()
181+
expect(state.obj.a).toBe('b')
182+
})
183+
184+
it('should reset a nested holder in one assignment', async () => {
185+
const state = proxy({ obj: deepClone(initialObj) })
186+
const reset = () => {
187+
state.obj = deepClone(initialObj)
188+
}
189+
190+
const Component = () => {
191+
const snap = useSnapshot(state)
192+
return (
193+
<>
194+
<div>text: {snap.obj.text}</div>
195+
<button onClick={reset}>reset</button>
196+
</>
197+
)
198+
}
199+
200+
render(<Component />)
201+
202+
state.obj.text = 'changed'
203+
await act(() => vi.advanceTimersByTimeAsync(0))
204+
expect(screen.getByText('text: changed')).toBeInTheDocument()
205+
206+
fireEvent.click(screen.getByText('reset'))
207+
await act(() => vi.advanceTimersByTimeAsync(0))
208+
expect(screen.getByText('text: hello')).toBeInTheDocument()
209+
})
210+
211+
it('should keep the initial object untouched when cloned', () => {
212+
const state = proxy(deepClone(initialObj))
213+
state.text = 'changed'
214+
state.arr.push(4)
215+
expect(initialObj.text).toBe('hello')
216+
expect(initialObj.arr).toEqual([1, 2, 3])
217+
})
218+
})

tests/react/arrays.test.tsx

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { act, fireEvent, render, screen } from '@testing-library/react'
2+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3+
import { proxy, useSnapshot } from 'valtio'
4+
5+
// Behavior described in docs/how-tos/how-to-update-values-inside-arrays.mdx
6+
describe('updating values inside arrays', () => {
7+
beforeEach(() => {
8+
vi.useFakeTimers()
9+
})
10+
11+
afterEach(() => {
12+
vi.useRealTimers()
13+
})
14+
15+
const createState = () =>
16+
proxy({
17+
title: 'My Counter list',
18+
items: [
19+
{ id: 1, count: 0 },
20+
{ id: 2, count: 0 },
21+
],
22+
})
23+
24+
it('should re-render the whole list when the parent maps over the snapshot', async () => {
25+
const state = createState()
26+
27+
const listRenderFn = vi.fn()
28+
const itemRenderFn = vi.fn()
29+
30+
const Counter = ({ item }: { item: { id: number; count: number } }) => {
31+
itemRenderFn(item.id)
32+
return <div>{`item ${item.id}: ${item.count}`}</div>
33+
}
34+
35+
const CounterList = () => {
36+
const snap = useSnapshot(state)
37+
listRenderFn()
38+
return (
39+
<>
40+
<h1>{snap.title}</h1>
41+
{snap.items.map((item) => (
42+
<Counter key={item.id} item={item} />
43+
))}
44+
</>
45+
)
46+
}
47+
48+
render(<CounterList />)
49+
expect(listRenderFn).toBeCalledTimes(1)
50+
expect(itemRenderFn).toBeCalledTimes(2)
51+
52+
state.items[0]!.count += 1
53+
await act(() => vi.advanceTimersByTimeAsync(0))
54+
55+
expect(screen.getByText('item 1: 1')).toBeInTheDocument()
56+
expect(listRenderFn).toBeCalledTimes(2)
57+
expect(itemRenderFn).toBeCalledTimes(4)
58+
})
59+
60+
it('should re-render only the changed item when children take the proxy', async () => {
61+
const state = createState()
62+
63+
const listRenderFn = vi.fn()
64+
const itemRenderFn = vi.fn()
65+
66+
const Counter = ({ item }: { item: { id: number; count: number } }) => {
67+
const snap = useSnapshot(item)
68+
itemRenderFn(snap.id)
69+
return <div>{`item ${snap.id}: ${snap.count}`}</div>
70+
}
71+
72+
const CounterList = () => {
73+
const snap = useSnapshot(state)
74+
listRenderFn()
75+
return (
76+
<>
77+
<h1>{snap.title}</h1>
78+
{Array.from({ length: snap.items.length }, (_, index) => (
79+
<Counter key={state.items[index]!.id} item={state.items[index]!} />
80+
))}
81+
</>
82+
)
83+
}
84+
85+
render(<CounterList />)
86+
expect(listRenderFn).toBeCalledTimes(1)
87+
expect(itemRenderFn).toBeCalledTimes(2)
88+
89+
state.items[0]!.count += 1
90+
await act(() => vi.advanceTimersByTimeAsync(0))
91+
92+
expect(screen.getByText('item 1: 1')).toBeInTheDocument()
93+
expect(listRenderFn).toBeCalledTimes(1)
94+
expect(itemRenderFn).toBeCalledTimes(3)
95+
})
96+
97+
it('should re-render the list when its length changes', async () => {
98+
const state = createState()
99+
100+
const listRenderFn = vi.fn()
101+
102+
const CounterList = () => {
103+
const snap = useSnapshot(state)
104+
listRenderFn()
105+
return <div>count: {snap.items.length}</div>
106+
}
107+
108+
render(<CounterList />)
109+
expect(listRenderFn).toBeCalledTimes(1)
110+
111+
state.items.push({ id: 3, count: 0 })
112+
await act(() => vi.advanceTimersByTimeAsync(0))
113+
114+
expect(screen.getByText('count: 3')).toBeInTheDocument()
115+
expect(listRenderFn).toBeCalledTimes(2)
116+
})
117+
118+
it('should mutate an item through the proxy from a child callback', async () => {
119+
const state = createState()
120+
121+
const Counter = ({ item }: { item: { id: number; count: number } }) => {
122+
const snap = useSnapshot(item)
123+
return (
124+
<>
125+
<div>{`item ${snap.id}: ${snap.count}`}</div>
126+
<button onClick={() => item.count++}>{`inc ${snap.id}`}</button>
127+
</>
128+
)
129+
}
130+
131+
const CounterList = () => {
132+
const snap = useSnapshot(state)
133+
return (
134+
<>
135+
{Array.from({ length: snap.items.length }, (_, index) => (
136+
<Counter key={state.items[index]!.id} item={state.items[index]!} />
137+
))}
138+
</>
139+
)
140+
}
141+
142+
render(<CounterList />)
143+
144+
fireEvent.click(screen.getByText('inc 2'))
145+
await act(() => vi.advanceTimersByTimeAsync(0))
146+
147+
expect(screen.getByText('item 1: 0')).toBeInTheDocument()
148+
expect(screen.getByText('item 2: 1')).toBeInTheDocument()
149+
expect(state.items[1]!.count).toBe(1)
150+
})
151+
})

0 commit comments

Comments
 (0)