Skip to content

Commit

Permalink
bug: add tests for derived async gets stale value
Browse files Browse the repository at this point in the history
discussion: #2192
  • Loading branch information
David Maskasky committed Oct 19, 2023
1 parent cdbfcdf commit 2af5f05
Show file tree
Hide file tree
Showing 3 changed files with 246 additions and 3 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
"@rollup/plugin-typescript": "^11.1.4",
"@testing-library/dom": "^9.3.3",
"@testing-library/react": "^14.0.0",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "14.4.3",
"@types/babel__core": "^7.20.2",
"@types/react": "^18.2.23",
Expand Down
233 changes: 230 additions & 3 deletions tests/react/async.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { StrictMode, Suspense, useEffect, useRef } from 'react'
import { act, fireEvent, render, waitFor } from '@testing-library/react'
import { renderHook } from '@testing-library/react-hooks'
import userEvent from '@testing-library/user-event'
import { expect, it } from 'vitest'
import { useAtom } from 'jotai/react'
import { atom } from 'jotai/vanilla'
import type { Atom } from 'jotai/vanilla'
import { Provider, useAtom, useAtomValue } from 'jotai/react'
import { atom, createStore } from 'jotai/vanilla'
import type { Atom, SetStateAction, Setter } from 'jotai/vanilla'

const useCommitCount = () => {
const commitCountRef = useRef(1)
Expand Down Expand Up @@ -1143,3 +1144,229 @@ it('multiple derived atoms with dependency chaining and async write (#813)', asy
getByText('bName: beta')
})
})

it('[renderHook] resolves dependencies reliably after a delay', async () => {
const countAtom = atom(0)
const asyncCountAtom = atom(0)

const resolve: (() => void)[] = []
const asyncAtom = atom(async (get) => {
const count = get(countAtom)
await new Promise<void>((r) => resolve.push(r))
console.log(`resolved (${count})`)
return count
})

const derivedAsyncAtom = atom(
async (get, { setSelf }) => {
const count = get(countAtom)
await microtask()
console.log(`derived (${count})`)
const asyncCount = await get(asyncAtom)
console.log(`derived (${count})`, asyncCount)
setSelf(asyncCountAtom, asyncCount)
},
(_get, set, ...args: Parameters<Setter>) => set(...args)
)

const derivedSyncAtom = atom((get) => {
get(derivedAsyncAtom)
})

function useTest() {
const asyncCount = useAtomValue(asyncCountAtom)
useAtom(derivedSyncAtom)
const [count, setCount] = useAtom(countAtom)
return { count, setCount, asyncCount }
}

const { result } = renderHook(useTest)
const { setCount } = result.current

await microtask()

expect(result.current.count).toBe(0)
expect(result.current.asyncCount).toBe(0)

await act(() => setCount(increment))
await act(() => setCount(increment))

resolve[0]!()
resolve[1]!()
resolve[2]!()

await microtask()
await microtask()
await microtask()
await macrotask()

expect(result.current.count).toBe(2)
expect(result.current.asyncCount).toBe(2)

await act(() => setCount(increment))
await act(() => setCount(increment))

resolve[3]!()
resolve[4]!()

await microtask()
await microtask()
await microtask()
await macrotask()

expect(result.current.count).toBe(4)
expect(result.current.asyncCount).toBe(4) // 3
})

it('[render] resolves dependencies reliably after a delay', async () => {
const countAtom = atom(0)
const asyncCountAtom = atom(0)

const resolve: (() => void)[] = []
const asyncAtom = atom(async (get) => {
const count = get(countAtom)
await new Promise<void>((r) => resolve.push(r))
console.log(`resolved (${count})`)
return count
})

const derivedAsyncAtom = atom(
async (get, { setSelf }) => {
const count = get(countAtom)
await microtask()
console.log(`derived (${count})`)
const asyncCount = await get(asyncAtom)
console.log(`derived (${count})`, asyncCount)
setSelf(asyncCountAtom, asyncCount)
},
(_get, set, ...args: Parameters<Setter>) => set(...args)
)

const derivedSyncAtom = atom((get) => {
get(derivedAsyncAtom)
})

function useTest() {
const asyncCount = useAtomValue(asyncCountAtom)
useAtom(derivedSyncAtom)
const [count, setCount] = useAtom(countAtom)
return { count, setCount, asyncCount }
}
function TestComponent() {
const values = useTest()
return <div data-testid="test-comp">{JSON.stringify(values)}</div>
}
const store = createStore()

const Wrapper = ({ children }: React.PropsWithChildren) => (
<Provider store={store}>{children}</Provider>
)
render(<TestComponent />, { wrapper: Wrapper })

const setCount = (arg: SetStateAction<number>) => store.set(countAtom, arg)

await microtask()

expect(store.get(countAtom)).toBe(0)
expect(store.get(asyncCountAtom)).toBe(0)

await act(() => setCount(increment))
await act(() => setCount(increment))

resolve[0]!()
resolve[1]!()
resolve[2]!()

await macrotask()

expect(store.get(countAtom)).toBe(2)
expect(store.get(asyncCountAtom)).toBe(2)

await act(() => setCount(increment))
await act(() => setCount(increment))

resolve[3]!()
resolve[4]!()

await macrotask()

expect(store.get(countAtom)).toBe(4)
expect(store.get(asyncCountAtom)).toBe(4) // 3
})

// TODO: How do we unit test this code with store?
it('[unit] resolves dependencies reliably after a delay', async () => {
const countAtom = atom(0)
const asyncCountAtom = atom(0)

const resolve: (() => void)[] = []
const asyncAtom = atom(async (get) => {
const count = get(countAtom)
await new Promise<void>((r) => resolve.push(r))
console.log(`resolved (${count})`)
return count
})

const derivedAsyncAtom = atom(
async (get, { setSelf }) => {
const count = get(countAtom)
await microtask()
console.log(`derived (${count})`)
const asyncCount = await get(asyncAtom)
console.log(`derived (${count})`, asyncCount)
setSelf(asyncCountAtom, asyncCount)
},
(_get, set, ...args: Parameters<Setter>) => set(...args)
)

const derivedSyncAtom = atom((get) => {
get(derivedAsyncAtom)
})

const store = createStore()
store.get(derivedSyncAtom)

expect(store.get(countAtom)).toBe(0)
expect(store.get(asyncCountAtom)).toBe(0)

store.set(countAtom, increment)
store.set(countAtom, increment)
await macrotask()

resolve[0]!()
// is it possible to do this test without react hooks?
resolve[1]!() // <--- Error: resolve[1] is not defined, it should be.
resolve[2]!()

await microtask()
await microtask()
await microtask()
await macrotask()

store.get(countAtom)
store.get(asyncCountAtom)

await act(() => store.set(countAtom, increment))
await act(() => store.set(countAtom, increment))

resolve[3]!()
resolve[4]!()

await microtask()
await microtask()
await microtask()
await macrotask()

expect(store.get(countAtom)).toBe(4)
expect(store.get(asyncCountAtom)).toBe(4) // 3
})

function macrotask() {
return new Promise((r) => setTimeout(r, 0))
}

async function microtask() {}

function increment(c: number) {
return c + 1
}
15 changes: 15 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1477,6 +1477,14 @@
lz-string "^1.5.0"
pretty-format "^27.0.2"

"@testing-library/react-hooks@^8.0.1":
version "8.0.1"
resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz#0924bbd5b55e0c0c0502d1754657ada66947ca12"
integrity sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==
dependencies:
"@babel/runtime" "^7.12.5"
react-error-boundary "^3.1.0"

"@testing-library/react@^14.0.0":
version "14.0.0"
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-14.0.0.tgz#59030392a6792450b9ab8e67aea5f3cc18d6347c"
Expand Down Expand Up @@ -4196,6 +4204,13 @@ react-dom@^18.2.0:
loose-envify "^1.1.0"
scheduler "^0.23.0"

react-error-boundary@^3.1.0:
version "3.1.4"
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0"
integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==
dependencies:
"@babel/runtime" "^7.12.5"

react-is@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
Expand Down

0 comments on commit 2af5f05

Please sign in to comment.