import { act, render, RenderResult } from '@testing-library/react' import { atom } from '@tldraw/state' import { sleep } from '@tldraw/utils' import * as React from 'react' import { useStateTracking } from './useStateTracking' describe('useStateTracking', () => { it('causes a rerender when a dependency changes', async () => { const a = atom('', 0) const Component = () => { const val = useStateTracking('', () => { return a.get() }) return <>You are {val} years old } let view: RenderResult await act(() => { view = render() }) expect(view!.asFragment().textContent).toMatchInlineSnapshot('"You are 0 years old"') act(() => { a.set(1) }) expect(view!.asFragment().textContent).toMatchInlineSnapshot('"You are 1 years old"') }) it('allows using hooks inside the callback', async () => { const _age = atom('', 0) let setHeight: (height: number) => void const Component = () => { let height const age = useStateTracking('', () => { // eslint-disable-next-line react-hooks/rules-of-hooks ;[height, setHeight] = React.useState(20) return _age.get() }) return ( <> You are {age} years old and {height} meters tall ) } let view: RenderResult await act(() => { view = render() }) expect(view!.asFragment().textContent).toMatchInlineSnapshot( '"You are 0 years old and 20 meters tall"' ) act(() => { _age.set(1) }) expect(view!.asFragment().textContent).toMatchInlineSnapshot( '"You are 1 years old and 20 meters tall"' ) act(() => { setHeight(21) }) expect(view!.asFragment().textContent).toMatchInlineSnapshot( '"You are 1 years old and 21 meters tall"' ) }) it('allows throwing promises to trigger suspense boundaries', async () => { const a = atom('age', null) let resolve = (_val: string) => { // noop } const Component = () => { const val = useStateTracking('', () => { if (a.get() === null) { throw new Promise((r) => { resolve = r }) } return a.get() }) return <>You are {val} years old } let view: RenderResult = null as any await act(() => { view = render( fallback}> ) }) expect(view.asFragment().textContent).toMatchInlineSnapshot(`"fallback"`) await act(() => { a.set(1) }) // merely setting the value won't trigger a rerender, the promise must resolve expect(view.asFragment().textContent).toMatchInlineSnapshot(`"fallback"`) await act(() => { resolve('resolved') }) await sleep(0) expect(view.asFragment().textContent).toMatchInlineSnapshot('"You are 1 years old"') }) it('stops reacting when the component unmounts', async () => { const a = atom('', 0) let numRenders = 0 const Component = () => { const val = useStateTracking('', () => { numRenders++ return a.get() }) return <>You are {val} years old } let view: RenderResult await act(() => { view = render(React.createElement(Component)) }) expect(view!.asFragment().textContent).toMatchInlineSnapshot('"You are 0 years old"') expect(numRenders).toBe(1) await act(() => { a.set(1) }) expect(view!.asFragment().textContent).toMatchInlineSnapshot('"You are 1 years old"') expect(numRenders).toBe(2) await act(() => { view!.unmount() }) await act(() => { a.set(2) }) expect(numRenders).toBe(2) }) })