|
| 1 | +import { afterEach, test } from 'vitest'; |
| 2 | +import { StrictMode, Suspense } from 'react'; |
| 3 | +import { cleanup, fireEvent, render } from '@testing-library/react'; |
| 4 | +import { expectType } from 'ts-expect'; |
| 5 | +import { useAtom } from 'jotai/react'; |
| 6 | +import { atom } from 'jotai/vanilla'; |
| 7 | +import type { SetStateAction, WritableAtom } from 'jotai/vanilla'; |
| 8 | +import * as O from 'optics-ts'; |
| 9 | +import { focusAtom } from 'jotai-optics'; |
| 10 | +import type { NotAnArrayType } from 'node_modules/optics-ts/utils.js'; |
| 11 | +import { useSetAtom } from 'jotai'; |
| 12 | + |
| 13 | +afterEach(cleanup); |
| 14 | + |
| 15 | +test('basic derivation using focus works', async () => { |
| 16 | + const bigAtom = atom([{ a: 0 }]); |
| 17 | + const focusFunction = (optic: O.OpticFor_<{ a: number }[]>) => |
| 18 | + optic.appendTo(); |
| 19 | + |
| 20 | + const Counter = () => { |
| 21 | + const appendNumber = useSetAtom(focusAtom(bigAtom, focusFunction)); |
| 22 | + const [bigAtomValue] = useAtom(bigAtom); |
| 23 | + return ( |
| 24 | + <> |
| 25 | + <div>bigAtom: {JSON.stringify(bigAtomValue)}</div> |
| 26 | + <button onClick={() => appendNumber({ a: bigAtomValue.length })}> |
| 27 | + Append to bigAtom |
| 28 | + </button> |
| 29 | + </> |
| 30 | + ); |
| 31 | + }; |
| 32 | + |
| 33 | + const { getByText, findByText } = render( |
| 34 | + <StrictMode> |
| 35 | + <Counter /> |
| 36 | + </StrictMode>, |
| 37 | + ); |
| 38 | + |
| 39 | + await findByText('bigAtom: [{"a":0}]'); |
| 40 | + |
| 41 | + fireEvent.click(getByText('Append to bigAtom')); |
| 42 | + await findByText('bigAtom: [{"a":0},{"a":1}]'); |
| 43 | + |
| 44 | + fireEvent.click(getByText('Append to bigAtom')); |
| 45 | + await findByText('bigAtom: [{"a":0},{"a":1},{"a":2}]'); |
| 46 | +}); |
| 47 | + |
| 48 | +test('double-focus on an atom works', async () => { |
| 49 | + const bigAtom = atom({ a: [0] }); |
| 50 | + const atomA = focusAtom(bigAtom, (optic) => optic.prop('a')); |
| 51 | + const atomAppend = focusAtom(atomA, (optic) => optic.appendTo()); |
| 52 | + |
| 53 | + const Counter = () => { |
| 54 | + const [bigAtomValue, setBigAtom] = useAtom(bigAtom); |
| 55 | + const [atomAValue, setAtomA] = useAtom(atomA); |
| 56 | + const append = useSetAtom(atomAppend); |
| 57 | + return ( |
| 58 | + <> |
| 59 | + <div>bigAtom: {JSON.stringify(bigAtomValue)}</div> |
| 60 | + <div>atomA: {JSON.stringify(atomAValue)}</div> |
| 61 | + <button onClick={() => setBigAtom((v) => ({ a: [...v.a, 1] }))}> |
| 62 | + inc bigAtom |
| 63 | + </button> |
| 64 | + <button onClick={() => setAtomA((v) => [...v, 2])}>inc atomA</button> |
| 65 | + <button onClick={() => append(3)}>append</button> |
| 66 | + </> |
| 67 | + ); |
| 68 | + }; |
| 69 | + |
| 70 | + const { getByText, findByText } = render( |
| 71 | + <StrictMode> |
| 72 | + <Counter /> |
| 73 | + </StrictMode>, |
| 74 | + ); |
| 75 | + |
| 76 | + await findByText('bigAtom: {"a":[0]}'); |
| 77 | + await findByText('atomA: [0]'); |
| 78 | + |
| 79 | + fireEvent.click(getByText('inc bigAtom')); |
| 80 | + await findByText('bigAtom: {"a":[0,1]}'); |
| 81 | + await findByText('atomA: [0,1]'); |
| 82 | + |
| 83 | + fireEvent.click(getByText('inc atomA')); |
| 84 | + await findByText('bigAtom: {"a":[0,1,2]}'); |
| 85 | + await findByText('atomA: [0,1,2]'); |
| 86 | + |
| 87 | + fireEvent.click(getByText('append')); |
| 88 | + await findByText('bigAtom: {"a":[0,1,2,3]}'); |
| 89 | + await findByText('atomA: [0,1,2,3]'); |
| 90 | +}); |
| 91 | + |
| 92 | +test('focus on async atom works', async () => { |
| 93 | + const baseAtom = atom([0]); |
| 94 | + const asyncAtom = atom( |
| 95 | + (get) => Promise.resolve(get(baseAtom)), |
| 96 | + async (get, set, param: SetStateAction<Promise<number[]>>) => { |
| 97 | + const prev = Promise.resolve(get(baseAtom)); |
| 98 | + const next = await (typeof param === 'function' ? param(prev) : param); |
| 99 | + set(baseAtom, next); |
| 100 | + }, |
| 101 | + ); |
| 102 | + const focusFunction = (optic: O.OpticFor_<number[]>) => optic.appendTo(); |
| 103 | + |
| 104 | + const Counter = () => { |
| 105 | + const append = useSetAtom(focusAtom(asyncAtom, focusFunction)); |
| 106 | + const [asyncValue, setAsync] = useAtom(asyncAtom); |
| 107 | + const [baseValue, setBase] = useAtom(baseAtom); |
| 108 | + return ( |
| 109 | + <> |
| 110 | + <div>baseAtom: {JSON.stringify(baseValue)}</div> |
| 111 | + <div>asyncAtom: {JSON.stringify(asyncValue)}</div> |
| 112 | + <button onClick={() => append(baseValue.length)}>append</button> |
| 113 | + <button |
| 114 | + onClick={() => setAsync((p) => p.then((v) => [...v, v.length]))} |
| 115 | + > |
| 116 | + incr async |
| 117 | + </button> |
| 118 | + <button onClick={() => setBase((v) => [...v, v.length])}> |
| 119 | + incr base |
| 120 | + </button> |
| 121 | + </> |
| 122 | + ); |
| 123 | + }; |
| 124 | + |
| 125 | + const { getByText, findByText } = render( |
| 126 | + <StrictMode> |
| 127 | + <Suspense fallback={<div>Loading...</div>}> |
| 128 | + <Counter /> |
| 129 | + </Suspense> |
| 130 | + </StrictMode>, |
| 131 | + ); |
| 132 | + |
| 133 | + await findByText('baseAtom: [0]'); |
| 134 | + await findByText('asyncAtom: [0]'); |
| 135 | + |
| 136 | + fireEvent.click(getByText('append')); |
| 137 | + await findByText('baseAtom: [0,1]'); |
| 138 | + await findByText('asyncAtom: [0,1]'); |
| 139 | + |
| 140 | + fireEvent.click(getByText('incr async')); |
| 141 | + await findByText('baseAtom: [0,1,2]'); |
| 142 | + await findByText('asyncAtom: [0,1,2]'); |
| 143 | + |
| 144 | + fireEvent.click(getByText('incr base')); |
| 145 | + await findByText('baseAtom: [0,1,2,3]'); |
| 146 | + await findByText('asyncAtom: [0,1,2,3]'); |
| 147 | +}); |
| 148 | + |
| 149 | +type BillingData = { |
| 150 | + id: string; |
| 151 | +}; |
| 152 | + |
| 153 | +type CustomerData = { |
| 154 | + id: string; |
| 155 | + billing: BillingData[]; |
| 156 | + someOtherData: string; |
| 157 | +}; |
| 158 | + |
| 159 | +test('typescript should accept "undefined" as valid value for lens', async () => { |
| 160 | + const customerListAtom = atom<CustomerData[]>([]); |
| 161 | + |
| 162 | + const foundCustomerAtom = focusAtom(customerListAtom, (optic) => |
| 163 | + optic.find((el) => el.id === 'some-invalid-id'), |
| 164 | + ); |
| 165 | + |
| 166 | + const derivedLens = focusAtom(foundCustomerAtom, (optic) => optic.appendTo()); |
| 167 | + |
| 168 | + expectType< |
| 169 | + WritableAtom<void, [NotAnArrayType<CustomerData | undefined>], void> |
| 170 | + >(derivedLens); |
| 171 | +}); |
| 172 | + |
| 173 | +test('should work with promise based atoms with "undefined" value', async () => { |
| 174 | + const customerBaseAtom = atom<CustomerData | undefined>(undefined); |
| 175 | + |
| 176 | + const asyncCustomerDataAtom = atom( |
| 177 | + async (get) => get(customerBaseAtom), |
| 178 | + async (_, set, nextValue: Promise<CustomerData>) => { |
| 179 | + set(customerBaseAtom, await nextValue); |
| 180 | + }, |
| 181 | + ); |
| 182 | + |
| 183 | + const focusedPromiseAtom = focusAtom(asyncCustomerDataAtom, (optic) => |
| 184 | + optic.appendTo(), |
| 185 | + ); |
| 186 | + |
| 187 | + expectType< |
| 188 | + WritableAtom< |
| 189 | + Promise<void>, |
| 190 | + [NotAnArrayType<CustomerData | undefined>], |
| 191 | + Promise<void> |
| 192 | + > |
| 193 | + >(focusedPromiseAtom); |
| 194 | +}); |
0 commit comments