deno.land / x / jotai@v1.8.4 / tests / urql / atomWithSubscription.test.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411import { Component, StrictMode, Suspense, useContext } from 'react'import type { ReactNode } from 'react'import { fireEvent, render } from '@testing-library/react'import type { Client, TypedDocumentNode } from '@urql/core'import { delay, fromValue, interval, map, pipe, switchMap } from 'wonka'import { atom, SECRET_INTERNAL_getScopeContext as getScopeContext, useAtom, useSetAtom,} from 'jotai'import { atomWithSubscription } from 'jotai/urql'import { StrictModeUnlessVersionedWrite, getTestProvider } from '../testUtils'
// This is only used to pass tests with unstable_enableVersionedWriteconst useRetryFromError = (scope?: symbol | string | number) => { const ScopeContext = getScopeContext(scope) const { r: retryFromError } = useContext(ScopeContext) return retryFromError || ((fn) => fn())}
const generateClient = (id = 'default', error?: () => boolean) => ({ subscription: () => pipe( interval(100), switchMap((i: number) => pipe(fromValue(i), delay(i > 2 ? 500 : 0))), map((i: number) => error?.() ? { error: new Error('fetch error') } : { data: { id, count: i } } ) ), } as unknown as Client)
const clientMock = generateClient()
const Provider = getTestProvider()
it('subscription basic test', async () => { const countAtom = atomWithSubscription( () => ({ query: 'subscription Test { count }' as unknown as TypedDocumentNode<{ count: number }>, variables: {}, }), () => clientMock )
const Counter = () => { const [{ data }] = useAtom(countAtom) return ( <> <div>count: {data.count}</div> </> ) }
const { findByText } = render( <> <Provider> <Suspense fallback="loading"> <Counter /> </Suspense> </Provider> </> )
await findByText('loading') await findByText('count: 0') await findByText('count: 1') await findByText('count: 2')})
it('subscription change client at runtime', async () => { const clientAtom = atom(generateClient('first')) const countAtom = atomWithSubscription( () => ({ query: 'subscription Test { id, count }' as unknown as TypedDocumentNode<{ id: string count: number }>, variables: {}, }), (get) => get(clientAtom) )
const Counter = () => { const [{ data }] = useAtom(countAtom) return ( <> <div> {data.id} count: {data.count} </div> </> ) }
const Controls = () => { const [, setClient] = useAtom(clientAtom) return ( <> <button onClick={() => setClient(generateClient('first'))}> first </button> <button onClick={() => setClient(generateClient('second'))}> second </button> </> ) }
const { findByText, getByText } = render( <> <Provider> <Suspense fallback="loading"> <Counter /> </Suspense> <Controls /> </Provider> </> )
await findByText('loading') await findByText('first count: 0') await findByText('first count: 1') await findByText('first count: 2')
await new Promise((r) => setTimeout(r, 100)) fireEvent.click(getByText('second')) await findByText('loading') await findByText('second count: 0') await findByText('second count: 1') await findByText('second count: 2')
await new Promise((r) => setTimeout(r, 100)) fireEvent.click(getByText('first')) await findByText('loading') await findByText('first count: 0') await findByText('first count: 1') await findByText('first count: 2')})
it('pause test', async () => { const enabledAtom = atom(false) const countAtom = atomWithSubscription( (get) => ({ query: 'subscription Test { count }' as unknown as TypedDocumentNode<{ count: number }>, variables: {}, pause: !get(enabledAtom), }), () => clientMock )
const Counter = () => { const [result] = useAtom(countAtom) return ( <> <div>count: {result ? result.data.count : 'paused'}</div> </> ) }
const Controls = () => { const [, setEnabled] = useAtom(enabledAtom) return <button onClick={() => setEnabled((x) => !x)}>toggle</button> }
const { getByText, findByText } = render( <StrictMode> <Provider> <Suspense fallback="loading"> <Counter /> </Suspense> <Controls /> </Provider> </StrictMode> )
await findByText('count: paused')
await new Promise((r) => setTimeout(r, 100)) fireEvent.click(getByText('toggle')) await findByText('loading') await findByText('count: 0') await findByText('count: 1') await findByText('count: 2')})
it('null client suspense', async () => { const clientAtom = atom<Client | null>(null) const countAtom = atomWithSubscription( () => ({ query: 'subscription Test { id, count }' as unknown as TypedDocumentNode<{ id: string count: number }>, variables: {}, }), (get) => get(clientAtom) as Client ) // Derived Atom to safe guard when client is null const guardedCountAtom = atom( (get): { data?: { id: string; count: number } } => { const client = get(clientAtom) if (client === null) return {} return get(countAtom) } )
const Counter = () => { const [{ data }] = useAtom(guardedCountAtom) return ( <> <div> {data ? ( <> {data?.id} count: {data?.count} </> ) : ( 'no data' )} </div> </> ) }
const Controls = () => { const [, setClient] = useAtom(clientAtom) return ( <> <button onClick={() => setClient(generateClient())}>set</button> <button onClick={() => setClient(null)}>unset</button> </> ) }
const { findByText, getByText } = render( <StrictModeUnlessVersionedWrite> <Provider> <Suspense fallback="loading"> <Counter /> </Suspense> <Controls /> </Provider> </StrictModeUnlessVersionedWrite> )
await findByText('no data')
await new Promise((r) => setTimeout(r, 100)) fireEvent.click(getByText('set')) await findByText('loading') await findByText('default count: 0') await findByText('default count: 1') await findByText('default count: 2')
await new Promise((r) => setTimeout(r, 100)) fireEvent.click(getByText('unset')) await findByText('no data')
await new Promise((r) => setTimeout(r, 100)) fireEvent.click(getByText('set')) await findByText('default count: 0') await findByText('default count: 1') await findByText('default count: 2')})
describe('error handling', () => { class ErrorBoundary extends Component< { message?: string; retry?: () => void; 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'} {this.props.retry && ( <button onClick={() => { this.props.retry?.() this.setState({ hasError: false }) }}> retry </button> )} </div> ) : ( this.props.children ) } }
it('can catch error in error boundary', async () => { const countAtom = atomWithSubscription( () => ({ query: 'subscription Test { count }' as unknown as TypedDocumentNode<{ count: number }>, variables: {}, }), () => generateClient(undefined, () => true) )
const Counter = () => { const [{ data }] = useAtom(countAtom) return <div>count: {data.count}</div> }
const { findByText } = render( <Provider> <ErrorBoundary> <Suspense fallback="loading"> <Counter /> </Suspense> </ErrorBoundary> </Provider> )
await findByText('loading') await findByText('errored') })
it('can recover from error', async () => { let willThrowError = true const countAtom = atomWithSubscription( () => ({ query: 'subscription Test { count }' as unknown as TypedDocumentNode<{ count: number }>, variables: {}, }), () => generateClient(undefined, () => willThrowError) )
const Counter = () => { const [ { data: { count }, }, dispatch, ] = useAtom(countAtom) const refetch = () => dispatch({ type: 'refetch' }) return ( <> <div>count: {count}</div> <button onClick={refetch}>refetch</button> </> ) }
const App = () => { const dispatch = useSetAtom(countAtom) const retryFromError = useRetryFromError() const retry = () => { retryFromError(() => { dispatch({ type: 'refetch' }) }) } return ( <ErrorBoundary retry={retry}> <Suspense fallback="loading"> <Counter /> </Suspense> </ErrorBoundary> ) }
const { findByText, getByText } = render( <Provider> <App /> </Provider> )
await findByText('loading') await findByText('errored')
await new Promise((r) => setTimeout(r, 100)) willThrowError = false fireEvent.click(getByText('retry')) await findByText('loading') await findByText('count: 0') await findByText('count: 1') await findByText('count: 2')
await new Promise((r) => setTimeout(r, 100)) willThrowError = true fireEvent.click(getByText('refetch')) await findByText('loading') await findByText('errored')
await new Promise((r) => setTimeout(r, 100)) willThrowError = false fireEvent.click(getByText('retry')) await findByText('loading') await findByText('count: 0') await findByText('count: 1') await findByText('count: 2') })})
Version Info