Skip to content

Commit a6ad505

Browse files
authored
fix(tests): add more test (#1233)
* fix(tests): add more test * fix
1 parent 670845b commit a6ad505

5 files changed

Lines changed: 303 additions & 1 deletion

File tree

tests/react/arrays.test.tsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,76 @@ describe('updating values inside arrays', () => {
115115
expect(listRenderFn).toBeCalledTimes(2)
116116
})
117117

118+
// The todo example derives its list inside a custom hook (useTodos) that
119+
// filters the snapshot array rather than reading it directly.
120+
describe('derived through a custom hook', () => {
121+
const createTodoState = () =>
122+
proxy({
123+
todos: [
124+
{ id: 1, name: 'a', completed: false },
125+
{ id: 2, name: 'b', completed: true },
126+
],
127+
filter: 'completed' as 'all' | 'completed',
128+
})
129+
130+
it('should track only what the derivation reads', async () => {
131+
const state = createTodoState()
132+
133+
const useTodos = () => {
134+
const snap = useSnapshot(state)
135+
return snap.filter === 'all'
136+
? snap.todos
137+
: snap.todos.filter((todo) => todo.completed)
138+
}
139+
140+
const renderFn = vi.fn()
141+
const TodoList = () => {
142+
const todos = useTodos()
143+
renderFn()
144+
return <div>{`ids: ${todos.map((todo) => todo.id).join(',')}`}</div>
145+
}
146+
147+
render(<TodoList />)
148+
expect(screen.getByText('ids: 2')).toBeInTheDocument()
149+
expect(renderFn).toBeCalledTimes(1)
150+
151+
// `name` is never read by the derivation or the render
152+
state.todos[0]!.name = 'renamed'
153+
await act(() => vi.advanceTimersByTimeAsync(0))
154+
expect(renderFn).toBeCalledTimes(1)
155+
156+
state.todos[0]!.completed = true
157+
await act(() => vi.advanceTimersByTimeAsync(0))
158+
expect(screen.getByText('ids: 1,2')).toBeInTheDocument()
159+
expect(renderFn).toBeCalledTimes(2)
160+
})
161+
162+
it('should re-render when the filter itself changes', async () => {
163+
const state = createTodoState()
164+
165+
const TodoList = () => {
166+
const snap = useSnapshot(state)
167+
const todos =
168+
snap.filter === 'all'
169+
? snap.todos
170+
: snap.todos.filter((todo) => todo.completed)
171+
return (
172+
<>
173+
<div>{`ids: ${todos.map((todo) => todo.id).join(',')}`}</div>
174+
<button onClick={() => (state.filter = 'all')}>all</button>
175+
</>
176+
)
177+
}
178+
179+
render(<TodoList />)
180+
expect(screen.getByText('ids: 2')).toBeInTheDocument()
181+
182+
fireEvent.click(screen.getByText('all'))
183+
await act(() => vi.advanceTimersByTimeAsync(0))
184+
expect(screen.getByText('ids: 1,2')).toBeInTheDocument()
185+
})
186+
})
187+
118188
it('should mutate an item through the proxy from a child callback', async () => {
119189
const state = createState()
120190

tests/react/object.test.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,35 @@ describe('object', () => {
3838
expect(screen.getByText('count: 1')).toBeInTheDocument()
3939
})
4040

41+
// The counter example creates the proxy without `nested`, assigns it right
42+
// after, and reads it optionally in the component.
43+
it('property added after creation', async () => {
44+
const obj = proxy<{ count: number; nested?: { ticks: number } }>({
45+
count: 0,
46+
})
47+
48+
const Counter = () => {
49+
const snap = useSnapshot(obj)
50+
return <div>ticks: {snap.nested?.ticks ?? 'none'}</div>
51+
}
52+
53+
render(
54+
<StrictMode>
55+
<Counter />
56+
</StrictMode>,
57+
)
58+
59+
expect(screen.getByText('ticks: none')).toBeInTheDocument()
60+
61+
obj.nested = { ticks: 0 }
62+
await act(() => vi.advanceTimersByTimeAsync(0))
63+
expect(screen.getByText('ticks: 0')).toBeInTheDocument()
64+
65+
obj.nested.ticks += 1
66+
await act(() => vi.advanceTimersByTimeAsync(0))
67+
expect(screen.getByText('ticks: 1')).toBeInTheDocument()
68+
})
69+
4170
it('deleting property', async () => {
4271
const obj = proxy<{ count?: number }>({ count: 1 })
4372

tests/utils/proxyMap.test.tsx

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { StrictMode } from 'react'
22
import { act, fireEvent, render, screen } from '@testing-library/react'
33
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4-
import { proxy, snapshot, useSnapshot } from 'valtio'
4+
import { proxy, snapshot, subscribe, useSnapshot } from 'valtio'
55
import { proxyMap, proxySet } from 'valtio/utils'
66

77
const initialValues = [
@@ -329,6 +329,83 @@ describe('proxyMap', () => {
329329
).toBe(false)
330330
})
331331

332+
// The todo-with-proxyMap example stores objects and mutates them in place
333+
// via get() and forEach(). The other ui tests here only store primitives.
334+
it('should proxy object values and notify when one is mutated', async () => {
335+
const state = proxy({
336+
todos: proxyMap<number, { name: string; completed: boolean }>(),
337+
})
338+
state.todos.set(1, { name: 'a', completed: false })
339+
340+
const handler = vi.fn()
341+
subscribe(state, handler)
342+
343+
state.todos.get(1)!.completed = true
344+
await vi.advanceTimersByTimeAsync(0)
345+
346+
expect(handler).toBeCalledTimes(1)
347+
expect(state.todos.get(1)).toEqual({ name: 'a', completed: true })
348+
expect(snapshot(state).todos.get(1)).toEqual({
349+
name: 'a',
350+
completed: true,
351+
})
352+
})
353+
354+
it('should notify when object values are mutated during forEach', async () => {
355+
const state = proxy({
356+
todos: proxyMap<number, { completed: boolean }>([
357+
[1, { completed: false }],
358+
[2, { completed: false }],
359+
]),
360+
})
361+
362+
const handler = vi.fn()
363+
subscribe(state, handler)
364+
365+
state.todos.forEach((todo) => {
366+
todo.completed = true
367+
})
368+
await vi.advanceTimersByTimeAsync(0)
369+
370+
expect(handler).toBeCalledTimes(1)
371+
expect(Array.from(state.todos.values())).toEqual([
372+
{ completed: true },
373+
{ completed: true },
374+
])
375+
})
376+
377+
it('should re-render a component when an object value is mutated', async () => {
378+
const state = proxy({
379+
todos: proxyMap<number, { name: string; completed: boolean }>([
380+
[1, { name: 'a', completed: false }],
381+
]),
382+
})
383+
384+
const TestComponent = () => {
385+
const snap = useSnapshot(state)
386+
return (
387+
<div>
388+
{Array.from(snap.todos.values())
389+
.map((todo) => `${todo.name}:${todo.completed}`)
390+
.join(',')}
391+
</div>
392+
)
393+
}
394+
395+
render(
396+
<StrictMode>
397+
<TestComponent />
398+
</StrictMode>,
399+
)
400+
401+
expect(screen.getByText('a:false')).toBeInTheDocument()
402+
403+
state.todos.get(1)!.completed = true
404+
await act(() => vi.advanceTimersByTimeAsync(0))
405+
406+
expect(screen.getByText('a:true')).toBeInTheDocument()
407+
})
408+
332409
it('should not implement getOrInsert', () => {
333410
const map = proxyMap<string, number>() as any
334411
expect(() => map.getOrInsert('a', 1)).toThrow('not implemented')

tests/vanilla/entrypoints.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
import * as main from 'valtio'
3+
import * as react from 'valtio/react'
4+
import * as reactUtils from 'valtio/react/utils'
5+
import * as mainUtils from 'valtio/utils'
6+
import * as vanilla from 'valtio/vanilla'
7+
import * as vanillaUtils from 'valtio/vanilla/utils'
8+
9+
// docs/how-tos/some-gotchas.mdx tells non-React users to import from
10+
// valtio/vanilla and valtio/vanilla/utils, and the photo-booth example does.
11+
// Nothing else in the suite touches those entry points.
12+
//
13+
// These tests also run against the built CJS and ESM output, where the CJS
14+
// interop adds a `default` key that the sources do not have, so it is dropped
15+
// before comparing.
16+
const exportsOf = (ns: object) =>
17+
Object.keys(ns)
18+
.filter((key) => key !== 'default')
19+
.sort()
20+
21+
describe('entry points', () => {
22+
it('should expose the core from valtio/vanilla', () => {
23+
expect(exportsOf(vanilla)).toEqual([
24+
'getVersion',
25+
'proxy',
26+
'ref',
27+
'snapshot',
28+
'subscribe',
29+
'unstable_enableOp',
30+
'unstable_getInternalStates',
31+
'unstable_replaceInternalFunction',
32+
])
33+
})
34+
35+
it('should not expose React bindings from valtio/vanilla', () => {
36+
expect(exportsOf(vanilla)).not.toContain('useSnapshot')
37+
expect(exportsOf(vanillaUtils)).not.toContain('useProxy')
38+
})
39+
40+
it('should expose the utils from valtio/vanilla/utils', () => {
41+
expect(exportsOf(vanillaUtils)).toEqual([
42+
'deepClone',
43+
'devtools',
44+
'isProxyMap',
45+
'isProxySet',
46+
'proxyMap',
47+
'proxySet',
48+
'subscribeKey',
49+
'unstable_deepProxy',
50+
'watch',
51+
])
52+
})
53+
54+
it('should expose useSnapshot from valtio/react and useProxy from valtio/react/utils', () => {
55+
expect(exportsOf(react)).toEqual(['useSnapshot'])
56+
expect(exportsOf(reactUtils)).toEqual(['useProxy'])
57+
})
58+
59+
it('should re-export both halves from the main entry points', () => {
60+
expect(exportsOf(main)).toEqual(
61+
[...exportsOf(vanilla), ...exportsOf(react)].sort(),
62+
)
63+
expect(exportsOf(mainUtils)).toEqual(
64+
[...exportsOf(vanillaUtils), ...exportsOf(reactUtils)].sort(),
65+
)
66+
})
67+
68+
it('should work end to end through valtio/vanilla alone', async () => {
69+
const state = vanilla.proxy({ count: 0, nested: { text: 'a' } })
70+
const handler = vi.fn()
71+
vanilla.subscribe(state, handler)
72+
73+
state.count += 1
74+
state.nested.text = 'b'
75+
await Promise.resolve()
76+
77+
expect(handler).toBeCalledTimes(1)
78+
expect(vanilla.snapshot(state)).toEqual({ count: 1, nested: { text: 'b' } })
79+
})
80+
81+
it('should work end to end through valtio/vanilla/utils alone', () => {
82+
const set = vanillaUtils.proxySet([1, 2])
83+
const map = vanillaUtils.proxyMap<string, number>([['a', 1]])
84+
85+
expect(vanillaUtils.isProxySet(set)).toBe(true)
86+
expect(vanillaUtils.isProxyMap(map)).toBe(true)
87+
expect(vanillaUtils.deepClone({ nested: { count: 0 } })).toEqual({
88+
nested: { count: 0 },
89+
})
90+
})
91+
})

tests/vanilla/proxy.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,41 @@ describe('proxy arrays', () => {
230230
expect([...state]).toEqual([3, 2, 1])
231231
})
232232

233+
// The todo example removes an item with
234+
// `store.todos = store.todos.filter(...)`, replacing the array with a plain
235+
// copy whose members are already proxies.
236+
it('should preserve member identity when replaced by a filtered copy', async () => {
237+
const state = proxy({ items: [{ id: 1 }, { id: 2 }, { id: 3 }] })
238+
const survivor = state.items[1]
239+
const handler = vi.fn()
240+
subscribe(state, handler)
241+
242+
state.items = state.items.filter((item) => item.id !== 1)
243+
244+
await Promise.resolve()
245+
expect(handler).toBeCalledTimes(1)
246+
expect(state.items.map((item) => item.id)).toEqual([2, 3])
247+
expect(state.items[0]).toBe(survivor)
248+
expect(snapshot(state)).toEqual({ items: [{ id: 2 }, { id: 3 }] })
249+
})
250+
251+
it('should keep notifying through a member that survived the replacement', async () => {
252+
const state = proxy({
253+
items: [
254+
{ id: 1, n: 0 },
255+
{ id: 2, n: 0 },
256+
],
257+
})
258+
state.items = state.items.filter((item) => item.id !== 1)
259+
260+
const handler = vi.fn()
261+
subscribe(state, handler)
262+
263+
state.items[0]!.n += 1
264+
await Promise.resolve()
265+
expect(handler).toBeCalledTimes(1)
266+
})
267+
233268
it('should keep length in sync with sparse assignment', () => {
234269
const state = proxy([0])
235270
state[3] = 3

0 commit comments

Comments
 (0)