diff --git a/__tests__/atomEffect.strict.test.ts b/__tests__/atomEffect.strict.test.ts index 63da819..5428d34 100644 --- a/__tests__/atomEffect.strict.test.ts +++ b/__tests__/atomEffect.strict.test.ts @@ -1,4 +1,4 @@ -import { StrictMode, useEffect } from 'react' +import { StrictMode } from 'react' import { act, renderHook, waitFor } from '@testing-library/react' import { useAtom, useAtomValue, useSetAtom } from 'jotai/react' import { atom, getDefaultStore } from 'jotai/vanilla' @@ -7,26 +7,21 @@ import { assert, delay, increment, incrementLetter } from './test-utils' const wrapper = StrictMode -it('should run the effect on mount and cleanup on unmount once', async () => { +it('should run the effect on mount and cleanup on unmount once', () => { expect.assertions(5) const effect = { mount: 0, unmount: 0 } - let hasMounted = false const effectAtom = atomEffect(() => { effect.mount++ - hasMounted = true return () => { effect.unmount++ } }) - let hasRun = false function useTest() { - hasRun = true return useAtomValue(effectAtom) } const { result, rerender, unmount } = renderHook(useTest, { wrapper }) - await waitFor(() => assert(hasRun && hasMounted)) // effect does not return a value expect(result.current).toBe(undefined) // initial render should run the effect @@ -44,7 +39,7 @@ it('should run the effect on mount and cleanup on unmount once', async () => { expect(effect.unmount).toBe(1) }) -it('should run the effect on mount and cleanup on unmount and whenever countAtom changes', async () => { +it('should run the effect on mount and cleanup on unmount and whenever countAtom changes', () => { expect.assertions(11) const effect = { mount: 0, unmount: 0 } @@ -58,21 +53,16 @@ it('should run the effect on mount and cleanup on unmount and whenever countAtom } }) - let didMount = false function useTest() { const [count, setCount] = useAtom(countAtom) useAtomValue(effectAtom) - useEffect(() => { - didMount = true - }, [count]) return setCount } const { result, rerender, unmount } = renderHook(useTest, { wrapper }) - async function incrementCount() { + function incrementCount() { const setCount = result.current - await act(async () => setCount(increment)) + act(() => setCount(increment)) } - await waitFor(() => assert(didMount && effect.mount === 1)) // initial render should run the effect but not the cleanup expect(effect.unmount).toBe(0) @@ -83,13 +73,13 @@ it('should run the effect on mount and cleanup on unmount and whenever countAtom expect(effect.unmount).toBe(0) expect(effect.mount).toBe(1) - await incrementCount() + incrementCount() // changing the value should run the effect again and the previous cleanup expect(effect.unmount).toBe(1) expect(effect.mount).toBe(2) - await incrementCount() + incrementCount() // changing the value should run the effect again and the previous cleanup expect(effect.unmount).toBe(2) @@ -107,7 +97,7 @@ it('should run the effect on mount and cleanup on unmount and whenever countAtom expect(effect.unmount).toBe(3) }) -it('should not cause infinite loops when effect updates the watched atom', async () => { +it('should not cause infinite loops when effect updates the watched atom', () => { expect.assertions(1) const watchedAtom = atom(0) let runCount = 0 @@ -125,11 +115,8 @@ it('should not cause infinite loops when effect updates the watched atom', async } const { rerender } = renderHook(useTest, { wrapper }) - // initial render should run the effect once - await waitFor(() => assert(runCount === 1)) // rerender should not run the effect again rerender() - await delay(0) expect({ runCount, watched: store.get(watchedAtom) }).toEqual({ runCount: 1, @@ -151,7 +138,7 @@ it('should not cause infinite loops when effect updates the watched atom asynchr function useTest() { useAtom(effectAtom) const setCount = useSetAtom(watchedAtom) - return () => act(async () => setCount(increment)) + return () => act(() => setCount(increment)) } const { result } = renderHook(useTest, { wrapper }) await delay(0) @@ -164,7 +151,7 @@ it('should not cause infinite loops when effect updates the watched atom asynchr expect(runCount).toBe(2) }) -it('should allow synchronous infinite loops with opt-in for first run', async () => { +it('should allow synchronous infinite loops with opt-in for first run', () => { expect.assertions(1) let runCount = 0 const watchedAtom = atom(0) @@ -179,18 +166,16 @@ it('should allow synchronous infinite loops with opt-in for first run', async () function useTest() { useAtom(effectAtom, { store }) const setCount = useSetAtom(watchedAtom, { store }) - return () => act(async () => setCount(increment)) + return () => act(() => setCount(increment)) } const { result } = renderHook(useTest, { wrapper }) - await delay(0) - await act(async () => result.current()) - await delay(0) + act(() => result.current()) expect({ runCount, watched: store.get(watchedAtom) }).toEqual({ runCount: 7, // extra run for strict mode render watched: 6, }) }) -it('should conditionally run the effect and cleanup when effectAtom is unmounted', async () => { +it('should conditionally run the effect and cleanup when effectAtom is unmounted', () => { expect.assertions(6) const booleanAtom = atom(false) @@ -215,19 +200,19 @@ it('should conditionally run the effect and cleanup when effectAtom is unmounted const { result } = renderHook(useTest, { wrapper }) const setBoolean = result.current - const toggleBoolean = () => act(async () => setBoolean((prev) => !prev)) + const toggleBoolean = () => act(() => setBoolean((prev) => !prev)) // Initially the effectAtom should not run as booleanAtom is false expect(effectRunCount).toBe(0) expect(cleanupRunCount).toBe(0) // Set booleanAtom to true, so effectAtom should run - await toggleBoolean() + toggleBoolean() expect(effectRunCount).toBe(1) expect(cleanupRunCount).toBe(0) // Set booleanAtom to false, so effectAtom should cleanup - await toggleBoolean() + toggleBoolean() expect(effectRunCount).toBe(1) expect(cleanupRunCount).toBe(1) }) @@ -384,7 +369,7 @@ describe('should correctly process synchronous updates to the same atom', () => it.each(solutions)( 'should correctly process synchronous updates when effectIncrementCountBy is $effectIncrementCountBy and incrementCountBy is $incrementCountBy', - async ({ effectIncrementCountBy, incrementCountBy, runs }) => { + ({ effectIncrementCountBy, incrementCountBy, runs }) => { expect.assertions(3) const { result, runCount } = setup({ effectIncrementCountBy, @@ -393,14 +378,11 @@ describe('should correctly process synchronous updates to the same atom', () => const [before, after] = runs - // initial value after $effectIncrementCountBy synchronous updates in the effect - await waitFor(() => assert(runCount.current === before.runCount)) - // initial render should run the effect once expect(runCount.current).toBe(before.runCount) // perform $incrementCountBy synchronous updates - await act(async () => result.current.incrementCount()) + act(() => result.current.incrementCount()) // final value after synchronous updates and rerun of the effect expect(result.current.count).toBe(after.resultCount) @@ -410,7 +392,7 @@ describe('should correctly process synchronous updates to the same atom', () => ) }) -it('should not batch effect setStates', async () => { +it('should not batch effect setStates', () => { expect.assertions(4) const valueAtom = atom(0) const runCount = { current: 0 } @@ -432,17 +414,17 @@ it('should not batch effect setStates', async () => { const { result } = renderHook(() => useSetAtom(triggerAtom), { wrapper }) const setTrigger = result.current - await waitFor(() => assert(runCount.current === 1)) + waitFor(() => assert(runCount.current === 1)) expect(valueResult.current).toBe(0) expect(runCount.current).toBe(1) - await act(async () => setTrigger((x) => !x)) + act(() => setTrigger((x) => !x)) expect(valueResult.current).toBe(2) expect(runCount.current).toBe(3) // <--- not batched (we would expect runCount to be 2 if batched) }) -it('should batch synchronous updates as a single transaction', async () => { +it('should batch synchronous updates as a single transaction', () => { expect.assertions(4) const lettersAtom = atom('a') lettersAtom.debugLabel = 'lettersAtom' @@ -450,6 +432,10 @@ it('should batch synchronous updates as a single transaction', async () => { numbersAtom.debugLabel = 'numbersAtom' const lettersAndNumbersAtom = atom([] as string[]) lettersAndNumbersAtom.debugLabel = 'lettersAndNumbersAtom' + const setLettersAndNumbersAtom = atom(null, (_get, set) => { + set(lettersAtom, incrementLetter) + set(numbersAtom, increment) + }) let runCount = 0 const effectAtom = atomEffect((get, set) => { runCount++ @@ -462,30 +448,30 @@ it('should batch synchronous updates as a single transaction', async () => { }) function useTest() { useAtomValue(effectAtom) - const setLetters = useSetAtom(lettersAtom) - const setNumbers = useSetAtom(numbersAtom) const lettersAndNumbers = useAtomValue(lettersAndNumbersAtom) - return { setLetters, setNumbers, lettersAndNumbers } + const setLettersAndNumbers = useSetAtom(setLettersAndNumbersAtom) + return { setLettersAndNumbers, lettersAndNumbers } } const { result } = renderHook(useTest, { wrapper }) - const { setLetters, setNumbers } = result.current - await waitFor(() => assert(!!runCount)) + const { setLettersAndNumbers } = result.current expect(runCount).toBe(1) expect(result.current.lettersAndNumbers).toEqual(['a0']) - await act(async () => { - setLetters(incrementLetter) - setNumbers(increment) - }) + act(setLettersAndNumbers) expect(runCount).toBe(2) expect(result.current.lettersAndNumbers).toEqual(['a0', 'b1']) }) -it('should run the effect once even if the effect is mounted multiple times', async () => { +it('should run the effect once even if the effect is mounted multiple times', () => { expect.assertions(3) const lettersAtom = atom('a') lettersAtom.debugLabel = 'lettersAtom' const numbersAtom = atom(0) numbersAtom.debugLabel = 'numbersAtom' + const setLettersAndNumbersAtom = atom(null, (_get, set) => { + set(lettersAtom, incrementLetter) + set(numbersAtom, increment) + }) + setLettersAndNumbersAtom.debugLabel = 'setLettersAndNumbersAtom' let runCount = 0 const effectAtom = atomEffect((get) => { runCount++ @@ -515,23 +501,14 @@ it('should run the effect once even if the effect is mounted multiple times', as useAtomValue(derivedAtom2) useAtomValue(derivedAtom3) useAtomValue(derivedAtom4) - const setLetters = useSetAtom(lettersAtom) - const setNumbers = useSetAtom(numbersAtom) - return { setLetters, setNumbers } + return useSetAtom(setLettersAndNumbersAtom) } const { result } = renderHook(useTest, { wrapper }) - const { setLetters, setNumbers } = result.current - await waitFor(() => assert(!!runCount)) + const setLettersAndNumbers = result.current expect(runCount).toBe(1) - await act(async () => { - setLetters(incrementLetter) - setNumbers(increment) - }) + act(setLettersAndNumbers) expect(runCount).toBe(2) - await act(async () => { - setLetters(incrementLetter) - setNumbers(increment) - }) + act(setLettersAndNumbers) expect(runCount).toBe(3) }) @@ -574,7 +551,6 @@ it('should abort the previous promise', async () => { async function resolveAll() { resolves.forEach((resolve) => resolve()) resolves.length = 0 - await delay(0) } function useTest() { useAtomValue(effectAtom) @@ -582,20 +558,19 @@ it('should abort the previous promise', async () => { } const { result } = renderHook(useTest, { wrapper }) const setCount = result.current - await waitFor(() => assert(!!runCount)) await resolveAll() expect(runCount).toBe(1) expect(abortedRuns).toEqual([]) expect(completedRuns).toEqual([0]) - await act(async () => setCount(increment)) + act(() => setCount(increment)) expect(runCount).toBe(2) expect(abortedRuns).toEqual([]) expect(completedRuns).toEqual([0]) // aborted run - await act(async () => setCount(increment)) + act(() => setCount(increment)) expect(runCount).toBe(3) expect(abortedRuns).toEqual([1]) expect(completedRuns).toEqual([0]) @@ -606,7 +581,7 @@ it('should abort the previous promise', async () => { expect(completedRuns).toEqual([0, 2]) }) -it('should not run the effect when the effectAtom is unmounted', async () => { +it('should not run the effect when the effectAtom is unmounted', () => { const countAtom = atom(0) let runCount = 0 const effectAtom = atomEffect((get) => { @@ -619,8 +594,7 @@ it('should not run the effect when the effectAtom is unmounted', async () => { } const { result } = renderHook(useTest, { wrapper }) const setCount = result.current - await delay(0) expect(runCount).toBe(1) - await act(() => setCount(increment)) + act(() => setCount(increment)) expect(runCount).toBe(2) }) diff --git a/__tests__/atomEffect.test.tsx b/__tests__/atomEffect.test.tsx index f377d79..8c9df45 100644 --- a/__tests__/atomEffect.test.tsx +++ b/__tests__/atomEffect.test.tsx @@ -11,7 +11,7 @@ import { incrementLetter, } from './test-utils' -it('should run the effect on vanilla store', async () => { +it('should run the effect on vanilla store', () => { const store = getDefaultStore() const countAtom = atom(0) const effectAtom = atomEffect((_, set) => { @@ -21,42 +21,26 @@ it('should run the effect on vanilla store', async () => { } }) const unsub = store.sub(effectAtom, () => void 0) - expect(store.get(countAtom)).toBe(0) - await waitFor(() => expect(store.get(countAtom)).toBe(1)) + expect(store.get(countAtom)).toBe(1) unsub() - await waitFor(() => expect(store.get(countAtom)).toBe(0)) -}) - -it('should not call effect if immediately unsubscribed', async () => { - expect.assertions(1) - const store = getDefaultStore() - const effect = jest.fn() - const effectAtom = atomEffect(effect) - const unsub = store.sub(effectAtom, () => void 0) - unsub() - expect(effect).not.toHaveBeenCalled() + expect(store.get(countAtom)).toBe(0) }) -it('should run the effect on mount and cleanup on unmount once', async () => { +it('should run the effect on mount and cleanup on unmount once', () => { expect.assertions(5) const effect = { mount: 0, unmount: 0 } - let hasMounted = false const effectAtom = atomEffect(() => { effect.mount++ - hasMounted = true return () => { effect.unmount++ } }) - let hasRun = false function useTest() { - hasRun = true return useAtomValue(effectAtom) } const { result, rerender, unmount } = renderHook(useTest) - await waitFor(() => assert(hasRun && hasMounted)) // effect does not return a value expect(result.current).toBe(undefined) @@ -75,7 +59,7 @@ it('should run the effect on mount and cleanup on unmount once', async () => { expect(effect.unmount).toBe(1) }) -it('should run the effect on mount and cleanup on unmount and whenever countAtom changes', async () => { +it('should run the effect on mount and cleanup on unmount and whenever countAtom changes', () => { expect.assertions(11) const effect = { mount: 0, unmount: 0 } @@ -99,11 +83,11 @@ it('should run the effect on mount and cleanup on unmount and whenever countAtom return setCount } const { result, rerender, unmount } = renderHook(useTest) - async function incrementCount() { + function incrementCount() { const setCount = result.current - await act(async () => setCount(increment)) + setCount(increment) } - await waitFor(() => assert(didMount && effect.mount === 1)) + waitFor(() => assert(didMount && effect.mount === 1)) // initial render should run the effect but not the cleanup expect(effect.unmount).toBe(0) @@ -114,13 +98,13 @@ it('should run the effect on mount and cleanup on unmount and whenever countAtom expect(effect.unmount).toBe(0) expect(effect.mount).toBe(1) - await incrementCount() + incrementCount() // changing the value should run the effect again and the previous cleanup expect(effect.unmount).toBe(1) expect(effect.mount).toBe(2) - await incrementCount() + incrementCount() // changing the value should run the effect again and the previous cleanup expect(effect.unmount).toBe(2) @@ -138,7 +122,7 @@ it('should run the effect on mount and cleanup on unmount and whenever countAtom expect(effect.unmount).toBe(3) }) -it('should not cause infinite loops when effect updates the watched atom', async () => { +it('should not cause infinite loops when effect updates the watched atom', () => { expect.assertions(2) const watchedAtom = atom(0) let runCount = 0 @@ -150,17 +134,15 @@ it('should not cause infinite loops when effect updates the watched atom', async const store = getDefaultStore() store.sub(effectAtom, () => void 0) - const incrementWatched = async () => store.set(watchedAtom, increment) - await delay(0) + const incrementWatched = () => store.set(watchedAtom, increment) // initial render should run the effect once - await waitFor(() => assert(runCount === 1)) expect(runCount).toBe(1) // changing the value should run the effect again one time - await incrementWatched() + incrementWatched() expect(runCount).toBe(2) }) -it('should not cause infinite loops when effect updates the watched atom asynchronous', async () => { +it('should not cause infinite loops when effect updates the watched atom asynchronous', () => { expect.assertions(1) const watchedAtom = atom(0) let runCount = 0 @@ -173,41 +155,32 @@ it('should not cause infinite loops when effect updates the watched atom asynchr }) const store = getDefaultStore() store.sub(effectAtom, () => void 0) - await delay(0) - // initial render should run the effect once - await waitFor(() => assert(runCount === 1)) - // changing the value should run the effect again one time store.set(watchedAtom, increment) - - await delay(0) expect(runCount).toBe(2) }) -it('should allow synchronous recursion with set.recurse for first run', async () => { +it('should allow synchronous recursion with set.recurse for first run', () => { expect.assertions(1) let runCount = 0 const watchedAtom = atom(0) - let done = false const effectAtom = atomEffect((get, { recurse }) => { const value = get(watchedAtom) runCount++ if (value >= 3) { - done = true return } recurse(watchedAtom, increment) }) const store = getDefaultStore() store.sub(effectAtom, () => void 0) - await waitFor(() => assert(done)) expect({ runCount, watched: store.get(watchedAtom) }).toEqual({ runCount: 4, watched: 3, }) }) -it('should allow synchronous recursion with set.recurse', async () => { +it('should allow synchronous recursion with set.recurse', () => { expect.assertions(2) let runCount = 0 const watchedAtom = atom(0) @@ -224,14 +197,12 @@ it('should allow synchronous recursion with set.recurse', async () => { }) const store = getDefaultStore() store.sub(effectAtom, () => void 0) - await delay(0) store.set(watchedAtom, increment) - await waitFor(() => assert(store.get(watchedAtom) === 5)) expect(store.get(watchedAtom)).toBe(5) expect(runCount).toBe(6) }) -it('should allow multiple synchronous recursion with set.recurse', async () => { +it('should allow multiple synchronous recursion with set.recurse', () => { expect.assertions(1) let runCount = 0 const watchedAtom = atom(0) @@ -249,16 +220,14 @@ it('should allow multiple synchronous recursion with set.recurse', async () => { }) const store = getDefaultStore() store.sub(effectAtom, () => void 0) - await delay(0) store.set(watchedAtom, increment) - await delay(0) expect({ runCount, value: store.get(watchedAtom) }).toEqual({ runCount: 6, value: 5, }) }) -it('should batch updates during synchronous recursion with set.recurse', async () => { +it('should batch updates during synchronous recursion with set.recurse', () => { expect.assertions(2) let runCount = 0 const lettersAtom = atom('a') @@ -288,9 +257,7 @@ it('should batch updates during synchronous recursion with set.recurse', async ( }) const store = getDefaultStore() store.sub(effectAtom, () => void 0) - await delay(0) store.set(watchedAtom, increment) - await delay(0) expect(store.get(lettersAndNumbersAtom)).toEqual(['a0', 'b1']) expect(runCount).toBe(4) }) @@ -335,12 +302,12 @@ it('should allow asynchronous recursion with microtask delay with set.recurse', }) const store = getDefaultStore() store.sub(effectAtom, () => void 0) - await delay(500) + await waitFor(() => assert(store.get(watchedAtom) >= 3)) expect(store.get(watchedAtom)).toBe(3) expect(runCount).toBe(4) }) -it('should work with both set.recurse and set', async () => { +it('should work with both set.recurse and set', () => { expect.assertions(3) let runCount = 0 const watchedAtom = atom(0) @@ -358,14 +325,13 @@ it('should work with both set.recurse and set', async () => { }) const store = getDefaultStore() store.sub(effectAtom, () => void 0) - await waitFor(() => assert(store.get(countAtom) === 3)) expect(store.get(countAtom)).toBe(3) expect(store.get(watchedAtom)).toBe(4) expect(runCount).toBe(4) }) -it('should disallow synchronous set.recurse in cleanup', async () => { - expect.assertions(2) +it('should disallow synchronous set.recurse in cleanup', () => { + expect.assertions(1) const watchedAtom = atom(0) const anotherAtom = atom(0) let cleanup @@ -379,18 +345,15 @@ it('should disallow synchronous set.recurse in cleanup', async () => { }) const store = getDefaultStore() store.sub(effectAtom, () => void 0) - await delay(0) store.set(anotherAtom, increment) - await delay(0) - expect(store.get(watchedAtom)).toBe(0) - expect(() => store.get(effectAtom)).toThrowError( + expect(() => store.set(anotherAtom, increment)).toThrowError( 'set.recurse is not allowed in cleanup' ) }) // FIXME: is there a way to disallow asynchronous infinite loops in cleanup? -it('should return value from set.recurse', async () => { +it('should return value from set.recurse', () => { expect.assertions(1) const countAtom = atom(0) const incrementCountAtom = atom(null, (get, set) => { @@ -398,23 +361,20 @@ it('should return value from set.recurse', async () => { return get(countAtom) }) const results = [] as number[] - let done = false const effectAtom = atomEffect((get, { recurse }) => { const value = get(countAtom) if (value < 5) { const result = recurse(incrementCountAtom) results.unshift(result) - done = true return } }) const store = getDefaultStore() store.sub(effectAtom, () => void 0) - await waitFor(() => assert(done)) expect(results).toEqual([1, 2, 3, 4, 5]) }) -it('should conditionally run the effect and cleanup when effectAtom is unmounted', async () => { +it('should conditionally run the effect and cleanup when effectAtom is unmounted', () => { expect.assertions(6) const booleanAtom = atom(false) @@ -439,19 +399,19 @@ it('should conditionally run the effect and cleanup when effectAtom is unmounted const { result } = renderHook(useTest) const setBoolean = result.current - const toggleBoolean = () => act(async () => setBoolean((prev) => !prev)) + const toggleBoolean = () => setBoolean((prev) => !prev) // Initially the effectAtom should not run as booleanAtom is false expect(effectRunCount).toBe(0) expect(cleanupRunCount).toBe(0) // Set booleanAtom to true, so effectAtom should run - await toggleBoolean() + toggleBoolean() expect(effectRunCount).toBe(1) expect(cleanupRunCount).toBe(0) // Set booleanAtom to false, so effectAtom should cleanup - await toggleBoolean() + toggleBoolean() expect(effectRunCount).toBe(1) expect(cleanupRunCount).toBe(1) }) @@ -522,6 +482,7 @@ describe('should correctly process synchronous updates to the same atom', () => // 2. incrementing count: count = 1 // 3. incrementing count: count = 2 // 4. incrementing count reruns the effect (batched): run = 2 + // FIXME: understand why run-result: 3-2 effectIncrementCountBy: 0, incrementCountBy: 2, runs: [ @@ -560,6 +521,7 @@ describe('should correctly process synchronous updates to the same atom', () => // 4. incrementing count: count = 3 // 5. incrementing count reruns the effect (batched): run = 2 // 6. effect increments count: count = 4 + // FIXME: understand why run-result: 3-5 effectIncrementCountBy: 1, incrementCountBy: 2, runs: [ @@ -597,6 +559,7 @@ describe('should correctly process synchronous updates to the same atom', () => // 4. incrementing count: count = 4 // 5. incrementing count reruns the effect (batched): run = 2 // 6. effect increments count by two: count = 6 + // FIXME: understand why run-result: 3-8 effectIncrementCountBy: 2, incrementCountBy: 2, runs: [ @@ -654,8 +617,6 @@ it('should not batch effect setStates', async () => { const { result } = renderHook(() => useSetAtom(triggerAtom)) const setTrigger = result.current - await waitFor(() => assert(runCount.current === 1)) - expect(valueResult.current).toBe(0) expect(runCount.current).toBe(1) @@ -664,7 +625,7 @@ it('should not batch effect setStates', async () => { expect(runCount.current).toBe(3) // <--- not batched (we would expect runCount to be 2 if batched) }) -it('should batch synchronous updates as a single transaction', async () => { +it('should batch synchronous updates as a single transaction', () => { expect.assertions(4) const lettersAtom = atom('a') lettersAtom.debugLabel = 'lettersAtom' @@ -685,23 +646,25 @@ it('should batch synchronous updates as a single transaction', async () => { const store = getDefaultStore() store.sub(effectAtom, () => void 0) - await waitFor(() => assert(!!runCount)) expect(runCount).toBe(1) expect(store.get(lettersAndNumbersAtom)).toEqual(['a0']) - await act(async () => { - store.set(lettersAtom, incrementLetter) - store.set(numbersAtom, increment) + const w = atom(null, (_get, set) => { + set(lettersAtom, incrementLetter) + set(numbersAtom, increment) }) + store.set(w) expect(runCount).toBe(2) expect(store.get(lettersAndNumbersAtom)).toEqual(['a0', 'b1']) }) -it('should run the effect once even if the effect is mounted multiple times', async () => { +it('should run the effect once even if the effect is mounted multiple times', () => { expect.assertions(3) const lettersAtom = atom('a') - lettersAtom.debugLabel = 'lettersAtom' const numbersAtom = atom(0) - numbersAtom.debugLabel = 'numbersAtom' + const lettersAndNumbersAtom = atom(null, (_get, set) => { + set(lettersAtom, incrementLetter) + set(numbersAtom, increment) + }) let runCount = 0 const effectAtom = atomEffect((get) => { runCount++ @@ -731,23 +694,14 @@ it('should run the effect once even if the effect is mounted multiple times', as useAtomValue(derivedAtom2) useAtomValue(derivedAtom3) useAtomValue(derivedAtom4) - const setLetters = useSetAtom(lettersAtom) - const setNumbers = useSetAtom(numbersAtom) - return { setLetters, setNumbers } + return useSetAtom(lettersAndNumbersAtom) } const { result } = renderHook(useTest) - const { setLetters, setNumbers } = result.current - await waitFor(() => assert(!!runCount)) + const setLettersAndNumbers = result.current expect(runCount).toBe(1) - await act(async () => { - setLetters(incrementLetter) - setNumbers(increment) - }) + act(setLettersAndNumbers) expect(runCount).toBe(2) - await act(async () => { - setLetters(incrementLetter) - setNumbers(increment) - }) + act(setLettersAndNumbers) expect(runCount).toBe(3) }) @@ -790,7 +744,6 @@ it('should abort the previous promise', async () => { async function resolveAll() { resolves.forEach((resolve) => resolve()) resolves.length = 0 - await delay(0) } function useTest() { useAtomValue(effectAtom) @@ -798,20 +751,19 @@ it('should abort the previous promise', async () => { } const { result } = renderHook(useTest) const setCount = result.current - await waitFor(() => assert(!!runCount)) await resolveAll() expect(runCount).toBe(1) expect(abortedRuns).toEqual([]) expect(completedRuns).toEqual([0]) - await act(async () => setCount(increment)) + act(() => setCount(increment)) expect(runCount).toBe(2) expect(abortedRuns).toEqual([]) expect(completedRuns).toEqual([0]) // aborted run - await act(async () => setCount(increment)) + act(() => setCount(increment)) expect(runCount).toBe(3) expect(abortedRuns).toEqual([1]) expect(completedRuns).toEqual([0]) @@ -835,10 +787,12 @@ it('should not infinite loop with nested atomEffects', async () => { return () => ++metrics.unmounted } + let delayedIncrement = false const effectAtom = atomEffect((_get, set) => { ++metrics.runCount1 if (metrics.runCount1 > 1) throw new Error('infinite loop') Promise.resolve().then(() => { + delayedIncrement = true set(countAtom, increment) }) }) @@ -856,7 +810,7 @@ it('should not infinite loop with nested atomEffects', async () => { const store = getDefaultStore() store.sub(effect2Atom, () => void 0) - await waitFor(() => assert(!!metrics.runCount1)) + await waitFor(() => assert(delayedIncrement)) if (!('dev4_get_mounted_atoms' in store)) return const atomSet = new Set(store.dev4_get_mounted_atoms()) @@ -878,7 +832,7 @@ it('should not infinite loop with nested atomEffects', async () => { }) }) -it('should not rerun with get.peek', async () => { +it('should not rerun with get.peek', () => { expect.assertions(1) const countAtom = atom(0) let runCount = 0 @@ -888,15 +842,12 @@ it('should not rerun with get.peek', async () => { }) const store = getDefaultStore() store.sub(effectAtom, () => void 0) - await waitFor(() => assert(runCount === 1)) store.set(countAtom, increment) - await delay(0) expect(runCount).toBe(1) }) it('should trigger the error boundary when an error is thrown', async () => { expect.assertions(1) - const effectAtom = atomEffect((_get, _set) => { throw new Error('effect error') }) @@ -913,7 +864,13 @@ it('should trigger the error boundary when an error is thrown', async () => { /> ) } - render(, { wrapper }) + const originalConsoleError = console.error + try { + console.error = jest.fn() + render(, { wrapper }) + } finally { + console.error = originalConsoleError + } await waitFor(() => assert(didThrow)) expect(didThrow).toBe(true) }) @@ -950,12 +907,17 @@ it('should trigger an error boundary when an error is thrown in a cleanup', asyn ) } render(, { wrapper }) - await delay(0) - act(() => store.set(refreshAtom, increment)) + const originalConsoleError = console.error + try { + console.error = jest.fn() + act(() => store.set(refreshAtom, increment)) + } finally { + console.error = originalConsoleError + } await waitFor(() => assert(didThrow)) }) -it('should not suspend the component', async () => { +it('should not suspend the component', () => { const countAtom = atom(0) const watchCounterEffect = atomEffect((get) => { get(countAtom) diff --git a/__tests__/withAtomEffect.test.ts b/__tests__/withAtomEffect.test.ts index 8813cdd..5db5524 100644 --- a/__tests__/withAtomEffect.test.ts +++ b/__tests__/withAtomEffect.test.ts @@ -3,7 +3,6 @@ import { useAtomValue } from 'jotai/react' import { atom, createStore } from 'jotai/vanilla' import { atomEffect } from '../src/atomEffect' import { withAtomEffect } from '../src/withAtomEffect' -import { delay } from './test-utils' describe('withAtomEffect', () => { it('ensures readonly atoms remain readonly', () => { @@ -29,7 +28,7 @@ describe('withAtomEffect', () => { expect(store.get(enhancedAtom)).toBe(6) }) - it('calls effect on initial use and on dependencies change of the base atom', async () => { + it('calls effect on initial use and on dependencies change of the base atom', () => { const baseAtom = atom(0) const effectMock = jest.fn() const enhancedAtom = withAtomEffect(baseAtom, (get) => { @@ -38,14 +37,12 @@ describe('withAtomEffect', () => { }) const store = createStore() store.sub(enhancedAtom, () => {}) - await Promise.resolve() expect(effectMock).toHaveBeenCalledTimes(1) store.set(enhancedAtom, 1) - await Promise.resolve() expect(effectMock).toHaveBeenCalledTimes(2) }) - it('calls effect on initial use and on dependencies change of the enhanced atom', async () => { + it('calls effect on initial use and on dependencies change of the enhanced atom', () => { const baseAtom = atom(0) const effectMock = jest.fn() const enhancedAtom = withAtomEffect(baseAtom, (get) => { @@ -54,14 +51,12 @@ describe('withAtomEffect', () => { }) const store = createStore() store.sub(enhancedAtom, () => {}) - await Promise.resolve() expect(effectMock).toHaveBeenCalledTimes(1) store.set(enhancedAtom, 1) - await Promise.resolve() expect(effectMock).toHaveBeenCalledTimes(2) }) - it('cleans up when the atom is no longer in use', async () => { + it('cleans up when the atom is no longer in use', () => { const cleanupMock = jest.fn() const baseAtom = atom(0) const mountMock = jest.fn() @@ -73,10 +68,8 @@ describe('withAtomEffect', () => { }) const store = createStore() const unsubscribe = store.sub(enhancedAtom, () => {}) - await Promise.resolve() expect(mountMock).toHaveBeenCalledTimes(1) unsubscribe() - await Promise.resolve() expect(cleanupMock).toHaveBeenCalledTimes(1) }) @@ -88,21 +81,21 @@ describe('withAtomEffect', () => { expect(enhancedAtom.read).not.toBe(read) }) - it('does not cause infinite loops when it references itself', async () => { + it('does not cause infinite loops when it references itself', () => { const countWithEffectAtom = withAtomEffect(atom(0), (get, set) => { get(countWithEffectAtom) set(countWithEffectAtom, (v) => v + 1) }) const store = createStore() store.sub(countWithEffectAtom, () => {}) - await Promise.resolve() expect(store.get(countWithEffectAtom)).toBe(1) - store.set(countWithEffectAtom, (v) => ++v) - await Promise.resolve() + store.set(countWithEffectAtom, (v) => { + return v + 1 + }) expect(store.get(countWithEffectAtom)).toBe(3) }) - it('can change the effect of the enhanced atom', async () => { + it('can change the effect of the enhanced atom', () => { const baseAtom = atom(0) const effectA = jest.fn((get) => { get(enhancedAtom) @@ -111,22 +104,19 @@ describe('withAtomEffect', () => { expect(enhancedAtom.effect).toBe(effectA) const store = createStore() store.sub(enhancedAtom, () => {}) - await Promise.resolve() effectA.mockClear() store.set(enhancedAtom, (v) => ++v) - await Promise.resolve() expect(effectA).toHaveBeenCalledTimes(1) effectA.mockClear() const effectB = jest.fn((get) => get(baseAtom)) enhancedAtom.effect = effectB expect(enhancedAtom.effect).toBe(effectB) store.set(enhancedAtom, (v) => ++v) - await Promise.resolve() expect(effectA).not.toHaveBeenCalled() expect(effectB).toHaveBeenCalledTimes(1) }) - it('runs the cleanup function the same number of times as the effect function', async () => { + it('runs the cleanup function the same number of times as the effect function', () => { const baseAtom = atom(0) const effectMock = jest.fn() const cleanupMock = jest.fn() @@ -139,20 +129,17 @@ describe('withAtomEffect', () => { }) const store = createStore() const unsub = store.sub(enhancedAtom, () => {}) - await Promise.resolve() expect(effectMock).toHaveBeenCalledTimes(1) expect(cleanupMock).toHaveBeenCalledTimes(0) store.set(enhancedAtom, 1) - await Promise.resolve() expect(effectMock).toHaveBeenCalledTimes(2) expect(cleanupMock).toHaveBeenCalledTimes(1) unsub() - await Promise.resolve() expect(effectMock).toHaveBeenCalledTimes(2) expect(cleanupMock).toHaveBeenCalledTimes(2) }) - it('runs the cleanup function the same number of times as the effect function in React', async () => { + it('runs the cleanup function the same number of times as the effect function in React', () => { const baseAtom = atom(0) const effectMock1 = jest.fn() const cleanupMock1 = jest.fn() @@ -183,21 +170,21 @@ describe('withAtomEffect', () => { useAtomValue(enhancedAtom2, { store }) } const { unmount } = renderHook(Test) - await waitFor(() => expect(effectMock1).toHaveBeenCalledTimes(1)) - await waitFor(() => expect(effectMock2).toHaveBeenCalledTimes(1)) + expect(effectMock1).toHaveBeenCalledTimes(1) + expect(effectMock2).toHaveBeenCalledTimes(1) expect(cleanupMock1).toHaveBeenCalledTimes(0) expect(cleanupMock2).toHaveBeenCalledTimes(0) act(() => store.set(baseAtom, 1)) - await waitFor(() => expect(effectMock1).toHaveBeenCalledTimes(2)) - await waitFor(() => expect(effectMock2).toHaveBeenCalledTimes(2)) + expect(effectMock1).toHaveBeenCalledTimes(2) + expect(effectMock2).toHaveBeenCalledTimes(2) expect(cleanupMock1).toHaveBeenCalledTimes(1) expect(cleanupMock2).toHaveBeenCalledTimes(1) act(unmount) - await waitFor(() => expect(cleanupMock1).toHaveBeenCalledTimes(2)) - await waitFor(() => expect(cleanupMock2).toHaveBeenCalledTimes(2)) + expect(cleanupMock1).toHaveBeenCalledTimes(2) + expect(cleanupMock2).toHaveBeenCalledTimes(2) }) - it('calculates price and discount', async () => { + it('calculates price and discount', () => { // https://github.com/pmndrs/jotai/discussions/2876 /* How can be implemented an atom to hold either a value or a calculated value at the same time? @@ -273,22 +260,18 @@ describe('withAtomEffect', () => { const store = createStore() store.sub(priceAndDiscount, () => void 0) - await delay(0) expect(store.get(priceAtom)).toBe(100) // value expect(store.get(discountAtom)).toBe(0) // (100-100)/100*100 = 0) store.set(discountAtom, 20) - await delay(0) expect(store.get(priceAtom)).toBe(80) // 100*(1-20/100) = 80) expect(store.get(discountAtom)).toBe(20) // value store.set(priceAtom, 50) - await delay(0) expect(store.get(priceAtom)).toBe(50) // value expect(store.get(discountAtom)).toBe(50) // (100-50)/100*100 = 50) store.set(unitPriceAtom, 200) - await delay(0) expect(store.get(priceAtom)).toBe(100) // 200*(1-50/100) = 100) expect(store.get(discountAtom)).toBe(50) // (200-100)/200*100 = 50) })