Skip to main content
Module

x/jotai/tests/error.test.tsx

👻 Primitive and flexible state management for React
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
import { Component, Suspense, useEffect, useState } from 'react'import type { ReactNode } from 'react'import { fireEvent, render, waitFor } from '@testing-library/react'import { atom, useAtom } from 'jotai'import { getTestProvider, itSkipIfVersionedWrite } from './testUtils'
const Provider = getTestProvider()
const consoleError = console.errorconst errorMessages: string[] = []beforeEach(() => { errorMessages.splice(0) console.error = jest.fn((err) => { const match = /^(.*?)(\n|$)/.exec(err) if (match?.[1]) { errorMessages.push(match[1]) } })})afterEach(() => { console.error = consoleError})
class ErrorBoundary extends Component< { message?: string; children: ReactNode }, { hasError: boolean }> { constructor(props: { message?: string; children: ReactNode }) { super(props) this.state = { hasError: false } } static getDerivedStateFromError() { return { hasError: true } } render() { return this.state.hasError ? ( <div> {this.props.message || 'errored'} <button onClick={() => this.setState({ hasError: false })}> retry </button> </div> ) : ( this.props.children ) }}
it('can throw an initial error in read function', async () => { const errorAtom = atom(() => { throw new Error() })
const Counter = () => { useAtom(errorAtom) return ( <> <div>no error</div> </> ) }
const { findByText } = render( <Provider> <ErrorBoundary> <Counter /> </ErrorBoundary> </Provider> )
await findByText('errored')})
it('can throw an error in read function', async () => { const countAtom = atom(0) const errorAtom = atom((get) => { if (get(countAtom) === 0) { return 0 } throw new Error() })
const Counter = () => { const [, setCount] = useAtom(countAtom) const [count] = useAtom(errorAtom) return ( <> <div>count: {count}</div> <div>no error</div> <button onClick={() => setCount((c) => c + 1)}>button</button> </> ) }
const { getByText, findByText } = render( <Provider> <ErrorBoundary> <Counter /> </ErrorBoundary> </Provider> )
await findByText('no error')
fireEvent.click(getByText('button')) await findByText('errored')})
it('can throw an initial chained error in read function', async () => { const errorAtom = atom(() => { throw new Error() }) const derivedAtom = atom((get) => get(errorAtom))
const Counter = () => { useAtom(derivedAtom) return ( <> <div>no error</div> </> ) }
const { findByText } = render( <Provider> <ErrorBoundary> <Counter /> </ErrorBoundary> </Provider> )
await findByText('errored')})
it('can throw a chained error in read function', async () => { const countAtom = atom(0) const errorAtom = atom((get) => { if (get(countAtom) === 0) { return 0 } throw new Error() }) const derivedAtom = atom((get) => get(errorAtom))
const Counter = () => { const [, setCount] = useAtom(countAtom) const [count] = useAtom(derivedAtom) return ( <> <div>count: {count}</div> <div>no error</div> <button onClick={() => setCount((c) => c + 1)}>button</button> </> ) }
const { getByText, findByText } = render( <Provider> <ErrorBoundary> <Counter /> </ErrorBoundary> </Provider> )
await findByText('no error')
fireEvent.click(getByText('button')) await findByText('errored')})
it('can throw an initial error in async read function', async () => { const errorAtom = atom(async () => { throw new Error() })
const Counter = () => { useAtom(errorAtom) return ( <> <div>no error</div> </> ) }
const { findByText } = render( <Provider> <ErrorBoundary> <Suspense fallback={null}> <Counter /> </Suspense> </ErrorBoundary> </Provider> )
await findByText('errored')})
it('can throw an error in async read function', async () => { const countAtom = atom(0) const errorAtom = atom(async (get) => { if (get(countAtom) === 0) { return 0 } throw new Error() })
const Counter = () => { const [, setCount] = useAtom(countAtom) const [count] = useAtom(errorAtom) return ( <> <div>count: {count}</div> <div>no error</div> <button onClick={() => setCount((c) => c + 1)}>button</button> </> ) }
const { getByText, findByText } = render( <Provider> <ErrorBoundary> <Suspense fallback={null}> <Counter /> </Suspense> </ErrorBoundary> </Provider> )
await findByText('no error')
fireEvent.click(getByText('button')) await findByText('errored')})
itSkipIfVersionedWrite('can throw an error in write function', async () => { const countAtom = atom(0) const errorAtom = atom( (get) => get(countAtom), () => { throw new Error('error_in_write_function') } )
const Counter = () => { const [count, dispatch] = useAtom(errorAtom) const onClick = () => { try { dispatch() } catch (e) { console.error(e) } } return ( <> <div>count: {count}</div> <div>no error</div> <button onClick={onClick}>button</button> </> ) }
const { getByText, findByText } = render( <Provider> <Counter /> </Provider> )
await findByText('no error') expect(errorMessages).not.toContain('Error: error_in_write_function')
fireEvent.click(getByText('button')) expect(errorMessages).toContain('Error: error_in_write_function')})
itSkipIfVersionedWrite( 'can throw an error in async write function', async () => { const countAtom = atom(0) const errorAtom = atom( (get) => get(countAtom), async () => { throw new Error('error_in_async_write_function') } )
const Counter = () => { const [count, dispatch] = useAtom(errorAtom) const onClick = async () => { try { await dispatch() } catch (e) { console.error(e) } } return ( <> <div>count: {count}</div> <div>no error</div> <button onClick={onClick}>button</button> </> ) }
const { getByText, findByText } = render( <Provider> <Suspense fallback={null}> <Counter /> </Suspense> </Provider> )
await findByText('no error') expect(errorMessages).not.toContain('Error: error_in_async_write_function')
fireEvent.click(getByText('button')) await waitFor(() => { expect(errorMessages).toContain('Error: error_in_async_write_function') }) })
itSkipIfVersionedWrite( 'can throw a chained error in write function', async () => { const countAtom = atom(0) const errorAtom = atom( (get) => get(countAtom), () => { throw new Error('chained_err_in_write') } ) const chainedAtom = atom( (get) => get(errorAtom), (_get, set) => { set(errorAtom, null) } )
const Counter = () => { const [count, dispatch] = useAtom(chainedAtom) const onClick = () => { try { dispatch() } catch (e) { console.error(e) } } return ( <> <div>count: {count}</div> <div>no error</div> <button onClick={onClick}>button</button> </> ) }
const { getByText, findByText } = render( <Provider> <Counter /> </Provider> )
await findByText('no error') expect(errorMessages).not.toContain('Error: chained_err_in_write')
fireEvent.click(getByText('button')) expect(errorMessages).toContain('Error: chained_err_in_write') })
itSkipIfVersionedWrite('throws an error while updating in effect', async () => { const countAtom = atom(0)
const Counter = () => { const [, setCount] = useAtom(countAtom) useEffect(() => { try { setCount(() => { throw new Error('err_updating_in_effect') }) } catch (e) { console.error(e) } }, [setCount]) return ( <> <div>no error</div> </> ) }
const { findByText } = render( <Provider> <ErrorBoundary> <Counter /> </ErrorBoundary> </Provider> )
await findByText('no error') expect(errorMessages).toContain('Error: err_updating_in_effect')})
describe('throws an error while updating in effect cleanup', () => { const countAtom = atom(0)
let doubleSetCount = false
const Counter = () => { const [, setCount] = useAtom(countAtom) useEffect(() => { return () => { if (doubleSetCount) { setCount((x) => x + 1) } setCount(() => { throw new Error('err_in_effect_cleanup') }) } }, [setCount]) return ( <> <div>no error</div> </> ) }
const Main = () => { const [hide, setHide] = useState(false) return ( <> <button onClick={() => setHide(true)}>close</button> {!hide && <Counter />} </> ) }
itSkipIfVersionedWrite('[DEV-ONLY] single setCount', async () => { const { getByText, findByText } = render( <Provider> <ErrorBoundary> <Main /> </ErrorBoundary> </Provider> )
await findByText('no error') expect(errorMessages).not.toContain( 'Error: Uncaught [Error: err_in_effect_cleanup]' )
fireEvent.click(getByText('close')) expect(errorMessages).toContain( 'Error: Uncaught [Error: err_in_effect_cleanup]' ) })
itSkipIfVersionedWrite('[DEV-ONLY] dobule setCount', async () => { doubleSetCount = true
const { getByText, findByText } = render( <Provider> <ErrorBoundary> <Main /> </ErrorBoundary> </Provider> )
await findByText('no error') expect(errorMessages).not.toContain( 'Error: Uncaught [Error: err_in_effect_cleanup]' )
fireEvent.click(getByText('close')) expect(errorMessages).toContain( 'Error: Uncaught [Error: err_in_effect_cleanup]' ) })})
describe('error recovery', () => { const createCounter = () => { const counterAtom = atom(0)
const Counter = () => { const [count, setCount] = useAtom(counterAtom) return <button onClick={() => setCount(count + 1)}>increment</button> }
return { Counter, counterAtom } }
it('recovers from sync errors', async () => { const { counterAtom, Counter } = createCounter()
const syncAtom = atom((get) => { const value = get(counterAtom)
if (value === 0) { throw new Error('An error occurred') }
return value })
const Display = () => { return <div>Value: {useAtom(syncAtom)[0]}</div> }
const { getByText, findByText } = render( <Provider> <Counter /> <ErrorBoundary> <Display /> </ErrorBoundary> </Provider> )
await findByText('errored')
fireEvent.click(getByText('increment')) fireEvent.click(getByText('retry')) await findByText('Value: 1') })
it('recovers from async errors', async () => { const { counterAtom, Counter } = createCounter() const asyncAtom = atom(async (get) => { const value = get(counterAtom) await new Promise((r) => setTimeout(r, 100))
if (value === 0) { throw new Error('An error occurred') }
return value })
const Display = () => { return <div>Value: {useAtom(asyncAtom)[0]}</div> }
const { getByText, findByText } = render( <Provider> <Counter /> <ErrorBoundary> <Suspense fallback={null}> <Display /> </Suspense> </ErrorBoundary> </Provider> )
await findByText('errored')
fireEvent.click(getByText('increment')) fireEvent.click(getByText('retry')) await findByText('Value: 1') })})