Version 1.0.2 mit node_modules Verzeichnis

This commit is contained in:
ISA
2024-10-02 07:58:24 +02:00
parent f353a06b1b
commit 62b6e55a0a
68228 changed files with 4548477 additions and 651 deletions

View File

@@ -0,0 +1,151 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
// Sanity tests for *_TRANSITION_SUPPORT_UNSTABLE() hooks. The actual tests
// for useTransition() support are in Recoil_useTransition-test.js
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
act,
selector,
stringAtom,
asyncSelector,
flushPromisesAndTimers,
renderElements,
useRecoilState,
useRecoilState_TRANSITION_SUPPORT_UNSTABLE,
useRecoilValue,
useRecoilValue_TRANSITION_SUPPORT_UNSTABLE,
useRecoilValueLoadable,
useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE,
useSetRecoilState,
reactMode;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({act} = require('ReactTestUtils'));
selector = require('../../recoil_values/Recoil_selector');
({
stringAtom,
asyncSelector,
flushPromisesAndTimers,
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({reactMode} = require('../../core/Recoil_ReactMode'));
({
useRecoilState,
useRecoilState_TRANSITION_SUPPORT_UNSTABLE,
useRecoilValue,
useRecoilValue_TRANSITION_SUPPORT_UNSTABLE,
useRecoilValueLoadable,
useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE,
useSetRecoilState,
} = require('../Recoil_Hooks'));
});
testRecoil('useRecoilValue_TRANSITION_SUPPORT_UNSTABLE', async () => {
if (!reactMode().early) {
return;
}
const myAtom = stringAtom();
const [mySelector, resolve] = asyncSelector<string, _>();
let setAtom;
function Component() {
setAtom = useSetRecoilState(myAtom);
return [
useRecoilValue(myAtom),
useRecoilValue_TRANSITION_SUPPORT_UNSTABLE(myAtom),
useRecoilValue(mySelector),
useRecoilValue_TRANSITION_SUPPORT_UNSTABLE(mySelector),
].join(' ');
}
const c = renderElements(<Component />);
expect(c.textContent).toBe('loading');
act(() => resolve('RESOLVE'));
await flushPromisesAndTimers();
expect(c.textContent).toBe('DEFAULT DEFAULT RESOLVE RESOLVE');
act(() => setAtom('SET'));
expect(c.textContent).toBe('SET SET RESOLVE RESOLVE');
});
testRecoil('useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE', async () => {
if (!reactMode().early) {
return;
}
const myAtom = stringAtom();
const [mySelector, resolve] = asyncSelector<string, _>();
let setAtom;
function Component() {
setAtom = useSetRecoilState(myAtom);
return [
useRecoilValueLoadable(myAtom).getValue(),
useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE(myAtom).getValue(),
useRecoilValueLoadable(mySelector).getValue(),
useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE(mySelector).getValue(),
].join(' ');
}
const c = renderElements(<Component />);
expect(c.textContent).toBe('loading');
act(() => resolve('RESOLVE'));
await flushPromisesAndTimers();
expect(c.textContent).toBe('DEFAULT DEFAULT RESOLVE RESOLVE');
act(() => setAtom('SET'));
expect(c.textContent).toBe('SET SET RESOLVE RESOLVE');
});
testRecoil('useRecoilState_TRANSITION_SUPPORT_UNSTABLE', async () => {
if (!reactMode().early) {
return;
}
const myAtom = stringAtom();
const [myAsyncSelector, resolve] = asyncSelector<string, _>();
// $FlowFixMe[incompatible-call]
const mySelector = selector({
key: 'useRecoilState_TRANSITION_SUPPORT_UNSTABLE selector',
get: () => myAsyncSelector,
// $FlowFixMe[incompatible-call]
set: ({set}, newValue) => set(myAtom, newValue),
});
let setAtom, setSelector;
function Component() {
const [v1] = useRecoilState(myAtom);
const [v2, setAtomValue] =
useRecoilState_TRANSITION_SUPPORT_UNSTABLE(myAtom);
setAtom = setAtomValue;
const [v3] = useRecoilState(mySelector);
const [v4, setSelectorValue] =
useRecoilState_TRANSITION_SUPPORT_UNSTABLE(mySelector);
setSelector = setSelectorValue;
return [v1, v2, v3, v4].join(' ');
}
const c = renderElements(<Component />);
expect(c.textContent).toBe('loading');
act(() => resolve('RESOLVE'));
await flushPromisesAndTimers();
expect(c.textContent).toBe('DEFAULT DEFAULT RESOLVE RESOLVE');
act(() => setAtom('SET'));
expect(c.textContent).toBe('SET SET RESOLVE RESOLVE');
act(() => setSelector('SETS'));
expect(c.textContent).toBe('SETS SETS RESOLVE RESOLVE');
});

View File

@@ -0,0 +1,935 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
/* eslint-disable fb-www/react-no-useless-fragment */
'use strict';
import type {
RecoilState,
RecoilValue,
RecoilValueReadOnly,
} from '../../core/Recoil_RecoilValue';
import type {PersistenceSettings} from '../../recoil_values/Recoil_atom';
import type {Node} from 'react';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
useEffect,
useState,
Profiler,
act,
Queue,
batchUpdates,
atom,
selector,
selectorFamily,
ReadsAtom,
renderElements,
renderUnwrappedElements,
recoilComponentGetRecoilValueCount_FOR_TESTING,
useRecoilState,
useRecoilStateLoadable,
useRecoilValue,
useSetRecoilState,
reactMode,
invariant;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useEffect, useState, Profiler} = require('react'));
({act} = require('ReactTestUtils'));
Queue = require('../../adt/Recoil_Queue');
({batchUpdates} = require('../../core/Recoil_Batching'));
atom = require('../../recoil_values/Recoil_atom');
selector = require('../../recoil_values/Recoil_selector');
selectorFamily = require('../../recoil_values/Recoil_selectorFamily');
({
ReadsAtom,
renderElements,
renderUnwrappedElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({reactMode} = require('../../core/Recoil_ReactMode'));
({
recoilComponentGetRecoilValueCount_FOR_TESTING,
useRecoilState,
useRecoilStateLoadable,
useRecoilValue,
useSetRecoilState,
} = require('../Recoil_Hooks'));
invariant = require('recoil-shared/util/Recoil_invariant');
});
let nextID = 0;
function counterAtom(persistence?: PersistenceSettings<number>) {
return atom({
key: `atom${nextID++}`,
default: 0,
persistence_UNSTABLE: persistence,
});
}
function plusOneSelector(dep: RecoilValue<number>) {
const fn = jest.fn(x => x + 1);
const sel = selector({
key: `selector${nextID++}`,
get: ({get}) => fn(get(dep)),
});
return [sel, fn];
}
function plusOneAsyncSelector(
dep: RecoilValue<number>,
): [RecoilValueReadOnly<number>, (number) => void] {
let nextTimeoutAmount = 100;
const fn = jest.fn(x => {
return new Promise(resolve => {
setTimeout(() => {
resolve(x + 1);
}, nextTimeoutAmount);
});
});
const sel = selector({
key: `selector${nextID++}`,
get: ({get}) => fn(get(dep)),
});
return [
// $FlowFixMe[incompatible-return]
sel,
x => {
nextTimeoutAmount = x;
},
];
}
function additionSelector(
depA: RecoilValue<number>,
depB: RecoilValue<number>,
) {
const fn = jest.fn((a, b) => a + b);
const sel = selector({
key: `selector${nextID++}`,
get: ({get}) => fn(get(depA), get(depB)),
});
return [sel, fn];
}
function componentThatReadsAndWritesAtom<T>(
recoilState: RecoilState<T>,
): [React.AbstractComponent<{...}>, ((T => T) | T) => void] {
let updateValue;
const Component = jest.fn(() => {
const [value, _updateValue] = useRecoilState(recoilState);
updateValue = _updateValue;
return value;
});
// flowlint-next-line unclear-type:off
return [(Component: any), (...args) => updateValue(...args)];
}
function componentThatWritesAtom<T>(
recoilState: RecoilState<T>,
// flowlint-next-line unclear-type:off
): [any, ((T => T) | T) => void] {
let updateValue;
const Component = jest.fn(() => {
updateValue = useSetRecoilState(recoilState);
return null;
});
// flowlint-next-line unclear-type:off
return [(Component: any), x => updateValue(x)];
}
function componentThatReadsTwoAtoms(
one: RecoilState<number>,
two: RecoilState<number> | RecoilValueReadOnly<number>,
) {
return (jest.fn(function ReadTwoAtoms() {
return `${useRecoilValue(one)},${useRecoilValue(two)}`;
}): any); // flowlint-line unclear-type:off
}
function componentThatReadsAtomWithCommitCount(
recoilState: RecoilState<number> | RecoilValueReadOnly<number>,
) {
const commit = jest.fn(() => {});
function ReadAtom() {
return (
// $FlowFixMe[invalid-tuple-arity]
<Profiler id="test" onRender={commit}>
{useRecoilValue(recoilState)}
</Profiler>
);
}
return [ReadAtom, commit];
}
function componentThatToggles(a: Node, b: null) {
const toggle = {current: () => invariant(false, 'bug in test code')};
const Toggle = () => {
const [value, setValue] = useState(false);
// $FlowFixMe[incompatible-type]
toggle.current = () => setValue(v => !v);
return value ? b : a;
};
return [Toggle, toggle];
}
function baseRenderCount(gks: Array<string>): number {
return reactMode().mode === 'LEGACY' &&
!gks.includes('recoil_suppress_rerender_in_callback')
? 1
: 0;
}
testRecoil('Component throws error when passing invalid node', async () => {
function Component() {
try {
// $FlowExpectedError[incompatible-call]
useRecoilValue('foo');
} catch (error) {
expect(error.message).toEqual(expect.stringContaining('useRecoilValue'));
return 'CAUGHT';
}
return 'INVALID';
}
const container = renderElements(<Component />);
expect(container.textContent).toEqual('CAUGHT');
});
testRecoil('Components are re-rendered when atoms change', async () => {
const anAtom = counterAtom();
const [Component, updateValue] = componentThatReadsAndWritesAtom(anAtom);
const container = renderElements(<Component />);
expect(container.textContent).toEqual('0');
act(() => updateValue(1));
expect(container.textContent).toEqual('1');
});
describe('Render counts', () => {
testRecoil(
'Component subscribed to atom is rendered just once',
({gks, strictMode}) => {
const BASE_CALLS = baseRenderCount(gks);
const sm = strictMode ? 2 : 1;
const anAtom = counterAtom();
const [Component, updateValue] = componentThatReadsAndWritesAtom(anAtom);
renderElements(
<>
<Component />
</>,
);
expect(Component).toHaveBeenCalledTimes((BASE_CALLS + 1) * sm);
act(() => updateValue(1));
expect(Component).toHaveBeenCalledTimes((BASE_CALLS + 2) * sm);
},
);
testRecoil('Write-only components are not subscribed', ({strictMode}) => {
const anAtom = counterAtom();
const [Component, updateValue] = componentThatWritesAtom(anAtom);
renderElements(
<>
<Component />
</>,
);
expect(Component).toHaveBeenCalledTimes(strictMode ? 2 : 1);
act(() => updateValue(1));
expect(Component).toHaveBeenCalledTimes(strictMode ? 2 : 1);
});
testRecoil(
'Component that depends on atom in multiple ways is rendered just once',
({gks, strictMode}) => {
const BASE_CALLS = baseRenderCount(gks);
const sm = strictMode ? 2 : 1;
const anAtom = counterAtom();
const [aSelector, _] = plusOneSelector(anAtom);
const [WriteComp, updateValue] = componentThatWritesAtom(anAtom);
// $FlowFixMe[incompatible-call]
const ReadComp = componentThatReadsTwoAtoms(anAtom, aSelector);
renderElements(
<>
<WriteComp />
<ReadComp />
</>,
);
expect(ReadComp).toHaveBeenCalledTimes((BASE_CALLS + 1) * sm);
act(() => updateValue(1));
expect(ReadComp).toHaveBeenCalledTimes((BASE_CALLS + 2) * sm);
},
);
testRecoil(
'Component that depends on multiple atoms via selector is rendered just once',
({gks}) => {
const BASE_CALLS = baseRenderCount(gks);
const atomA = counterAtom();
const atomB = counterAtom();
const [aSelector, _] = additionSelector(atomA, atomB);
const [ComponentA, updateValueA] = componentThatWritesAtom(atomA);
const [ComponentB, updateValueB] = componentThatWritesAtom(atomB);
const [ReadComp, commit] =
// $FlowFixMe[incompatible-call]
componentThatReadsAtomWithCommitCount(aSelector);
renderElements(
<>
<ComponentA />
<ComponentB />
<ReadComp />
</>,
);
expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 1);
act(() => {
batchUpdates(() => {
updateValueA(1);
updateValueB(1);
});
});
expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 2);
},
);
testRecoil(
'Component that depends on multiple atoms directly is rendered just once',
({gks, strictMode}) => {
const BASE_CALLS = baseRenderCount(gks);
const sm = strictMode ? 2 : 1;
const atomA = counterAtom();
const atomB = counterAtom();
const [ComponentA, updateValueA] = componentThatWritesAtom(atomA);
const [ComponentB, updateValueB] = componentThatWritesAtom(atomB);
const ReadComp = componentThatReadsTwoAtoms(atomA, atomB);
renderElements(
<>
<ComponentA />
<ComponentB />
<ReadComp />
</>,
);
expect(ReadComp).toHaveBeenCalledTimes((BASE_CALLS + 1) * sm);
act(() => {
batchUpdates(() => {
updateValueA(1);
updateValueB(1);
});
});
expect(ReadComp).toHaveBeenCalledTimes((BASE_CALLS + 2) * sm);
},
);
testRecoil(
'Component is rendered just once when atom is changed twice',
({gks}) => {
const BASE_CALLS = baseRenderCount(gks);
const atomA = counterAtom();
const [ComponentA, updateValueA] = componentThatWritesAtom(atomA);
const [ReadComp, commit] = componentThatReadsAtomWithCommitCount(atomA);
renderElements(
<>
<ComponentA />
<ReadComp />
</>,
);
expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 1);
act(() => {
batchUpdates(() => {
updateValueA(1);
updateValueA(2);
});
});
expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 2);
},
);
testRecoil(
'Component does not re-read atom when rendered due to another atom changing, parent re-render, or other state change',
() => {
// useSyncExternalStore() will always call getSnapshot() to see if it has
// mutated between render and commit.
if (
reactMode().mode === 'LEGACY' ||
reactMode().mode === 'SYNC_EXTERNAL_STORE'
) {
return;
}
const atomA = counterAtom();
const atomB = counterAtom();
let _, setLocal;
let _a, setA;
let _b, _setB;
function Component() {
[_, setLocal] = useState(0);
[_a, setA] = useRecoilState(atomA);
[_b, _setB] = useRecoilState(atomB);
return null;
}
let __, setParentLocal;
function Parent() {
[__, setParentLocal] = useState(0);
return <Component />;
}
renderElements(<Parent />);
const initialCalls =
recoilComponentGetRecoilValueCount_FOR_TESTING.current;
expect(initialCalls).toBeGreaterThan(0);
// No re-read when setting local state on the component:
act(() => {
setLocal(1);
});
expect(recoilComponentGetRecoilValueCount_FOR_TESTING.current).toBe(
initialCalls,
);
// No re-read when setting local state on its parent causing it to re-render:
act(() => {
setParentLocal(1);
});
expect(recoilComponentGetRecoilValueCount_FOR_TESTING.current).toBe(
initialCalls,
);
// Setting an atom causes a re-read for that atom only, not others:
act(() => {
setA(1);
});
expect(recoilComponentGetRecoilValueCount_FOR_TESTING.current).toBe(
initialCalls + 1,
);
},
);
testRecoil(
'Components re-render only one time if selectorFamily changed',
({gks, strictMode}) => {
const BASE_CALLS = baseRenderCount(gks);
const sm = strictMode ? 2 : 1;
const atomA = counterAtom();
const selectAFakeId = selectorFamily({
key: 'selectItem',
get:
(_id: number) =>
// $FlowFixMe[missing-local-annot]
({get}) =>
get(atomA),
});
const Component = (jest.fn(function ReadFromSelector({id}) {
return useRecoilValue(selectAFakeId(id));
}): ({id: number}) => React.Node);
let increment;
const App = () => {
const [state, setState] = useRecoilState(atomA);
increment = () => setState(s => s + 1);
return <Component id={state} />;
};
const container = renderElements(<App />);
let baseCalls = BASE_CALLS;
expect(container.textContent).toEqual('0');
expect(Component).toHaveBeenCalledTimes((baseCalls + 1) * sm);
act(() => increment());
if (
(reactMode().mode === 'LEGACY' &&
!gks.includes('recoil_suppress_rerender_in_callback')) ||
reactMode().mode === 'TRANSITION_SUPPORT'
) {
baseCalls += 1;
}
expect(container.textContent).toEqual('1');
expect(Component).toHaveBeenCalledTimes((baseCalls + 2) * sm);
},
);
});
describe('Component Subscriptions', () => {
testRecoil(
'Can subscribe to and also change an atom in the same batch',
() => {
const anAtom = counterAtom();
let setVisible;
function Switch({children}: $TEMPORARY$object<{children: Node}>) {
const [visible, mySetVisible] = useState(false);
setVisible = mySetVisible;
return visible ? children : null;
}
const [Component, updateValue] = componentThatWritesAtom(anAtom);
const container = renderElements(
<>
<Component />
<Switch>
<ReadsAtom atom={anAtom} />
</Switch>
</>,
);
expect(container.textContent).toEqual('');
act(() => {
batchUpdates(() => {
setVisible(true);
updateValue(1337);
});
});
expect(container.textContent).toEqual('1337');
},
);
testRecoil('Atom values are retained when atom has no subscribers', () => {
const anAtom = counterAtom();
let setVisible;
function Switch({children}: $TEMPORARY$object<{children: Node}>) {
const [visible, mySetVisible] = useState(true);
setVisible = mySetVisible;
return visible ? children : null;
}
const [Component, updateValue] = componentThatWritesAtom(anAtom);
const container = renderElements(
<>
<Component />
<Switch>
<ReadsAtom atom={anAtom} />
</Switch>
</>,
);
act(() => updateValue(1337));
expect(container.textContent).toEqual('1337');
act(() => setVisible(false));
expect(container.textContent).toEqual('');
act(() => setVisible(true));
expect(container.textContent).toEqual('1337');
});
testRecoil(
'Components unsubscribe from atoms when rendered without using them',
({gks, strictMode}) => {
const BASE_CALLS = baseRenderCount(gks);
const sm = strictMode ? 2 : 1;
const atomA = counterAtom();
const atomB = counterAtom();
const [WriteA, updateValueA] = componentThatWritesAtom(atomA);
const [WriteB, updateValueB] = componentThatWritesAtom(atomB);
const Component = (jest.fn(function Read({state}) {
const [value] = useRecoilState<mixed>(state);
return value;
}): any); // flowlint-line unclear-type:off
let toggleSwitch;
const Switch = () => {
const [value, setValue] = useState(false);
toggleSwitch = () => setValue(true);
return value ? (
<Component state={atomB} />
) : (
<Component state={atomA} />
);
};
const container = renderElements(
<>
<Switch />
<WriteA />
<WriteB />
</>,
);
let baseCalls = BASE_CALLS;
expect(container.textContent).toEqual('0');
expect(Component).toHaveBeenCalledTimes((baseCalls + 1) * sm);
act(() => updateValueA(1));
expect(container.textContent).toEqual('1');
expect(Component).toHaveBeenCalledTimes((baseCalls + 2) * sm);
if (
(reactMode().mode === 'LEGACY' &&
!gks.includes('recoil_suppress_rerender_in_callback')) ||
reactMode().mode === 'TRANSITION_SUPPORT'
) {
baseCalls += 1;
}
act(() => toggleSwitch());
expect(container.textContent).toEqual('0');
expect(Component).toHaveBeenCalledTimes((baseCalls + 3) * sm);
// Now update the atom that it used to be subscribed to but should be no longer:
act(() => updateValueA(2));
expect(container.textContent).toEqual('0');
expect(Component).toHaveBeenCalledTimes((baseCalls + 3) * sm); // Important part: same as before
// It is subscribed to the atom that it switched to:
act(() => updateValueB(3));
expect(container.textContent).toEqual('3');
expect(Component).toHaveBeenCalledTimes((baseCalls + 4) * sm);
},
);
testRecoil(
'Selectors unsubscribe from upstream when they have no subscribers',
() => {
const atomA = counterAtom();
const atomB = counterAtom();
const [WriteA, updateValueA] = componentThatWritesAtom(atomA);
// Do two layers of selectors to test that the unsubscribing is recursive:
const selectorMapFn1 = jest.fn(x => x);
const sel1 = selector({
key: 'selUpstream',
get: ({get}) => selectorMapFn1(get(atomA)),
});
const selectorMapFn2 = jest.fn(x => x);
const sel2 = selector({
key: 'selDownstream',
get: ({get}) => selectorMapFn2(get(sel1)),
});
let toggleSwitch;
const Switch = () => {
const [value, setValue] = useState(false);
toggleSwitch = () => setValue(true);
return value ? <ReadsAtom atom={atomB} /> : <ReadsAtom atom={sel2} />;
};
const container = renderElements(
<>
<Switch />
<WriteA />
</>,
);
expect(container.textContent).toEqual('0');
expect(selectorMapFn1).toHaveBeenCalledTimes(1);
expect(selectorMapFn2).toHaveBeenCalledTimes(1);
act(() => updateValueA(1));
expect(container.textContent).toEqual('1');
expect(selectorMapFn1).toHaveBeenCalledTimes(2);
expect(selectorMapFn2).toHaveBeenCalledTimes(2);
act(() => toggleSwitch());
expect(container.textContent).toEqual('0');
expect(selectorMapFn1).toHaveBeenCalledTimes(2);
expect(selectorMapFn2).toHaveBeenCalledTimes(2);
act(() => updateValueA(2));
expect(container.textContent).toEqual('0');
expect(selectorMapFn1).toHaveBeenCalledTimes(2);
expect(selectorMapFn2).toHaveBeenCalledTimes(2);
},
);
testRecoil(
'Unsubscribes happen in case of unmounting of a suspended component',
() => {
const anAtom = counterAtom();
const [aSelector, _selFn] = plusOneSelector(anAtom);
// $FlowFixMe[incompatible-call]
const [_asyncSel, _adjustTimeout] = plusOneAsyncSelector(aSelector);
// FIXME to implement
},
);
testRecoil(
'Selectors stay up to date if deps are changed while they have no subscribers',
() => {
const anAtom = counterAtom();
const [aSelector, _] = plusOneSelector(anAtom);
let setVisible;
function Switch({children}: $TEMPORARY$object<{children: Node}>) {
const [visible, mySetVisible] = useState(true);
setVisible = mySetVisible;
return visible ? children : null;
}
const [Component, updateValue] = componentThatWritesAtom(anAtom);
const container = renderElements(
<>
<Component />
<Switch>
<ReadsAtom atom={aSelector} />
</Switch>
</>,
);
act(() => updateValue(1));
expect(container.textContent).toEqual('2');
act(() => setVisible(false));
expect(container.textContent).toEqual('');
act(() => updateValue(2));
expect(container.textContent).toEqual('');
act(() => setVisible(true));
expect(container.textContent).toEqual('3');
},
);
testRecoil(
'Selector subscriptions are correct when a selector is unsubscribed the second time',
async () => {
// This regression test would fail by an exception being thrown because subscription refcounts
// would would fall below zero.
const anAtom = counterAtom();
const [sel, _] = plusOneSelector(anAtom);
const [Toggle, toggle] = componentThatToggles(
<ReadsAtom atom={sel} />,
null,
);
const container = renderElements(
<>
<Toggle />
</>,
);
expect(container.textContent).toEqual('1');
act(() => toggle.current());
expect(container.textContent).toEqual('');
act(() => toggle.current());
expect(container.textContent).toEqual('1');
act(() => toggle.current());
expect(container.textContent).toEqual('');
},
);
});
testRecoil('Can set an atom during rendering', () => {
const anAtom = counterAtom();
function SetsDuringRendering() {
const [value, setValue] = useRecoilState(anAtom);
if (value !== 1) {
setValue(1);
}
return null;
}
const container = renderElements(
<>
<SetsDuringRendering />
<ReadsAtom atom={anAtom} />
</>,
);
expect(container.textContent).toEqual('1');
});
testRecoil(
'Does not re-create "setter" function after setting a value',
({strictMode, concurrentMode}) => {
const sm = strictMode && concurrentMode ? 2 : 1;
const anAtom = counterAtom();
const anotherAtom = counterAtom();
let useRecoilStateCounter = 0;
let useRecoilStateErrorStatesCounter = 0;
let useTwoAtomsCounter = 0;
function Component1() {
const [_, setValue] = useRecoilState(anAtom);
useEffect(() => {
setValue(1);
useRecoilStateCounter += 1;
}, [setValue]);
return null;
}
function Component2() {
const [_, setValue] = useRecoilStateLoadable(anAtom);
useEffect(() => {
setValue(2);
useRecoilStateErrorStatesCounter += 1;
}, [setValue]);
return null;
}
// It is important to test here that the component will re-render with the
// new setValue() function for a new atom, even if the value of the new
// atom is the same as the previous value of the previous atom.
function Component3() {
const a = useTwoAtomsCounter > 0 ? anotherAtom : anAtom;
// setValue fn should change when we use a different atom.
const [, setValue] = useRecoilState(a);
useEffect(() => {
setValue(1);
useTwoAtomsCounter += 1;
}, [setValue]);
return null;
}
renderElements(
<>
<Component1 />
<Component2 />
<Component3 />
</>,
);
expect(useRecoilStateCounter).toBe(1 * sm);
expect(useRecoilStateErrorStatesCounter).toBe(1 * sm);
// Component3's effect is ran twice because the atom changes and we get a new setter.
// StrictMode renders twice, but we only change atoms once. So, only one extra count.
expect(useTwoAtomsCounter).toBe(strictMode && concurrentMode ? 3 : 2);
},
);
testRecoil(
'Can set atom during post-atom-setting effect (NOT during initial render)',
async () => {
const anAtom = counterAtom();
let done = false;
function SetsDuringEffect() {
const setValue = useSetRecoilState(anAtom);
useEffect(() => {
Queue.enqueueExecution('SetsDuringEffect', () => {
if (!done) {
setValue(1);
done = true;
}
});
});
return null;
}
const container = renderElements(
<>
<SetsDuringEffect />
<ReadsAtom atom={anAtom} />
</>,
);
expect(container.textContent).toEqual('1');
},
);
testRecoil(
'Can set atom during post-atom-setting effect regardless of effect order',
async ({concurrentMode}) => {
// TODO Test doesn't work in ConcurrentMode. Haven't investigated why,
// but it seems fragile with the Queue for enforcing order.
if (concurrentMode) {
return;
}
function testWithOrder(
order: $TEMPORARY$array<
$TEMPORARY$string<'Batcher'> | $TEMPORARY$string<'SetsDuringEffect'>,
>,
) {
const anAtom = counterAtom();
let q: Array<[string, () => mixed]> = [];
let seen = false;
const original = Queue.enqueueExecution;
try {
Queue.enqueueExecution = (s, f) => {
if (s === order[0] || seen) {
seen = true;
f();
q.forEach(([_, g]) => g());
} else {
q.push([s, f]);
}
};
function SetsDuringEffect() {
const [value, setValue] = useRecoilState(anAtom);
useEffect(() => {
Queue.enqueueExecution('SetsDuringEffect', () => {
if (value !== 1) {
setValue(1);
}
});
});
return null;
}
const [Comp, updateValue] = componentThatWritesAtom(anAtom);
const container = renderElements(
<>
<SetsDuringEffect />
<ReadsAtom atom={anAtom} />
<Comp />
</>,
);
q = [];
seen = false;
// Thus it appears that it only breaks on the initial render.
act(() => {
updateValue(0);
});
expect(container.textContent).toEqual('1');
} finally {
Queue.enqueueExecution = original;
}
}
testWithOrder(['SetsDuringEffect', 'Batcher']);
testWithOrder(['Batcher', 'SetsDuringEffect']);
},
);
testRecoil('Hooks cannot be used outside of RecoilRoot', () => {
const myAtom = atom({key: 'hook outside RecoilRoot', default: 'INVALID'});
function Test() {
useRecoilValue(myAtom);
return 'TEST';
}
// Make sure there is a friendly error message mentioning <RecoilRoot>
expect(() => renderUnwrappedElements(<Test />)).toThrow('<RecoilRoot>');
});

View File

@@ -0,0 +1,117 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
useState,
flushSync,
act,
atom,
renderElements,
useRecoilState,
reactMode;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useState} = require('react'));
// @fb-only: ({flushSync} = require('ReactDOMComet'));
({flushSync} = require('react-dom')); // @oss-only
({act} = require('ReactTestUtils'));
atom = require('../../recoil_values/Recoil_atom');
({
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({reactMode} = require('../../core/Recoil_ReactMode'));
({useRecoilState} = require('../Recoil_Hooks'));
});
testRecoil('Sync React and Recoil state changes', ({gks}) => {
if (
reactMode().mode === 'MUTABLE_SOURCE' &&
!gks.includes('recoil_suppress_rerender_in_callback')
) {
return;
}
const myAtom = atom({key: 'sync react recoil', default: 0});
let setReact, setRecoil;
function Component() {
const [reactState, setReactState] = useState(0);
const [recoilState, setRecoilState] = useRecoilState(myAtom);
setReact = setReactState;
setRecoil = setRecoilState;
expect(reactState).toBe(recoilState);
return `${reactState} - ${recoilState}`;
}
const c = renderElements(<Component />);
expect(c.textContent).toBe('0 - 0');
// Set both React and Recoil state in the same batch and ensure the component
// render always seems consistent picture of both state changes.
act(() => {
flushSync(() => {
setReact(1);
setRecoil(1);
});
});
expect(c.textContent).toBe('1 - 1');
});
testRecoil('React and Recoil state change ordering', ({gks}) => {
if (
reactMode().mode === 'MUTABLE_SOURCE' &&
!gks.includes('recoil_suppress_rerender_in_callback')
) {
return;
}
const myAtom = atom({key: 'sync react recoil', default: 0});
let setReact, setRecoil;
function Component() {
const [reactState, setReactState] = useState(0);
const [recoilState, setRecoilState] = useRecoilState(myAtom);
setReact = setReactState;
setRecoil = setRecoilState;
// State changes may not be atomic. However, render functions should
// still see state changes in the order in which they were made.
expect(reactState).toBeGreaterThanOrEqual(recoilState);
return `${reactState} - ${recoilState}`;
}
const c = renderElements(<Component />);
expect(c.textContent).toBe('0 - 0');
// Test that changing React state before Recoil is seen in order
act(() => {
setReact(1);
setRecoil(1);
});
expect(c.textContent).toBe('1 - 1');
// Test that changing Recoil state before React is seen in order
act(() => {
setRecoil(0);
setReact(0);
});
expect(c.textContent).toBe('0 - 0');
});

View File

@@ -0,0 +1,321 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {RecoilState, RecoilValueReadOnly} from 'Recoil_RecoilValue';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
act,
atom,
selector,
ReadsAtom,
componentThatReadsAndWritesAtom,
renderElements,
useGetRecoilValueInfo;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({act} = require('ReactTestUtils'));
atom = require('../../recoil_values/Recoil_atom');
selector = require('../../recoil_values/Recoil_selector');
({
ReadsAtom,
componentThatReadsAndWritesAtom,
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
useGetRecoilValueInfo = require('../Recoil_useGetRecoilValueInfo');
});
testRecoil('useGetRecoilValueInfo', ({gks}) => {
const myAtom = atom<string>({
key: 'useGetRecoilValueInfo atom',
default: 'DEFAULT',
});
// $FlowFixMe[incompatible-call]
const selectorA = selector({
key: 'useGetRecoilValueInfo A',
get: ({get}) => get(myAtom),
});
const selectorB = selector({
key: 'useGetRecoilValueInfo B',
get: ({get}) => get(selectorA) + get(myAtom),
});
let getNodeInfo = (_: RecoilState<string> | RecoilValueReadOnly<string>) => {
expect(false).toBe(true);
throw new Error('getRecoilValue not set');
};
function GetRecoilValueInfo() {
const getRecoilValueInfo = useGetRecoilValueInfo();
// $FlowFixMe[incompatible-type]
getNodeInfo = node => ({...getRecoilValueInfo(node)});
return null;
}
// Initial status
renderElements(<GetRecoilValueInfo />);
expect(getNodeInfo(myAtom)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULT',
}),
isActive: false,
isSet: false,
isModified: false,
type: 'atom',
});
expect(Array.from(getNodeInfo(myAtom).deps)).toEqual([]);
expect(Array.from(getNodeInfo(myAtom).subscribers.nodes)).toEqual([]);
if (gks.includes('recoil_infer_component_names')) {
expect(Array.from(getNodeInfo(myAtom).subscribers.components)).toEqual([]);
}
expect(getNodeInfo(selectorA)).toMatchObject({
loadable: undefined,
isActive: false,
isSet: false,
isModified: false,
type: 'selector',
});
expect(Array.from(getNodeInfo(selectorA).deps)).toEqual([]);
expect(Array.from(getNodeInfo(selectorA).subscribers.nodes)).toEqual([]);
if (gks.includes('recoil_infer_component_names')) {
expect(Array.from(getNodeInfo(selectorA).subscribers.components)).toEqual(
[],
);
}
// $FlowFixMe[incompatible-call]
expect(getNodeInfo(selectorB)).toMatchObject({
loadable: undefined,
isActive: false,
isSet: false,
isModified: false,
type: 'selector',
});
// $FlowFixMe[incompatible-call]
expect(Array.from(getNodeInfo(selectorB).deps)).toEqual([]);
// $FlowFixMe[incompatible-call]
expect(Array.from(getNodeInfo(selectorB).subscribers.nodes)).toEqual([]);
if (gks.includes('recoil_infer_component_names')) {
// $FlowFixMe[incompatible-call]
expect(Array.from(getNodeInfo(selectorB).subscribers.components)).toEqual(
[],
);
}
// After reading values
const [ReadWriteAtom, setAtom, resetAtom] =
componentThatReadsAndWritesAtom(myAtom);
const c = renderElements(
<>
<GetRecoilValueInfo />
<ReadWriteAtom />
<ReadsAtom atom={selectorB} />
</>,
);
expect(c.textContent).toEqual('"DEFAULT""DEFAULTDEFAULT"');
expect(getNodeInfo(myAtom)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULT',
}),
isActive: true,
isSet: false,
isModified: false,
type: 'atom',
});
expect(Array.from(getNodeInfo(myAtom).deps)).toEqual([]);
expect(Array.from(getNodeInfo(myAtom).subscribers.nodes)).toEqual(
expect.arrayContaining([selectorA, selectorB]),
);
if (gks.includes('recoil_infer_component_names')) {
expect(Array.from(getNodeInfo(myAtom).subscribers.components)).toEqual([
{name: 'ReadsAndWritesAtom'},
]);
}
expect(getNodeInfo(selectorA)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULT',
}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector',
});
expect(Array.from(getNodeInfo(selectorA).deps)).toEqual(
expect.arrayContaining([myAtom]),
);
expect(Array.from(getNodeInfo(selectorA).subscribers.nodes)).toEqual(
expect.arrayContaining([selectorB]),
);
if (gks.includes('recoil_infer_component_names')) {
expect(Array.from(getNodeInfo(selectorA).subscribers.components)).toEqual(
[],
);
}
// $FlowFixMe[incompatible-call]
expect(getNodeInfo(selectorB)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULTDEFAULT',
}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector',
});
// $FlowFixMe[incompatible-call]
expect(Array.from(getNodeInfo(selectorB).deps)).toEqual(
expect.arrayContaining([myAtom, selectorA]),
);
// $FlowFixMe[incompatible-call]
expect(Array.from(getNodeInfo(selectorB).subscribers.nodes)).toEqual([]);
if (gks.includes('recoil_infer_component_names')) {
// $FlowFixMe[incompatible-call]
expect(Array.from(getNodeInfo(selectorB).subscribers.components)).toEqual([
{name: 'ReadsAtom'},
]);
}
// After setting a value
act(() => setAtom('SET'));
expect(getNodeInfo(myAtom)).toMatchObject({
loadable: expect.objectContaining({state: 'hasValue', contents: 'SET'}),
isActive: true,
isSet: true,
isModified: true,
type: 'atom',
});
expect(Array.from(getNodeInfo(myAtom).deps)).toEqual([]);
expect(Array.from(getNodeInfo(myAtom).subscribers.nodes)).toEqual(
expect.arrayContaining([selectorA, selectorB]),
);
if (gks.includes('recoil_infer_component_names')) {
expect(Array.from(getNodeInfo(myAtom).subscribers.components)).toEqual([
{name: 'ReadsAndWritesAtom'},
]);
}
expect(getNodeInfo(selectorA)).toMatchObject({
loadable: expect.objectContaining({state: 'hasValue', contents: 'SET'}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector',
});
expect(Array.from(getNodeInfo(selectorA).deps)).toEqual(
expect.arrayContaining([myAtom]),
);
expect(Array.from(getNodeInfo(selectorA).subscribers.nodes)).toEqual(
expect.arrayContaining([selectorB]),
);
if (gks.includes('recoil_infer_component_names')) {
expect(Array.from(getNodeInfo(selectorA).subscribers.components)).toEqual(
[],
);
}
// $FlowFixMe[incompatible-call]
expect(getNodeInfo(selectorB)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'SETSET',
}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector',
});
// $FlowFixMe[incompatible-call]
expect(Array.from(getNodeInfo(selectorB).deps)).toEqual(
expect.arrayContaining([myAtom, selectorA]),
);
// $FlowFixMe[incompatible-call]
expect(Array.from(getNodeInfo(selectorB).subscribers.nodes)).toEqual([]);
if (gks.includes('recoil_infer_component_names')) {
// $FlowFixMe[incompatible-call]
expect(Array.from(getNodeInfo(selectorB).subscribers.components)).toEqual([
{name: 'ReadsAtom'},
]);
}
// After reseting a value
act(resetAtom);
expect(getNodeInfo(myAtom)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULT',
}),
isActive: true,
isSet: false,
isModified: true,
type: 'atom',
});
expect(Array.from(getNodeInfo(myAtom).deps)).toEqual([]);
expect(Array.from(getNodeInfo(myAtom).subscribers.nodes)).toEqual(
expect.arrayContaining([selectorA, selectorB]),
);
if (gks.includes('recoil_infer_component_names')) {
expect(Array.from(getNodeInfo(myAtom).subscribers.components)).toEqual([
{name: 'ReadsAndWritesAtom'},
]);
}
expect(getNodeInfo(selectorA)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULT',
}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector',
});
expect(Array.from(getNodeInfo(selectorA).deps)).toEqual(
expect.arrayContaining([myAtom]),
);
expect(Array.from(getNodeInfo(selectorA).subscribers.nodes)).toEqual(
expect.arrayContaining([selectorB]),
);
if (gks.includes('recoil_infer_component_names')) {
expect(Array.from(getNodeInfo(selectorA).subscribers.components)).toEqual(
[],
);
}
// $FlowFixMe[incompatible-call]
expect(getNodeInfo(selectorB)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULTDEFAULT',
}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector',
});
// $FlowFixMe[incompatible-call]
expect(Array.from(getNodeInfo(selectorB).deps)).toEqual(
expect.arrayContaining([myAtom, selectorA]),
);
// $FlowFixMe[incompatible-call]
expect(Array.from(getNodeInfo(selectorB).subscribers.nodes)).toEqual([]);
if (gks.includes('recoil_infer_component_names')) {
// $FlowFixMe[incompatible-call]
expect(Array.from(getNodeInfo(selectorB).subscribers.components)).toEqual([
{name: 'ReadsAtom'},
]);
}
});

View File

@@ -0,0 +1,281 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
useState,
act,
freshSnapshot,
useGotoRecoilSnapshot,
useRecoilCallback,
useRecoilValue,
atom,
constSelector,
selector,
ReadsAtom,
asyncSelector,
componentThatReadsAndWritesAtom,
flushPromisesAndTimers,
renderElements;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useState} = require('react'));
({act} = require('ReactTestUtils'));
({freshSnapshot} = require('../../core/Recoil_Snapshot'));
({useRecoilValue} = require('../../hooks/Recoil_Hooks'));
({useGotoRecoilSnapshot} = require('../../hooks/Recoil_SnapshotHooks'));
({useRecoilCallback} = require('../../hooks/Recoil_useRecoilCallback'));
atom = require('../../recoil_values/Recoil_atom');
constSelector = require('../../recoil_values/Recoil_constSelector');
selector = require('../../recoil_values/Recoil_selector');
({
ReadsAtom,
asyncSelector,
componentThatReadsAndWritesAtom,
flushPromisesAndTimers,
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
});
testRecoil('Goto mapped snapshot', async () => {
const snapshot = freshSnapshot();
snapshot.retain();
const myAtom = atom({
key: 'Goto Snapshot Atom',
default: 'DEFAULT',
});
const [ReadsAndWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom);
const mySelector = constSelector(myAtom);
const updatedSnapshot = snapshot.map(({set}) => {
set(myAtom, 'SET IN SNAPSHOT');
});
updatedSnapshot.retain();
let gotoRecoilSnapshot;
function GotoRecoilSnapshot() {
gotoRecoilSnapshot = useGotoRecoilSnapshot();
return null;
}
const c = renderElements(
<>
<ReadsAndWritesAtom />
<ReadsAtom atom={mySelector} />
<GotoRecoilSnapshot />
</>,
);
expect(c.textContent).toEqual('"DEFAULT""DEFAULT"');
act(() => setAtom('SET IN CURRENT'));
expect(c.textContent).toEqual('"SET IN CURRENT""SET IN CURRENT"');
await expect(updatedSnapshot.getPromise(myAtom)).resolves.toEqual(
'SET IN SNAPSHOT',
);
act(() => gotoRecoilSnapshot(updatedSnapshot));
expect(c.textContent).toEqual('"SET IN SNAPSHOT""SET IN SNAPSHOT"');
act(() => setAtom('SET AGAIN IN CURRENT'));
expect(c.textContent).toEqual('"SET AGAIN IN CURRENT""SET AGAIN IN CURRENT"');
// Test that atoms set after snapshot were created are reset
act(() => gotoRecoilSnapshot(snapshot));
expect(c.textContent).toEqual('"DEFAULT""DEFAULT"');
});
testRecoil('Goto callback snapshot', () => {
const myAtom = atom({
key: 'Goto Snapshot From Callback',
default: 'DEFAULT',
});
const [ReadsAndWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom);
const mySelector = constSelector(myAtom);
let cb;
function RecoilCallback() {
const gotoSnapshot = useGotoRecoilSnapshot();
cb = useRecoilCallback(({snapshot}) => () => {
const updatedSnapshot = snapshot.map(({set}) => {
set(myAtom, 'SET IN SNAPSHOT');
});
gotoSnapshot(updatedSnapshot);
});
return null;
}
const c = renderElements(
<>
<ReadsAndWritesAtom />
<ReadsAtom atom={mySelector} />
<RecoilCallback />
</>,
);
expect(c.textContent).toEqual('"DEFAULT""DEFAULT"');
act(() => setAtom('SET IN CURRENT'));
expect(c.textContent).toEqual('"SET IN CURRENT""SET IN CURRENT"');
act(cb);
expect(c.textContent).toEqual('"SET IN SNAPSHOT""SET IN SNAPSHOT"');
});
testRecoil('Goto snapshot with dependent async selector', async () => {
const snapshot = freshSnapshot();
snapshot.retain();
const myAtom = atom({
key: 'atom for dep async snapshot',
default: 'DEFAULT',
});
const [ReadsAndWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom);
// $FlowFixMe[incompatible-call]
const mySelector = selector({
key: 'selector for async snapshot',
get: ({get}) => {
const dep = get(myAtom);
return Promise.resolve(dep);
},
});
const updatedSnapshot = snapshot.map(({set}) => {
set(myAtom, 'SET IN SNAPSHOT');
});
updatedSnapshot.retain();
let gotoRecoilSnapshot;
function GotoRecoilSnapshot() {
gotoRecoilSnapshot = useGotoRecoilSnapshot();
return null;
}
const c = renderElements(
<>
<ReadsAndWritesAtom />
<ReadsAtom atom={mySelector} />
<GotoRecoilSnapshot />
</>,
);
expect(c.textContent).toEqual('loading');
await flushPromisesAndTimers();
await flushPromisesAndTimers();
expect(c.textContent).toEqual('"DEFAULT""DEFAULT"');
act(() => setAtom('SET IN CURRENT'));
await flushPromisesAndTimers();
expect(c.textContent).toEqual('"SET IN CURRENT""SET IN CURRENT"');
await expect(updatedSnapshot.getPromise(myAtom)).resolves.toEqual(
'SET IN SNAPSHOT',
);
act(() => gotoRecoilSnapshot(updatedSnapshot));
await flushPromisesAndTimers();
expect(c.textContent).toEqual('"SET IN SNAPSHOT""SET IN SNAPSHOT"');
});
testRecoil('Goto snapshot with async selector', async () => {
const snapshot = freshSnapshot();
snapshot.retain();
const [mySelector, resolve] = asyncSelector<string, _>();
let gotoRecoilSnapshot;
function GotoRecoilSnapshot() {
gotoRecoilSnapshot = useGotoRecoilSnapshot();
return null;
}
const c = renderElements(
<>
<ReadsAtom atom={mySelector} />
<GotoRecoilSnapshot />
</>,
);
expect(c.textContent).toEqual('loading');
act(() => resolve('RESOLVE'));
await flushPromisesAndTimers();
await flushPromisesAndTimers();
expect(c.textContent).toEqual('"RESOLVE"');
act(() => gotoRecoilSnapshot(snapshot));
expect(c.textContent).toEqual('"RESOLVE"');
});
// Test that going to a snapshot where an atom was not yet initialized will
// not cause the atom to be re-initialized when used again.
testRecoil(
'Effects going to previous snapshot',
({strictMode, concurrentMode}) => {
const sm = strictMode && concurrentMode ? 2 : 1;
let init = 0;
const myAtom = atom({
key: 'gotoSnapshot effect',
default: 'DEFAULT',
effects: [
() => {
init++;
},
],
});
let forceUpdate;
function ReadAtom() {
const [_, setValue] = useState<{}>({});
forceUpdate = () => setValue({});
return useRecoilValue(myAtom);
}
let gotoRecoilSnapshot;
function GotoRecoilSnapshot() {
gotoRecoilSnapshot = useGotoRecoilSnapshot();
return null;
}
expect(init).toEqual(0);
renderElements(
<>
<ReadAtom />
<GotoRecoilSnapshot />
</>,
);
expect(init).toEqual(1 * sm);
act(forceUpdate);
expect(init).toEqual(1 * sm);
act(() => gotoRecoilSnapshot?.(freshSnapshot()));
expect(init).toEqual(1 * sm);
act(forceUpdate);
expect(init).toEqual(1 * sm);
act(forceUpdate);
expect(init).toEqual(1 * sm);
},
);

View File

@@ -0,0 +1,127 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {StoreID} from '../../core/Recoil_Keys';
import type {MutableSnapshot} from 'Recoil_Snapshot';
import type {Node} from 'react';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
renderElements,
renderUnwrappedElements,
useEffect,
useRef,
reactMode,
act,
RecoilRoot,
useRecoilStoreID,
atom,
componentThatReadsAndWritesAtom,
useRecoilBridgeAcrossReactRoots;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useEffect, useRef} = React);
({act} = require('ReactTestUtils'));
({reactMode} = require('../../core/Recoil_ReactMode'));
({
renderElements,
renderUnwrappedElements,
componentThatReadsAndWritesAtom,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({RecoilRoot, useRecoilStoreID} = require('../../core/Recoil_RecoilRoot'));
atom = require('../../recoil_values/Recoil_atom');
useRecoilBridgeAcrossReactRoots = require('../Recoil_useRecoilBridgeAcrossReactRoots');
});
function NestedReactRoot({children}: $TEMPORARY$object<{children: Node}>) {
const ref = useRef<?HTMLDivElement>();
const RecoilBridge = useRecoilBridgeAcrossReactRoots();
useEffect(() => {
renderUnwrappedElements(
<RecoilBridge>{children}</RecoilBridge>,
ref.current,
);
}, [RecoilBridge, children]);
return <div ref={ref} />;
}
testRecoil(
'useRecoilBridgeAcrossReactRoots - create a context bridge',
async ({concurrentMode}) => {
// Test fails with useRecoilBridgeAcrossReactRoots() and useMutableSource().
// It only reproduces if act() is used in renderElements() for the nested
// root, so it may just be a testing environment issue.
if (concurrentMode && reactMode().mode === 'MUTABLE_SOURCE') {
return;
}
const myAtom = atom({
key: 'useRecoilBridgeAcrossReactRoots - context bridge',
default: 'DEFAULT',
});
function initializeState({set, getLoadable}: MutableSnapshot) {
expect(getLoadable(myAtom).contents).toEqual('DEFAULT');
set(myAtom, 'INITIALIZE');
expect(getLoadable(myAtom).contents).toEqual('INITIALIZE');
}
const [ReadWriteAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom);
const container = renderElements(
<RecoilRoot initializeState={initializeState}>
<ReadWriteAtom />
<NestedReactRoot>
<ReadWriteAtom />
</NestedReactRoot>
</RecoilRoot>,
);
expect(container.textContent).toEqual('"INITIALIZE""INITIALIZE"');
act(() => setAtom('SET'));
expect(container.textContent).toEqual('"SET""SET"');
},
);
testRecoil('StoreID matches bridged store', () => {
function RecoilStoreID({storeIDRef}: {storeIDRef: {current: ?StoreID}}) {
storeIDRef.current = useRecoilStoreID();
return null;
}
const rootStoreIDRef = {current: null};
const nestedStoreIDRef = {current: null};
const c = renderElements(
<>
<RecoilStoreID storeIDRef={rootStoreIDRef} />
<NestedReactRoot>
<RecoilStoreID storeIDRef={nestedStoreIDRef} />
</NestedReactRoot>
RENDER
</>,
);
expect(c.textContent).toEqual('RENDER');
expect(rootStoreIDRef.current).toBe(nestedStoreIDRef.current);
expect(rootStoreIDRef.current).not.toBe(null);
});

View File

@@ -0,0 +1,845 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {Snapshot} from '../../core/Recoil_Snapshot';
import type {RecoilCallbackInterface} from '../Recoil_useRecoilCallback';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
useRef,
useState,
useEffect,
act,
useStoreRef,
atom,
atomFamily,
selector,
useRecoilCallback,
useRecoilValue,
useRecoilState,
useSetRecoilState,
useResetRecoilState,
ReadsAtom,
flushPromisesAndTimers,
renderElements,
stringAtom,
invariant;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useRef, useState, useEffect} = require('react'));
({act} = require('ReactTestUtils'));
({useStoreRef} = require('../../core/Recoil_RecoilRoot'));
({
atom,
atomFamily,
selector,
useRecoilCallback,
useSetRecoilState,
useResetRecoilState,
useRecoilValue,
useRecoilState,
} = require('../../Recoil_index'));
({
ReadsAtom,
flushPromisesAndTimers,
renderElements,
stringAtom,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
invariant = require('recoil-shared/util/Recoil_invariant');
});
testRecoil('Reads Recoil values', async () => {
const anAtom = atom({key: 'atom1', default: 'DEFAULT'});
let pTest: ?Promise<mixed> = Promise.reject(
new Error("Callback didn't resolve"),
);
let cb;
function Component() {
cb = useRecoilCallback(({snapshot}) => () => {
// eslint-disable-next-line jest/valid-expect
pTest = expect(snapshot.getPromise(anAtom)).resolves.toBe('DEFAULT');
});
return null;
}
renderElements(<Component />);
act(() => void cb());
await pTest;
});
testRecoil('Can read Recoil values without throwing', async () => {
const anAtom = atom({key: 'atom2', default: 123});
const asyncSelector = selector({
key: 'sel',
get: () => {
return new Promise(() => undefined);
},
});
let didRun = false;
let cb;
function Component() {
cb = useRecoilCallback(({snapshot}) => () => {
expect(snapshot.getLoadable(anAtom)).toMatchObject({
state: 'hasValue',
contents: 123,
});
expect(snapshot.getLoadable(asyncSelector)).toMatchObject({
state: 'loading',
});
didRun = true; // ensure these assertions do get made
});
return null;
}
renderElements(<Component />);
act(() => void cb());
expect(didRun).toBe(true);
});
testRecoil('Sets Recoil values (by queueing them)', async () => {
const anAtom = atom({key: 'atom3', default: 'DEFAULT'});
let cb;
let pTest: ?Promise<mixed> = Promise.reject(
new Error("Callback didn't resolve"),
);
function Component() {
// $FlowFixMe[missing-local-annot]
cb = useRecoilCallback(({snapshot, set}) => value => {
set(anAtom, value);
// eslint-disable-next-line jest/valid-expect
pTest = expect(snapshot.getPromise(anAtom)).resolves.toBe('DEFAULT');
});
return null;
}
const container = renderElements(
<>
<Component />
<ReadsAtom atom={anAtom} />
</>,
);
expect(container.textContent).toBe('"DEFAULT"');
act(() => void cb(123));
expect(container.textContent).toBe('123');
await pTest;
});
testRecoil('Reset Recoil values', async () => {
const anAtom = atom({key: 'atomReset', default: 'DEFAULT'});
let setCB, resetCB;
function Component() {
setCB = useRecoilCallback(
({set}) =>
// $FlowFixMe[missing-local-annot]
value =>
set(anAtom, value),
);
resetCB = useRecoilCallback(
({reset}) =>
() =>
reset(anAtom),
);
return null;
}
const container = renderElements(
<>
<Component />
<ReadsAtom atom={anAtom} />
</>,
);
expect(container.textContent).toBe('"DEFAULT"');
act(() => void setCB(123));
expect(container.textContent).toBe('123');
act(() => void resetCB());
expect(container.textContent).toBe('"DEFAULT"');
});
testRecoil('Sets Recoil values from async callback', async () => {
const anAtom = atom({key: 'set async callback', default: 'DEFAULT'});
let cb;
const pTest = [];
function Component() {
// $FlowFixMe[missing-local-annot]
cb = useRecoilCallback(({snapshot, set}) => async value => {
set(anAtom, value);
pTest.push(
// eslint-disable-next-line jest/valid-expect
expect(snapshot.getPromise(anAtom)).resolves.toBe(
value === 123 ? 'DEFAULT' : 123,
),
);
});
return null;
}
const container = renderElements([
<Component />,
<ReadsAtom atom={anAtom} />,
]);
expect(container.textContent).toBe('"DEFAULT"');
act(() => void cb(123));
expect(container.textContent).toBe('123');
act(() => void cb(456));
expect(container.textContent).toBe('456');
for (const aTest of pTest) {
await aTest;
}
});
testRecoil('Reads from a snapshot created at callback call time', async () => {
const anAtom = atom({key: 'atom4', default: 123});
let cb;
let setter;
let seenValue = null;
let delay = () => new Promise(r => r()); // no delay initially
function Component() {
setter = useSetRecoilState(anAtom);
cb = useRecoilCallback(({snapshot}) => async () => {
snapshot.retain();
await delay();
seenValue = await snapshot.getPromise(anAtom);
});
return null;
}
// It sees an update flushed after the cb is created:
renderElements(<Component />);
act(() => setter(345));
act(() => void cb());
await flushPromisesAndTimers();
await flushPromisesAndTimers();
expect(seenValue).toBe(345);
// But does not see an update flushed while the cb is in progress:
seenValue = null;
let resumeCallback: () => void = () =>
invariant(false, 'must be initialized');
delay = () => {
return new Promise(resolve => {
resumeCallback = resolve;
});
};
act(() => void cb());
act(() => setter(678));
resumeCallback();
await flushPromisesAndTimers();
await flushPromisesAndTimers();
expect(seenValue).toBe(345);
});
testRecoil('Setter updater sees latest state', () => {
const myAtom = atom({key: 'useRecoilCallback updater', default: 'DEFAULT'});
let setAtom;
let cb;
function Component() {
setAtom = useSetRecoilState(myAtom);
// $FlowFixMe[missing-local-annot]
cb = useRecoilCallback(({snapshot, set}) => prevValue => {
// snapshot sees a snapshot with the latest set state
expect(snapshot.getLoadable(myAtom).contents).toEqual(prevValue);
// Test that callback sees value updates from within the same transaction
set(myAtom, value => {
expect(value).toEqual(prevValue);
return 'UPDATE';
});
set(myAtom, value => {
expect(value).toEqual('UPDATE');
return 'UPDATE AGAIN';
});
});
return null;
}
const c = renderElements(
<>
<ReadsAtom atom={myAtom} />
<Component />
</>,
);
expect(c.textContent).toEqual('"DEFAULT"');
// Set and callback in the same transaction
act(() => {
setAtom('SET');
cb('SET');
cb('UPDATE AGAIN');
});
expect(c.textContent).toEqual('"UPDATE AGAIN"');
});
testRecoil('Snapshot from effect uses rendered state', () => {
const myAtom = stringAtom();
let setState,
actCallback,
effectCallback,
actCallbackValue,
effectCallbackValue,
effectValue;
function Component() {
setState = useSetRecoilState(myAtom);
const value = useRecoilValue(myAtom);
effectCallback = useRecoilCallback(
({snapshot}) =>
() => {
effectCallbackValue = snapshot.getLoadable(myAtom).getValue();
},
[],
);
actCallback = useRecoilCallback(
({snapshot}) =>
() => {
actCallbackValue = snapshot.getLoadable(myAtom).getValue();
},
[],
);
useEffect(() => {
effectValue = value;
effectCallback();
}, [value]);
return null;
}
renderElements(<Component />);
act(() => {
setState('SET');
actCallback();
});
expect(actCallbackValue).toBe('SET');
expect(effectValue).toBe('SET');
expect(effectCallbackValue).toBe('SET');
});
testRecoil('goes to snapshot', async () => {
const myAtom = atom({
key: 'Goto Snapshot From Callback',
default: 'DEFAULT',
});
let cb;
function RecoilCallback() {
cb = useRecoilCallback(({snapshot, gotoSnapshot}) => () => {
const updatedSnapshot = snapshot.map(({set}) => {
set(myAtom, 'SET IN SNAPSHOT');
});
expect(updatedSnapshot.getLoadable(myAtom).contents).toEqual(
'SET IN SNAPSHOT',
);
gotoSnapshot(updatedSnapshot);
});
return null;
}
const c = renderElements(
<>
<ReadsAtom atom={myAtom} />
<RecoilCallback />
</>,
);
expect(c.textContent).toEqual('"DEFAULT"');
act(() => void cb());
await flushPromisesAndTimers();
expect(c.textContent).toEqual('"SET IN SNAPSHOT"');
});
testRecoil('Updates are batched', () => {
const family = atomFamily<_, number>({
key: 'useRecoilCallback/batching/family',
default: 0,
});
let cb;
function RecoilCallback() {
cb = useRecoilCallback(({set}) => () => {
for (let i = 0; i < 100; i++) {
set(family(i), 1);
}
});
return null;
}
let store: any; // flowlint-line unclear-type:off
function GetStore() {
store = useStoreRef().current;
return null;
}
renderElements(
<>
<RecoilCallback />
<GetStore />
</>,
);
invariant(store, 'store should be initialized');
const originalReplaceState = store.replaceState;
store.replaceState = jest.fn(originalReplaceState);
expect(store.replaceState).toHaveBeenCalledTimes(0);
act(() => cb());
expect(store.replaceState).toHaveBeenCalledTimes(1);
store.replaceState = originalReplaceState;
});
// Test that we always get a consistent instance of the callback function
// from useRecoilCallback() when it is memoizaed
testRecoil('Consistent callback function', () => {
let setIteration;
const Component = () => {
const [iteration, _setIteration] = useState(0);
setIteration = _setIteration;
const callback = useRecoilCallback(() => () => {});
const callbackRef = useRef(callback);
iteration
? expect(callback).not.toBe(callbackRef.current)
: expect(callback).toBe(callbackRef.current);
const callbackMemoized = useRecoilCallback(() => () => {}, []);
const callbackMemoizedRef = useRef(callbackMemoized);
expect(callbackMemoized).toBe(callbackMemoizedRef.current);
return iteration;
};
const out = renderElements(<Component />);
expect(out.textContent).toBe('0');
act(() => setIteration(1)); // Force a re-render of the Component
expect(out.textContent).toBe('1');
});
describe('Atom Effects', () => {
testRecoil(
'Atom effects are initialized twice if first seen on snapshot and then on root store',
({strictMode, concurrentMode}) => {
const sm = strictMode ? 1 : 0;
let numTimesEffectInit = 0;
const atomWithEffect = atom({
key: 'atomWithEffect',
default: 0,
effects: [
() => {
numTimesEffectInit++;
},
],
});
// StrictMode will render the component twice
let renderCount = 0;
const Component = () => {
const readAtomFromSnapshot = useRecoilCallback(({snapshot}) => () => {
snapshot.getLoadable(atomWithEffect);
});
readAtomFromSnapshot(); // first initialization
expect(numTimesEffectInit).toBe(1 + sm * renderCount);
useRecoilValue(atomWithEffect); // second initialization
expect(numTimesEffectInit).toBe(2);
renderCount++;
return null;
};
const c = renderElements(<Component />);
expect(c.textContent).toBe(''); // Confirm no failures from rendering
expect(numTimesEffectInit).toBe(strictMode && concurrentMode ? 3 : 2);
},
);
testRecoil(
'Atom effects are initialized once if first seen on root store and then on snapshot',
({strictMode, concurrentMode}) => {
let numTimesEffectInit = 0;
const atomWithEffect = atom({
key: 'atomWithEffect2',
default: 0,
effects: [
() => {
numTimesEffectInit++;
},
],
});
const Component = () => {
const readAtomFromSnapshot = useRecoilCallback(({snapshot}) => () => {
snapshot.getLoadable(atomWithEffect);
});
useRecoilValue(atomWithEffect); // first initialization
expect(numTimesEffectInit).toBe(1);
/**
* should not re-initialize b/c snapshot should inherit from latest state,
* wherein atom was already initialized
*/
readAtomFromSnapshot();
expect(numTimesEffectInit).toBe(1);
return null;
};
const c = renderElements(<Component />);
expect(c.textContent).toBe(''); // Confirm no failures from rendering
expect(numTimesEffectInit).toBe(strictMode && concurrentMode ? 2 : 1);
},
);
testRecoil('onSet() called when atom initialized with snapshot', () => {
const setValues = [];
const myAtom = atom({
key: 'useRecoilCallback - atom effect - onSet',
default: 0,
effects: [
({onSet, setSelf}) => {
onSet(value => {
setValues.push(value);
// Confirm setSelf() still valid when initialized from snapshot
setSelf(value + 1);
});
},
],
});
let setAtom;
const Component = () => {
const readAtomFromSnapshot = useRecoilCallback(({snapshot}) => () => {
snapshot.getLoadable(myAtom);
});
// First initialization with snapshot
readAtomFromSnapshot();
// Second initialization with hook
let value;
[value, setAtom] = useRecoilState(myAtom);
return value;
};
const c = renderElements(<Component />);
expect(c.textContent).toBe('0');
expect(setValues).toEqual([]);
act(() => setAtom(1));
expect(setValues).toEqual([1]);
expect(c.textContent).toBe('2');
});
});
describe('Selector Cache', () => {
testRecoil('Refresh selector cache - transitive', () => {
const getA = jest.fn(() => 'A');
// $FlowFixMe[incompatible-call]
const selectorA = selector({
key: 'useRecoilCallback refresh ancestors A',
get: getA,
});
const getB = jest.fn(({get}) => get(selectorA) + 'B');
const selectorB = selector({
key: 'useRecoilCallback refresh ancestors B',
get: getB,
});
const getC = jest.fn(({get}) => get(selectorB) + 'C');
const selectorC = selector({
key: 'useRecoilCallback refresh ancestors C',
get: getC,
});
let refreshSelector;
function Component() {
refreshSelector = useRecoilCallback(({refresh}) => () => {
refresh(selectorC);
});
return useRecoilValue(selectorC);
}
const container = renderElements(<Component />);
expect(container.textContent).toBe('ABC');
expect(getC).toHaveBeenCalledTimes(1);
expect(getB).toHaveBeenCalledTimes(1);
expect(getA).toHaveBeenCalledTimes(1);
act(() => refreshSelector());
expect(container.textContent).toBe('ABC');
expect(getC).toHaveBeenCalledTimes(2);
expect(getB).toHaveBeenCalledTimes(2);
expect(getA).toHaveBeenCalledTimes(2);
});
testRecoil('Refresh selector cache - clears entire cache', async () => {
const myatom = atom({
key: 'useRecoilCallback refresh entire cache atom',
default: 'a',
});
let i = 0;
// $FlowFixMe[incompatible-call]
const myselector = selector({
key: 'useRecoilCallback refresh entire cache selector',
get: ({get}) => [get(myatom), i++],
});
let setMyAtom;
let refreshSelector;
function Component() {
const [atomValue, iValue] = useRecoilValue(myselector);
refreshSelector = useRecoilCallback(({refresh}) => () => {
refresh(myselector);
});
setMyAtom = useSetRecoilState(myatom);
return `${atomValue}-${iValue}`;
}
const container = renderElements(<Component />);
expect(container.textContent).toBe('a-0');
act(() => setMyAtom('b'));
expect(container.textContent).toBe('b-1');
act(() => refreshSelector());
expect(container.textContent).toBe('b-2');
act(() => setMyAtom('a'));
expect(container.textContent).toBe('a-3');
});
});
describe('Snapshot', () => {
testRecoil('Snapshot is retained for async callbacks', async ({gks}) => {
let callback,
callbackSnapshot,
resolveSelector,
resolveSelector2,
resolveCallback;
const myAtom = stringAtom();
// $FlowFixMe[incompatible-call]
const mySelector1 = selector({
key: 'useRecoilCallback snapshot retain 1',
get: async ({get}) => {
await new Promise(resolve => {
resolveSelector = resolve;
});
return get(myAtom);
},
});
// $FlowFixMe[incompatible-call]
const mySelector2 = selector({
key: 'useRecoilCallback snapshot retain 2',
get: async ({get}) => {
await new Promise(resolve => {
resolveSelector2 = resolve;
});
return get(myAtom);
},
});
function Component() {
callback = useRecoilCallback(({snapshot}) => async () => {
callbackSnapshot = snapshot;
return new Promise(resolve => {
resolveCallback = resolve;
});
});
return null;
}
renderElements(<Component />);
callback?.();
const selector1Promise = callbackSnapshot?.getPromise(mySelector1);
const selector2Promise = callbackSnapshot?.getPromise(mySelector2);
// Wait to allow callback snapshot to auto-release after clock tick.
// It should still be retained for duration of callback, though.
await flushPromisesAndTimers();
// Selectors resolving before callback is resolved should not be canceled
act(() => resolveSelector());
await expect(selector1Promise).resolves.toBe('DEFAULT');
// Selectors resolving after callback is resolved should be canceled
if (gks.includes('recoil_memory_managament_2020')) {
act(() => resolveCallback());
act(() => resolveSelector2());
await expect(selector2Promise).rejects.toEqual({});
}
});
testRecoil('Access snapshot asynchronously', async () => {
const myAtom = stringAtom();
let setAtom;
function Component() {
const childFunction = async (
args: RecoilCallbackInterface,
newValue: string,
) => {
const oldValue = await args.snapshot.getPromise(myAtom);
args.set(myAtom, newValue);
return oldValue;
};
const parentFunction = async (
args: RecoilCallbackInterface,
newValue: string,
) => {
await Promise.resolve();
return childFunction(args, newValue);
};
setAtom = useRecoilCallback(
// $FlowFixMe[missing-local-annot]
args => async newValue => parentFunction(args, newValue),
);
return useRecoilValue(myAtom);
}
const c = renderElements(<Component />);
expect(c.textContent).toBe('DEFAULT');
let oldValue;
await act(async () => (oldValue = await setAtom('SET')));
expect(oldValue).toBe('DEFAULT');
expect(c.textContent).toBe('SET');
await act(async () => (oldValue = await setAtom('SET2')));
expect(oldValue).toBe('SET');
expect(c.textContent).toBe('SET2');
});
testRecoil('Snapshot is cached', () => {
const myAtom = stringAtom();
let getSnapshot;
let setMyAtom, resetMyAtom;
function Component() {
getSnapshot = useRecoilCallback(
({snapshot}) =>
() =>
snapshot,
);
setMyAtom = useSetRecoilState(myAtom);
resetMyAtom = useResetRecoilState(myAtom);
return null;
}
renderElements(<Component />);
const getAtom = (snapshot: void | Snapshot) =>
snapshot?.getLoadable(myAtom).getValue();
const initialSnapshot = getSnapshot?.();
expect(getAtom(initialSnapshot)).toEqual('DEFAULT');
// If there are no state changes, the snapshot should be cached
const nextSnapshot = getSnapshot?.();
expect(getAtom(nextSnapshot)).toEqual('DEFAULT');
expect(nextSnapshot).toBe(initialSnapshot);
// With a state change, there is a new snapshot
act(() => setMyAtom('SET'));
const setSnapshot = getSnapshot?.();
expect(getAtom(setSnapshot)).toEqual('SET');
expect(setSnapshot).not.toBe(initialSnapshot);
const nextSetSnapshot = getSnapshot?.();
expect(getAtom(nextSetSnapshot)).toEqual('SET');
expect(nextSetSnapshot).toBe(setSnapshot);
act(() => setMyAtom('SET2'));
const set2Snapshot = getSnapshot?.();
expect(getAtom(set2Snapshot)).toEqual('SET2');
expect(set2Snapshot).not.toBe(initialSnapshot);
expect(set2Snapshot).not.toBe(setSnapshot);
const nextSet2Snapshot = getSnapshot?.();
expect(getAtom(nextSet2Snapshot)).toEqual('SET2');
expect(nextSet2Snapshot).toBe(set2Snapshot);
act(() => resetMyAtom());
const resetSnapshot = getSnapshot?.();
expect(getAtom(resetSnapshot)).toEqual('DEFAULT');
expect(resetSnapshot).not.toBe(initialSnapshot);
expect(resetSnapshot).not.toBe(setSnapshot);
const nextResetSnapshot = getSnapshot?.();
expect(getAtom(nextResetSnapshot)).toEqual('DEFAULT');
expect(nextResetSnapshot).toBe(resetSnapshot);
});
testRecoil('cached snapshot is invalidated if not retained', async () => {
const myAtom = stringAtom();
let getSnapshot;
let setMyAtom;
function Component() {
getSnapshot = useRecoilCallback(
({snapshot}) =>
() =>
snapshot,
);
setMyAtom = useSetRecoilState(myAtom);
return null;
}
renderElements(<Component />);
const getAtom = (snapshot: void | Snapshot) =>
snapshot?.getLoadable(myAtom).getValue();
act(() => setMyAtom('SET'));
const setSnapshot = getSnapshot?.();
expect(getAtom(setSnapshot)).toEqual('SET');
// If cached snapshot is released, a new snapshot is provided
await flushPromisesAndTimers();
const nextSetSnapshot = getSnapshot?.();
expect(nextSetSnapshot).not.toBe(setSnapshot);
expect(getAtom(nextSetSnapshot)).toEqual('SET');
act(() => setMyAtom('SET2'));
const set2Snapshot = getSnapshot?.();
expect(getAtom(set2Snapshot)).toEqual('SET2');
expect(set2Snapshot).not.toBe(setSnapshot);
// If cached snapshot is retained, then it is used again
set2Snapshot?.retain();
await flushPromisesAndTimers();
const nextSet2Snapshot = getSnapshot?.();
expect(getAtom(nextSet2Snapshot)).toEqual('SET2');
expect(nextSet2Snapshot).toBe(set2Snapshot);
});
});

View File

@@ -0,0 +1,150 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {RecoilInterface} from 'Recoil_Hooks';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
useRef,
useState,
act,
atom,
counterAtom,
renderElements,
useRecoilInterface;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useRef, useState} = require('react'));
({act} = require('ReactTestUtils'));
atom = require('../../recoil_values/Recoil_atom');
({
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({useRecoilInterface} = require('../Recoil_Hooks'));
counterAtom = atom({
key: `counterAtom`,
default: 0,
});
});
testRecoil('Interface for non-react code - useRecoilState', () => {
function nonReactCode(recoilInterface: RecoilInterface) {
return recoilInterface.getRecoilState(counterAtom);
}
let updateValue;
const Component = () => {
const recoilInterface = useRecoilInterface();
const [value, _updateValue] = nonReactCode(recoilInterface);
updateValue = _updateValue;
return value;
};
const container = renderElements(<Component />);
expect(container.textContent).toEqual('0');
act(() => updateValue(1));
expect(container.textContent).toEqual('1');
});
testRecoil('Interface for non-react code - useRecoilStateNoThrow', () => {
function nonReactCode(recoilInterface: RecoilInterface) {
const [loadable, setValue] =
recoilInterface.getRecoilStateLoadable(counterAtom);
const value = loadable.state === 'hasValue' ? loadable.contents : null;
return [value, setValue];
}
let updateValue;
const Component = () => {
const recoilInterface = useRecoilInterface();
const [value, _updateValue] = nonReactCode(recoilInterface);
updateValue = _updateValue;
return value;
};
const container = renderElements(<Component />);
expect(container.textContent).toEqual('0');
act(() => updateValue(1));
expect(container.textContent).toEqual('1');
});
testRecoil(
'Interface for non-react code - useRecoilValue, useSetRecoilState',
() => {
function nonReactCode(recoilInterface: RecoilInterface) {
return [
recoilInterface.getRecoilValue(counterAtom),
recoilInterface.getSetRecoilState(counterAtom),
];
}
let updateValue;
const Component = () => {
const recoilInterface = useRecoilInterface();
const [value, _updateValue] = nonReactCode(recoilInterface);
updateValue = _updateValue;
return value;
};
const container = renderElements(<Component />);
expect(container.textContent).toEqual('0');
act(() => updateValue(1));
expect(container.textContent).toEqual('1');
},
);
testRecoil('Interface for non-react code - useRecoilValueNoThrow', () => {
function nonReactCode(recoilInterface: RecoilInterface) {
const value = recoilInterface
.getRecoilValueLoadable(counterAtom)
.valueMaybe();
const setValue = recoilInterface.getSetRecoilState(counterAtom);
return [value, setValue];
}
let updateValue;
const Component = () => {
const recoilInterface = useRecoilInterface();
const [value, _updateValue] = nonReactCode(recoilInterface);
updateValue = _updateValue;
return value;
};
const container = renderElements(<Component />);
expect(container.textContent).toEqual('0');
act(() => updateValue(1));
expect(container.textContent).toEqual('1');
});
// Test that we always get a consistent instance of the interface object and
// hooks from useRecoilInterface() (at least for a given <AppRoot> store)
testRecoil('Consistent interface object', () => {
let setValue;
const Component = () => {
const [value, _setValue] = useState(0);
const recoilInterface = useRecoilInterface();
const recoilInterfaceRef = useRef(recoilInterface);
expect(recoilInterface).toBe(recoilInterfaceRef.current);
expect(recoilInterface.getRecoilState).toBe(recoilInterface.getRecoilState);
setValue = _setValue;
return value;
};
const out = renderElements(<Component />);
expect(out.textContent).toBe('0');
act(() => setValue(1)); // Force a re-render of the Component
expect(out.textContent).toBe('1');
});

View File

@@ -0,0 +1,147 @@
/**
* (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
act,
atom,
selector,
renderElements,
useRecoilValue,
useSetRecoilState,
useRecoilRefresher;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({act} = require('ReactTestUtils'));
atom = require('../../recoil_values/Recoil_atom');
selector = require('../../recoil_values/Recoil_selector');
({
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
useRecoilRefresher = require('../Recoil_useRecoilRefresher');
({useRecoilValue, useSetRecoilState} = require('../Recoil_Hooks'));
});
testRecoil('useRecoilRefresher - no-op for atom', async () => {
const myAtom = atom({
key: 'useRecoilRefresher no-op',
default: 'default',
});
let refresh;
function Component() {
const value = useRecoilValue(myAtom);
refresh = useRecoilRefresher(myAtom);
return value;
}
const container = renderElements(<Component />);
expect(container.textContent).toBe('default');
act(() => refresh());
expect(container.textContent).toBe('default');
});
testRecoil('useRecoilRefresher - re-executes selector', async () => {
let i = 0;
const myselector = selector({
key: 'useRecoilRefresher re-execute',
get: () => i++,
});
let refresh;
function Component() {
const value = useRecoilValue(myselector);
refresh = useRecoilRefresher(myselector);
return value;
}
const container = renderElements(<Component />);
expect(container.textContent).toBe('0');
act(() => refresh());
expect(container.textContent).toBe('1');
});
testRecoil('useRecoilRefresher - clears entire cache', async () => {
const myatom = atom({
key: 'useRecoilRefresher entire cache atom',
default: 'a',
});
let i = 0;
// $FlowFixMe[incompatible-call]
const myselector = selector({
key: 'useRecoilRefresher entire cache selector',
get: ({get}) => [get(myatom), i++],
});
let setMyAtom;
let refresh;
function Component() {
const [atomValue, iValue] = useRecoilValue(myselector);
refresh = useRecoilRefresher(myselector);
setMyAtom = useSetRecoilState(myatom);
return `${atomValue}-${iValue}`;
}
const container = renderElements(<Component />);
expect(container.textContent).toBe('a-0');
act(() => setMyAtom('b'));
expect(container.textContent).toBe('b-1');
act(() => refresh());
expect(container.textContent).toBe('b-2');
act(() => setMyAtom('a'));
expect(container.textContent).toBe('a-3');
});
testRecoil('useRecoilRefresher - clears ancestor selectors', async () => {
const getA = jest.fn(() => 'A');
// $FlowFixMe[incompatible-call]
const selectorA = selector({
key: 'useRecoilRefresher ancestors A',
get: getA,
});
const getB = jest.fn(({get}) => get(selectorA) + 'B');
const selectorB = selector({
key: 'useRecoilRefresher ancestors B',
get: getB,
});
const getC = jest.fn(({get}) => get(selectorB) + 'C');
const selectorC = selector({
key: 'useRecoilRefresher ancestors C',
get: getC,
});
let refresh;
function Component() {
refresh = useRecoilRefresher(selectorC);
return useRecoilValue(selectorC);
}
const container = renderElements(<Component />);
expect(container.textContent).toBe('ABC');
expect(getC).toHaveBeenCalledTimes(1);
expect(getB).toHaveBeenCalledTimes(1);
expect(getA).toHaveBeenCalledTimes(1);
act(() => refresh());
expect(container.textContent).toBe('ABC');
expect(getC).toHaveBeenCalledTimes(2);
expect(getB).toHaveBeenCalledTimes(2);
expect(getA).toHaveBeenCalledTimes(2);
});

View File

@@ -0,0 +1,505 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
useEffect,
useState,
act,
freshSnapshot,
atom,
constSelector,
selector,
ReadsAtom,
asyncSelector,
stringAtom,
componentThatReadsAndWritesAtom,
flushPromisesAndTimers,
renderElements,
useGotoRecoilSnapshot,
useRecoilSnapshot;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useEffect, useState} = React);
({act} = require('ReactTestUtils'));
({freshSnapshot} = require('../../core/Recoil_Snapshot'));
atom = require('../../recoil_values/Recoil_atom');
constSelector = require('../../recoil_values/Recoil_constSelector');
selector = require('../../recoil_values/Recoil_selector');
({
ReadsAtom,
asyncSelector,
stringAtom,
componentThatReadsAndWritesAtom,
flushPromisesAndTimers,
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({
useGotoRecoilSnapshot,
useRecoilSnapshot,
} = require('../Recoil_SnapshotHooks'));
});
testRecoil('useRecoilSnapshot - subscribe to updates', ({strictMode}) => {
if (strictMode) {
return;
}
const myAtom = stringAtom();
const [ReadsAndWritesAtom, setAtom, resetAtom] =
componentThatReadsAndWritesAtom(myAtom);
const mySelector = constSelector(myAtom);
const snapshots = [];
function RecoilSnapshotAndSubscribe() {
const snapshot = useRecoilSnapshot();
snapshot.retain();
snapshots.push(snapshot);
return null;
}
const c = renderElements(
<>
<ReadsAndWritesAtom />
<ReadsAtom atom={mySelector} />
<RecoilSnapshotAndSubscribe />
</>,
);
expect(c.textContent).toEqual('"DEFAULT""DEFAULT"');
act(() => setAtom('SET IN CURRENT'));
expect(c.textContent).toEqual('"SET IN CURRENT""SET IN CURRENT"');
act(resetAtom);
expect(c.textContent).toEqual('"DEFAULT""DEFAULT"');
expect(snapshots.length).toEqual(3);
expect(snapshots[0].getLoadable(myAtom).contents).toEqual('DEFAULT');
expect(snapshots[1].getLoadable(myAtom).contents).toEqual('SET IN CURRENT');
expect(snapshots[1].getLoadable(mySelector).contents).toEqual(
'SET IN CURRENT',
);
expect(snapshots[2].getLoadable(myAtom).contents).toEqual('DEFAULT');
});
testRecoil('useRecoilSnapshot - goto snapshots', ({strictMode}) => {
if (strictMode) {
return;
}
const atomA = atom({
key: 'useRecoilSnapshot - goto A',
default: 'DEFAULT',
});
const [ReadsAndWritesAtomA, setAtomA] =
componentThatReadsAndWritesAtom(atomA);
const atomB = atom<string | number>({
key: 'useRecoilSnapshot - goto B',
default: 'DEFAULT',
});
const [ReadsAndWritesAtomB, setAtomB] =
componentThatReadsAndWritesAtom(atomB);
const snapshots = [];
let gotoSnapshot;
function RecoilSnapshotAndSubscribe() {
gotoSnapshot = useGotoRecoilSnapshot();
const snapshot = useRecoilSnapshot();
snapshot.retain();
snapshots.push(snapshot);
return null;
}
const c = renderElements(
<>
<ReadsAndWritesAtomA />
<ReadsAndWritesAtomB />
<RecoilSnapshotAndSubscribe />
</>,
);
expect(c.textContent).toEqual('"DEFAULT""DEFAULT"');
// $FlowFixMe[incompatible-call]
act(() => setAtomA(1));
expect(c.textContent).toEqual('1"DEFAULT"');
act(() => setAtomB(2));
expect(c.textContent).toEqual('12');
expect(snapshots.length).toEqual(3);
act(() => gotoSnapshot(snapshots[1]));
expect(c.textContent).toEqual('1"DEFAULT"');
act(() => gotoSnapshot(snapshots[0]));
expect(c.textContent).toEqual('"DEFAULT""DEFAULT"');
act(() => gotoSnapshot(snapshots[2].map(({set}) => set(atomB, 3))));
expect(c.textContent).toEqual('13');
});
testRecoil(
'useRecoilSnapshot - async selector',
async ({strictMode, concurrentMode}) => {
const [mySelector, resolve] = asyncSelector<string, _>();
const snapshots = [];
function RecoilSnapshotAndSubscribe() {
const snapshot = useRecoilSnapshot();
snapshot.retain();
useEffect(() => {
snapshots.push(snapshot);
}, [snapshot]);
return null;
}
renderElements(<RecoilSnapshotAndSubscribe />);
expect(snapshots.length).toEqual(strictMode && concurrentMode ? 2 : 1);
act(() => resolve('RESOLVE'));
expect(snapshots.length).toEqual(strictMode && concurrentMode ? 2 : 1);
// On the first request the selector is unresolved and returns the promise
await expect(
snapshots[0].getLoadable(mySelector).contents,
).resolves.toEqual('RESOLVE');
// On the second request the resolved value is cached.
expect(snapshots[0].getLoadable(mySelector).contents).toEqual('RESOLVE');
},
);
testRecoil(
'useRecoilSnapshot - cloned async selector',
async ({strictMode, concurrentMode}) => {
const [mySelector, resolve] = asyncSelector<string, _>();
const snapshots = [];
function RecoilSnapshotAndSubscribe() {
const snapshot = useRecoilSnapshot();
snapshot.retain();
useEffect(() => {
snapshots.push(snapshot);
});
return null;
}
const c = renderElements(
<>
<React.Suspense fallback="loading">
<ReadsAtom atom={mySelector} />
</React.Suspense>
<RecoilSnapshotAndSubscribe />
</>,
);
expect(c.textContent).toEqual('loading');
expect(snapshots.length).toEqual(strictMode && concurrentMode ? 2 : 1);
act(() => resolve('RESOLVE'));
await flushPromisesAndTimers();
await flushPromisesAndTimers();
expect(c.textContent).toEqual('"RESOLVE"');
expect(snapshots.length).toEqual(strictMode && concurrentMode ? 3 : 2);
// Snapshot contains cached result since it was cloned after resolved
expect(snapshots[0].getLoadable(mySelector).contents).toEqual('RESOLVE');
},
);
testRecoil('Subscriptions', async () => {
const myAtom = atom<string>({
key: 'useRecoilSnapshot Subscriptions atom',
default: 'ATOM',
});
// $FlowFixMe[incompatible-call]
const selectorA = selector({
key: 'useRecoilSnapshot Subscriptions A',
get: ({get}) => get(myAtom),
});
const selectorB = selector({
key: 'useRecoilSnapshot Subscriptions B',
get: ({get}) => get(selectorA) + get(myAtom),
});
const selectorC = selector({
key: 'useRecoilSnapshot Subscriptions C',
get: async ({get}) => {
const ret = get(selectorA) + get(selectorB);
await Promise.resolve();
return ret;
},
});
let snapshot = freshSnapshot();
function RecoilSnapshot() {
snapshot = useRecoilSnapshot();
return null;
}
const c = renderElements(
<>
<ReadsAtom atom={selectorC} />
<RecoilSnapshot />
</>,
);
await flushPromisesAndTimers();
expect(c.textContent).toBe('"ATOMATOMATOM"');
expect(
Array.from(snapshot.getInfo_UNSTABLE(myAtom).subscribers.nodes).length,
).toBe(3);
expect(
Array.from(snapshot.getInfo_UNSTABLE(myAtom).subscribers.nodes),
).toEqual(expect.arrayContaining([selectorA, selectorB, selectorC]));
expect(
Array.from(snapshot.getInfo_UNSTABLE(selectorA).subscribers.nodes).length,
).toBe(2);
expect(
Array.from(snapshot.getInfo_UNSTABLE(selectorA).subscribers.nodes),
).toEqual(expect.arrayContaining([selectorB, selectorC]));
expect(
Array.from(snapshot.getInfo_UNSTABLE(selectorB).subscribers.nodes).length,
).toBe(1);
expect(
Array.from(snapshot.getInfo_UNSTABLE(selectorB).subscribers.nodes),
).toEqual(expect.arrayContaining([selectorC]));
expect(
Array.from(snapshot.getInfo_UNSTABLE(selectorC).subscribers.nodes).length,
).toBe(0);
expect(
Array.from(snapshot.getInfo_UNSTABLE(selectorC).subscribers.nodes),
).toEqual(expect.arrayContaining([]));
});
describe('Snapshot Retention', () => {
testRecoil('Retained for duration component is mounted', async () => {
let retainedDuringEffect = false;
let setMount;
let checkRetention;
function UseRecoilSnapshot() {
const snapshot = useRecoilSnapshot();
expect(snapshot.isRetained()).toBe(true);
useEffect(() => {
retainedDuringEffect = snapshot.isRetained();
});
checkRetention = () => snapshot.isRetained();
return null;
}
function Component() {
const [mount, setMountState] = useState(false);
setMount = setMountState;
return mount ? <UseRecoilSnapshot /> : null;
}
renderElements(<Component />);
expect(retainedDuringEffect).toBe(false);
act(() => setMount(true));
expect(retainedDuringEffect).toBe(true);
expect(checkRetention?.()).toBe(true);
act(() => setMount(false));
await flushPromisesAndTimers();
expect(checkRetention?.()).toBe(false);
});
testRecoil('Snapshot auto-release', async ({gks}) => {
let rootFirstCnt = 0;
const rootFirstAtom = atom({
key: 'useRecoilSnapshot auto-release root-first',
default: 'DEFAULT',
effects: [
({setSelf}) => {
rootFirstCnt++;
setSelf('ROOT');
return () => {
rootFirstCnt--;
};
},
],
});
let snapshotFirstCnt = 0;
const snapshotFirstAtom = atom({
key: 'useRecoilSnapshot auto-release snapshot-first',
default: 'DEFAULT',
effects: [
({setSelf}) => {
snapshotFirstCnt++;
setSelf('SNAPSHOT FIRST');
return () => {
snapshotFirstCnt--;
};
},
],
});
let snapshotOnlyCnt = 0;
const snapshotOnlyAtom = atom({
key: 'useRecoilSnapshot auto-release snapshot-only',
default: 'DEFAULT',
effects: [
({setSelf}) => {
snapshotOnlyCnt++;
setSelf('SNAPSHOT ONLY');
return () => {
snapshotOnlyCnt--;
};
},
],
});
let rootOnlyCnt = 0;
const rootOnlyAtom = atom({
key: 'useRecoilSnapshot auto-release root-only',
default: 'DEFAULT',
effects: [
({setSelf}) => {
rootOnlyCnt++;
setSelf('RETAIN');
return () => {
rootOnlyCnt--;
};
},
],
});
let setMount: boolean => void = () => {
throw new Error('Test Error');
};
function UseRecoilSnapshot() {
const snapshot = useRecoilSnapshot();
return (
snapshot.getLoadable(snapshotFirstAtom).getValue() +
snapshot.getLoadable(snapshotOnlyAtom).getValue()
);
}
function Component() {
const [mount, setState] = useState(false);
setMount = setState;
return mount ? (
<>
<ReadsAtom atom={rootOnlyAtom} />
<ReadsAtom atom={rootFirstAtom} />
<UseRecoilSnapshot />
<ReadsAtom atom={snapshotFirstAtom} />
</>
) : (
<ReadsAtom atom={rootOnlyAtom} />
);
}
const c = renderElements(<Component />);
expect(c.textContent).toBe('"RETAIN"');
expect(rootOnlyCnt).toBe(1);
expect(snapshotOnlyCnt).toBe(0);
expect(rootFirstCnt).toBe(0);
expect(snapshotFirstCnt).toBe(0);
act(() => setMount(true));
expect(c.textContent).toBe(
'"RETAIN""ROOT"SNAPSHOT FIRSTSNAPSHOT ONLY"SNAPSHOT FIRST"',
);
await flushPromisesAndTimers();
expect(rootOnlyCnt).toBe(1);
expect(snapshotOnlyCnt).toBe(1);
expect(rootFirstCnt).toBe(1);
expect(snapshotFirstCnt).toBe(2);
// Confirm snapshot isn't released until component is unmounted
await flushPromisesAndTimers();
expect(rootOnlyCnt).toBe(1);
expect(snapshotOnlyCnt).toBe(1);
expect(rootFirstCnt).toBe(1);
expect(snapshotFirstCnt).toBe(2);
// Auto-release snapshot
act(() => setMount(false));
await flushPromisesAndTimers();
expect(c.textContent).toBe('"RETAIN"');
expect(rootOnlyCnt).toBe(1);
expect(snapshotOnlyCnt).toBe(0);
if (gks.includes('recoil_memory_management_2020')) {
expect(rootFirstCnt).toBe(0);
expect(snapshotFirstCnt).toBe(0);
}
});
});
testRecoil('useRecoilSnapshot - re-render', () => {
const myAtom = stringAtom();
const [ReadsAndWritesAtom, setAtom, resetAtom] =
componentThatReadsAndWritesAtom(myAtom);
const snapshots = [];
let forceUpdate;
function RecoilSnapshotAndSubscribe() {
const [, setState] = useState(([]: Array<$FlowFixMe>));
forceUpdate = () => setState([]);
const snapshot = useRecoilSnapshot();
snapshots.push(snapshot);
return null;
}
const c = renderElements(
<>
<ReadsAndWritesAtom />
<RecoilSnapshotAndSubscribe />
</>,
);
expect(c.textContent).toEqual('"DEFAULT"');
expect(snapshots[snapshots.length - 1].getLoadable(myAtom).contents).toBe(
'DEFAULT',
);
act(forceUpdate);
expect(snapshots[snapshots.length - 1].getLoadable(myAtom).contents).toBe(
'DEFAULT',
);
act(() => setAtom('SET'));
expect(c.textContent).toEqual('"SET"');
expect(snapshots[snapshots.length - 1].getLoadable(myAtom).contents).toBe(
'SET',
);
act(forceUpdate);
expect(c.textContent).toEqual('"SET"');
expect(snapshots[snapshots.length - 1].getLoadable(myAtom).contents).toBe(
'SET',
);
act(resetAtom);
expect(c.textContent).toEqual('"DEFAULT"');
expect(snapshots[snapshots.length - 1].getLoadable(myAtom).contents).toBe(
'DEFAULT',
);
act(forceUpdate);
expect(c.textContent).toEqual('"DEFAULT"');
expect(snapshots[snapshots.length - 1].getLoadable(myAtom).contents).toBe(
'DEFAULT',
);
});

View File

@@ -0,0 +1,214 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
act,
atom,
atomFamily,
selector,
selectorFamily,
asyncSelector,
componentThatReadsAndWritesAtom,
renderElements;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({act} = require('ReactTestUtils'));
atom = require('../../recoil_values/Recoil_atom');
atomFamily = require('../../recoil_values/Recoil_atomFamily');
selector = require('../../recoil_values/Recoil_selector');
selectorFamily = require('../../recoil_values/Recoil_selectorFamily');
({
asyncSelector,
componentThatReadsAndWritesAtom,
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
});
testRecoil('useRecoilValueReset - value default', () => {
const myAtom = atom({
key: 'useResetRecoilState/atom',
default: 'default',
});
const [Component, setValue, resetValue] =
componentThatReadsAndWritesAtom(myAtom);
const container = renderElements(<Component />);
expect(container.textContent).toBe('"default"');
act(() => setValue('set value'));
expect(container.textContent).toBe('"set value"');
act(() => resetValue());
expect(container.textContent).toBe('"default"');
});
testRecoil('useResetRecoilState - sync selector default', () => {
const mySelector = selector({
key: 'useResetRecoilState/sync_selector/default',
get: () => 'fallback',
});
const myAtom = atom({
key: 'useResetRecoilState/sync_selector',
default: mySelector,
});
const [Component, setValue, resetValue] =
componentThatReadsAndWritesAtom(myAtom);
const container = renderElements(<Component />);
expect(container.textContent).toBe('"fallback"');
act(() => setValue('set value'));
expect(container.textContent).toBe('"set value"');
act(() => resetValue());
expect(container.textContent).toBe('"fallback"');
});
// Test resetting an atom to a fallback selector with a pending async value
testRecoil('useResetRecoilState - async selector default', () => {
const [mySelector, resolve] = asyncSelector<string, _>();
const myAtom = atom({
key: 'useResetRecoilState/async_selector',
default: mySelector,
});
const [Component, setValue, resetValue] =
componentThatReadsAndWritesAtom(myAtom);
const container = renderElements(<Component />);
expect(container.textContent).toBe('loading');
act(() => setValue('set value'));
act(() => jest.runAllTimers()); // Hmm, interesting this is required
expect(container.textContent).toBe('"set value"');
act(() => resetValue());
expect(container.textContent).toBe('loading');
act(() => resolve('resolved fallback'));
act(() => jest.runAllTimers());
expect(container.textContent).toBe('"resolved fallback"');
});
// Test resetting an atom to a fallback selector with a pending async value
testRecoil('useResetRecoilState - scoped atom', () => {
return; // @oss-only
const myAtom = atom({
key: 'useResetRecoilState/scoped_atom',
default: 'default',
scopeRules_APPEND_ONLY_READ_THE_DOCS: [
[atom({key: 'useResetRecoilState/scoped_atom/scope_rule', default: 0})],
],
});
const [Component, setValue, resetValue] =
componentThatReadsAndWritesAtom(myAtom);
const container = renderElements(<Component />);
expect(container.textContent).toBe('"default"');
act(() => setValue('set value'));
expect(container.textContent).toBe('"set value"');
act(() => resetValue());
expect(container.textContent).toBe('"default"');
// TODO test resetting a scoped atom that was upgraded with a new rule
});
// Test resetting an atom to a fallback selector with a pending async value
testRecoil('useResetRecoilState - atom family', () => {
const myAtom = atomFamily<
_,
{default: string} | {default: string, secondParam: string},
>({
key: 'useResetRecoilState/atomFamily',
// $FlowFixMe[missing-local-annot]
default: ({default: def}) => def,
});
const [Component, setValue, resetValue] = componentThatReadsAndWritesAtom(
myAtom({default: 'default'}),
);
const [ComponentB, setValueB, resetValueB] = componentThatReadsAndWritesAtom(
myAtom({default: 'default', secondParam: 'superset'}),
);
const container = renderElements(
<>
<Component />
<ComponentB />
</>,
);
expect(container.textContent).toBe('"default""default"');
// $FlowFixMe[incompatible-call]
act(() => setValue('set value'));
expect(container.textContent).toBe('"set value""default"');
act(() => resetValue());
expect(container.textContent).toBe('"default""default"');
// $FlowFixMe[incompatible-call]
act(() => setValue('set value A'));
expect(container.textContent).toBe('"set value A""default"');
// $FlowFixMe[incompatible-call]
act(() => setValueB('set value B'));
expect(container.textContent).toBe('"set value A""set value B"');
act(() => resetValueB());
expect(container.textContent).toBe('"set value A""default"');
});
testRecoil('useResetRecoilState - selector', () => {
const myAtom = atom({
key: 'useResetRecoilState/selector/atom',
default: 'default',
});
// $FlowFixMe[incompatible-call]
const mySelector = selector({
key: 'useResetRecoilState/selector',
get: ({get}) => get(myAtom),
// $FlowFixMe[incompatible-call]
set: ({set}, value) => set(myAtom, value),
});
const [Component, setValue, resetValue] =
componentThatReadsAndWritesAtom(mySelector);
const container = renderElements(<Component />);
expect(container.textContent).toBe('"default"');
act(() => setValue('set value'));
expect(container.textContent).toBe('"set value"');
act(() => resetValue());
expect(container.textContent).toBe('"default"');
});
testRecoil('useResetRecoilState - parameterized selector', () => {
const myAtom = atom({
key: 'useResetRecoilState/parameterized_selector/atom',
default: 'default',
});
const mySelector = selectorFamily<_, string>({
key: 'useResetRecoilState/parameterized_selector',
get:
() =>
// $FlowFixMe[missing-local-annot]
({get}) =>
get(myAtom),
set:
() =>
// $FlowFixMe[missing-local-annot]
({set}, value) =>
set(myAtom, value),
});
const [Component, setValue, resetValue] = componentThatReadsAndWritesAtom(
mySelector('parameter'),
);
const container = renderElements(<Component />);
expect(container.textContent).toBe('"default"');
act(() => setValue('set value'));
expect(container.textContent).toBe('"set value"');
act(() => resetValue());
expect(container.textContent).toBe('"default"');
});

View File

@@ -0,0 +1,367 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {act} = require('ReactTestUtils');
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
useState,
useEffect,
atom,
useRecoilValue,
useRecoilState,
useRecoilTransaction,
useRecoilSnapshot,
renderElements,
flushPromisesAndTimers,
ReadsAtom;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useState, useEffect} = React);
({
atom,
useRecoilValue,
useRecoilState,
useRecoilTransaction_UNSTABLE: useRecoilTransaction,
useRecoilSnapshot,
} = require('../../Recoil_index'));
({
renderElements,
flushPromisesAndTimers,
ReadsAtom,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
});
describe('Atoms', () => {
testRecoil('Get with transaction', () => {
const myAtom = atom({
key: 'useRecoilTransaction atom get',
default: 'DEFAULT',
});
let readAtom;
let ranTransaction = false;
function Component() {
readAtom = useRecoilTransaction(({get}) => () => {
expect(get(myAtom)).toEqual('DEFAULT');
ranTransaction = true;
});
return null;
}
renderElements(<Component />);
expect(ranTransaction).toBe(false);
act(readAtom);
expect(ranTransaction).toBe(true);
});
testRecoil('Set with transaction', () => {
const myAtom = atom<string>({
key: 'useRecoilTransaction atom set',
default: 'DEFAULT',
});
function Component() {
// $FlowFixMe[missing-local-annot]
const transact = useRecoilTransaction(({set, get}) => value => {
set(myAtom, 'TMP');
expect(get(myAtom)).toEqual('TMP');
set(myAtom, old => {
expect(old).toEqual('TMP');
return value;
});
expect(get(myAtom)).toEqual(value);
});
useEffect(() => {
transact('TRANSACT');
});
return null;
}
const c = renderElements(
<>
<ReadsAtom atom={myAtom} />
<Component />
</>,
);
expect(c.textContent).toEqual('"TRANSACT"');
});
testRecoil('Dirty atoms', async () => {
const beforeAtom = atom({
key: 'useRecoilTransaction dirty before',
default: 'DEFAULT',
});
const duringAtomA = atom({
key: 'useRecoilTransaction dirty during A',
default: 'DEFAULT',
});
const duringAtomB = atom({
key: 'useRecoilTransaction dirty during B',
default: 'DEFAULT',
});
const afterAtom = atom({
key: 'useRecoilTransaction dirty after',
default: 'DEFAULT',
});
let snapshot;
let firstEffect = true;
function Component() {
const [beforeValue, setBefore] = useState('INITIAL');
const [beforeAtomValue, setBeforeAtom] = useRecoilState(beforeAtom);
const duringAValue = useRecoilValue(duringAtomA);
const duringBValue = useRecoilValue(duringAtomB);
const [afterAtomValue, setAfterAtom] = useRecoilState(afterAtom);
const [afterValue, setAfter] = useState('INITIAL');
const transaction = useRecoilTransaction(({set, get}) => () => {
expect(get(beforeAtom)).toEqual('BEFORE');
expect(get(duringAtomA)).toEqual('DEFAULT');
expect(get(duringAtomB)).toEqual('DEFAULT');
expect(get(afterAtom)).toEqual('DEFAULT');
set(duringAtomA, 'DURING_A');
set(duringAtomB, 'DURING_B');
});
snapshot = useRecoilSnapshot();
useEffect(() => {
setTimeout(() => {
act(() => {
if (firstEffect) {
setBefore('BEFORE');
setBeforeAtom('BEFORE');
transaction();
setAfterAtom('AFTER');
setAfter('AFTER');
}
firstEffect = false;
});
}, 0);
});
return [
beforeValue,
beforeAtomValue,
duringAValue,
duringBValue,
afterAtomValue,
afterValue,
].join(',');
}
const c = renderElements(<Component />);
expect(c.textContent).toBe(
'INITIAL,DEFAULT,DEFAULT,DEFAULT,DEFAULT,INITIAL',
);
expect(
Array.from(snapshot?.getNodes_UNSTABLE({isModified: true}) ?? []),
).toEqual([]);
await flushPromisesAndTimers();
expect(c.textContent).toBe('BEFORE,BEFORE,DURING_A,DURING_B,AFTER,AFTER');
expect(
Array.from(snapshot?.getNodes_UNSTABLE({isModified: true}) ?? []).map(
({key}) => key,
),
).toEqual([
'useRecoilTransaction dirty before',
'useRecoilTransaction dirty during A',
'useRecoilTransaction dirty during B',
'useRecoilTransaction dirty after',
]);
});
});
describe('Atom Effects', () => {
testRecoil(
'Atom effects are run when first get from a transaction',
async () => {
let numTimesEffectInit = 0;
const atomWithEffect = atom({
key: 'atom effect first get transaction',
default: 'DEFAULT',
effects: [
({trigger}) => {
expect(trigger).toEqual('get');
numTimesEffectInit++;
},
],
});
let getAtomWithTransaction;
let ranTransaction = false;
const Component = () => {
getAtomWithTransaction = useRecoilTransaction(({get}) => () => {
expect(get(atomWithEffect)).toEqual('DEFAULT');
ranTransaction = true;
});
return null;
};
renderElements(<Component />);
act(() => getAtomWithTransaction());
expect(ranTransaction).toBe(true);
expect(numTimesEffectInit).toBe(1);
},
);
testRecoil(
'Atom effects are run when first set with a transaction',
async ({strictMode, concurrentMode}) => {
let numTimesEffectInit = 0;
const atomWithEffect = atom({
key: 'atom effect first set transaction',
default: 'DEFAULT',
effects: [
({trigger}) => {
expect(trigger).toEqual('set');
numTimesEffectInit++;
},
],
});
let setAtomWithTransaction;
const Component = () => {
setAtomWithTransaction = useRecoilTransaction(({set}) => () => {
set(atomWithEffect, 'SET');
});
useEffect(() => {
act(setAtomWithTransaction);
});
return null;
};
renderElements(<Component />);
expect(numTimesEffectInit).toBe(strictMode && concurrentMode ? 2 : 1);
},
);
testRecoil('Atom effects can initialize for a transaction', async () => {
let numTimesEffectInit = 0;
const atomWithEffect = atom({
key: 'atom effect init transaction',
default: 'DEFAULT',
effects: [
({setSelf}) => {
setSelf('INIT');
numTimesEffectInit++;
},
],
});
let initAtomWithTransaction;
let ranTransaction = false;
const Component = () => {
initAtomWithTransaction = useRecoilTransaction(({get}) => () => {
expect(get(atomWithEffect)).toEqual('INIT');
ranTransaction = true;
});
return null;
};
renderElements(<Component />);
act(() => initAtomWithTransaction());
expect(ranTransaction).toBe(true);
expect(numTimesEffectInit).toBe(1);
});
testRecoil(
'Atom effects are initialized once if first seen on transaction and then on root store',
({strictMode, concurrentMode}) => {
const sm = strictMode && concurrentMode ? 2 : 1;
let numTimesEffectInit = 0;
const atomWithEffect = atom({
key: 'useRecoilTransaction effect first get transaction',
default: 0,
effects: [
() => {
numTimesEffectInit++;
},
],
});
const Component = () => {
const readAtomFromSnapshot = useRecoilTransaction(({get}) => () => {
get(atomWithEffect);
});
readAtomFromSnapshot(); // first initialization
expect(numTimesEffectInit).toBeGreaterThanOrEqual(1);
const effectsRan = numTimesEffectInit;
/**
* Transactions do not use a snapshot under the hood, so any initialized
* effects from a transaction will be reflected in root store
*/
useRecoilValue(atomWithEffect);
expect(numTimesEffectInit).toBe(effectsRan);
return 'RENDERED';
};
const c = renderElements(<Component />);
expect(c.textContent).toBe('RENDERED');
expect(numTimesEffectInit).toBe(1 * sm);
},
);
testRecoil(
'Atom effects are initialized once if first seen on root store and then on snapshot',
({strictMode, concurrentMode}) => {
const sm = strictMode && concurrentMode ? 2 : 1;
let numTimesEffectInit = 0;
const atomWithEffect = atom({
key: 'atom effect first get root',
default: 0,
effects: [
() => {
numTimesEffectInit++;
},
],
});
const Component = () => {
const readAtomFromSnapshot = useRecoilTransaction(({get}) => () => {
get(atomWithEffect);
});
useRecoilValue(atomWithEffect); // first initialization
expect(numTimesEffectInit).toBeGreaterThanOrEqual(1);
const effectsRan = numTimesEffectInit;
/**
* Transactions do not use a snapshot under the hood, so any initialized
* effects from a transaction will be reflected in root store
*/
readAtomFromSnapshot();
expect(numTimesEffectInit).toBe(effectsRan);
return 'RENDERED';
};
const c = renderElements(<Component />);
expect(c.textContent).toBe('RENDERED');
expect(numTimesEffectInit).toBe(1 * sm);
},
);
});

View File

@@ -0,0 +1,302 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall perf_viz
*/
'use strict';
import type {Snapshot} from 'Recoil_Snapshot';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
act,
freshSnapshot,
atom,
atomFamily,
selector,
ReadsAtom,
asyncSelector,
componentThatReadsAndWritesAtom,
renderElements,
useRecoilTransactionObserver;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({act} = require('ReactTestUtils'));
({freshSnapshot} = require('../../core/Recoil_Snapshot'));
atom = require('../../recoil_values/Recoil_atom');
atomFamily = require('../../recoil_values/Recoil_atomFamily');
selector = require('../../recoil_values/Recoil_selector');
({
ReadsAtom,
asyncSelector,
componentThatReadsAndWritesAtom,
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({useRecoilTransactionObserver} = require('../Recoil_SnapshotHooks'));
});
function TransactionObserver({
callback,
}: $TEMPORARY$object<{
callback: ({previousSnapshot: Snapshot, snapshot: Snapshot}) => void,
}>) {
useRecoilTransactionObserver(callback);
return null;
}
// Run test first since it deals with all registered atoms
testRecoil('getNodes', () => {
let snapshot = freshSnapshot();
function UseRecoilTransactionObserver() {
useRecoilTransactionObserver(p => {
p.snapshot.retain();
snapshot = p.snapshot;
});
return null;
}
const atoms = atomFamily<string, string>({
key: 'useRecoilTransactionObserver getNodes atom',
default: x => x,
});
const [ReadsAtomA, setAtomA, resetAtomA] = componentThatReadsAndWritesAtom(
atoms('A'),
);
const [ReadsAtomB, setAtomB] = componentThatReadsAndWritesAtom(atoms('B'));
// $FlowFixMe[incompatible-call]
const selectorA = selector({
key: 'useRecoilTransactionObserver getNodes selector',
get: ({get}) => get(atoms('A')) + '-SELECTOR',
});
const c = renderElements(
<>
<ReadsAtomA />
<ReadsAtomB />
<ReadsAtom atom={selectorA} />
<UseRecoilTransactionObserver />
</>,
);
expect(c.textContent).toEqual('"A""B""A-SELECTOR"');
expect(
Array.from(snapshot.getNodes_UNSTABLE({isInitialized: true})).length,
).toEqual(0);
act(() => setAtomA('A'));
// >= 3 because we expect at least nodes for atom's A and B from
// the family and selectorA. In reality we could get more due to internal
// helper selectors and default fallback atoms.
expect(
Array.from(snapshot.getNodes_UNSTABLE({isInitialized: true})).length,
).toBeGreaterThanOrEqual(3);
const nodes = Array.from(snapshot.getNodes_UNSTABLE({isInitialized: true}));
expect(nodes).toEqual(
expect.arrayContaining([atoms('A'), atoms('B'), selectorA]),
);
// Test atom A is set
const aDirty = Array.from(snapshot.getNodes_UNSTABLE({isModified: true}));
expect(aDirty.length).toEqual(1);
expect(snapshot.getLoadable(aDirty[0]).contents).toEqual('A');
// Test atom B is set
act(() => setAtomB('B'));
const bDirty = Array.from(snapshot.getNodes_UNSTABLE({isModified: true}));
expect(bDirty.length).toEqual(1);
expect(snapshot.getLoadable(bDirty[0]).contents).toEqual('B');
// Test atoms
const atomNodes = Array.from(
snapshot.getNodes_UNSTABLE({isInitialized: true}),
);
expect(atomNodes.map(atom => snapshot.getLoadable(atom).contents)).toEqual(
expect.arrayContaining(['A', 'B']),
);
// Test selector
const selectorNodes = Array.from(
snapshot.getNodes_UNSTABLE({isInitialized: true}),
);
expect(
selectorNodes.map(atom => snapshot.getLoadable(atom).contents),
).toEqual(expect.arrayContaining(['A-SELECTOR']));
// Test Reset
act(resetAtomA);
const resetDirty = Array.from(snapshot.getNodes_UNSTABLE({isModified: true}));
expect(resetDirty.length).toEqual(1);
expect(resetDirty[0]).toBe(aDirty[0]);
// TODO Test dirty selectors
});
testRecoil('Can observe atom value', async () => {
const atomA = atom({
key: 'Observe Atom A',
default: 'DEFAULT A',
});
const atomB = atom({
key: 'Observe Atom B',
default: 'DEFAULT B',
});
const [WriteAtomA, setAtomA, resetA] = componentThatReadsAndWritesAtom(atomA);
const [WriteAtomB, setAtomB] = componentThatReadsAndWritesAtom(atomB);
const transactions = [];
renderElements(
<>
<TransactionObserver
callback={({snapshot, previousSnapshot}) => {
snapshot.retain();
previousSnapshot.retain();
transactions.push({snapshot, previousSnapshot});
}}
/>
<WriteAtomA />
<WriteAtomB />
</>,
);
act(() => setAtomB('SET B'));
expect(transactions.length).toEqual(1);
await expect(transactions[0].snapshot.getPromise(atomA)).resolves.toEqual(
'DEFAULT A',
);
await expect(
transactions[0].previousSnapshot.getPromise(atomA),
).resolves.toEqual('DEFAULT A');
await expect(transactions[0].snapshot.getPromise(atomB)).resolves.toEqual(
'SET B',
);
await expect(
transactions[0].previousSnapshot.getPromise(atomB),
).resolves.toEqual('DEFAULT B');
act(() => setAtomA('SET A'));
expect(transactions.length).toEqual(2);
await expect(transactions[1].snapshot.getPromise(atomA)).resolves.toEqual(
'SET A',
);
await expect(
transactions[1].previousSnapshot.getPromise(atomA),
).resolves.toEqual('DEFAULT A');
await expect(transactions[1].snapshot.getPromise(atomB)).resolves.toEqual(
'SET B',
);
await expect(
transactions[1].previousSnapshot.getPromise(atomB),
).resolves.toEqual('SET B');
act(() => resetA());
expect(transactions.length).toEqual(3);
await expect(transactions[2].snapshot.getPromise(atomA)).resolves.toEqual(
'DEFAULT A',
);
await expect(
transactions[2].previousSnapshot.getPromise(atomA),
).resolves.toEqual('SET A');
await expect(transactions[2].snapshot.getPromise(atomB)).resolves.toEqual(
'SET B',
);
await expect(
transactions[2].previousSnapshot.getPromise(atomB),
).resolves.toEqual('SET B');
});
testRecoil('Can observe selector value', async () => {
const atomA = atom({
key: 'Observe Atom for Selector',
default: 'DEFAULT',
});
// $FlowFixMe[incompatible-call]
const selectorA = selector({
key: 'Observer Selector As',
get: ({get}) => `SELECTOR ${get(atomA)}`,
});
const [WriteAtom, setAtom] = componentThatReadsAndWritesAtom(atomA);
const transactions = [];
renderElements(
<>
<TransactionObserver
callback={({snapshot, previousSnapshot}) => {
snapshot.retain();
previousSnapshot.retain();
transactions.push({snapshot, previousSnapshot});
}}
/>
<WriteAtom />
</>,
);
act(() => setAtom('SET'));
expect(transactions.length).toEqual(1);
await expect(transactions[0].snapshot.getPromise(atomA)).resolves.toEqual(
'SET',
);
await expect(
transactions[0].previousSnapshot.getPromise(atomA),
).resolves.toEqual('DEFAULT');
await expect(transactions[0].snapshot.getPromise(selectorA)).resolves.toEqual(
'SELECTOR SET',
);
await expect(
transactions[0].previousSnapshot.getPromise(selectorA),
).resolves.toEqual('SELECTOR DEFAULT');
});
testRecoil('Can observe async selector value', async () => {
const atomA = atom({
key: 'Observe Atom for Async Selector',
default: 'DEFAULT',
});
const [WriteAtom, setAtom] = componentThatReadsAndWritesAtom(atomA);
const [selectorA, resolveA] = asyncSelector<string, _>();
const transactions = [];
renderElements(
<>
<TransactionObserver
callback={({snapshot, previousSnapshot}) => {
snapshot.retain();
previousSnapshot.retain();
transactions.push({snapshot, previousSnapshot});
}}
/>
<WriteAtom />
</>,
);
act(() => setAtom('SET'));
expect(transactions.length).toEqual(1);
expect(transactions[0].snapshot.getLoadable(selectorA).state).toEqual(
'loading',
);
act(() => resolveA('RESOLVE'));
expect(transactions.length).toEqual(1);
await expect(transactions[0].snapshot.getPromise(selectorA)).resolves.toEqual(
'RESOLVE',
);
});

View File

@@ -0,0 +1,215 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
act,
selector,
constSelector,
errorSelector,
asyncSelector,
renderElements,
useRecoilValueLoadable,
flushPromisesAndTimers;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({act} = require('ReactTestUtils'));
constSelector = require('../../recoil_values/Recoil_constSelector');
errorSelector = require('../../recoil_values/Recoil_errorSelector');
selector = require('../../recoil_values/Recoil_selector');
({
asyncSelector,
renderElements,
flushPromisesAndTimers,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({useRecoilValueLoadable} = require('../Recoil_Hooks'));
});
// These tests should cover the Loadable interface returned by useRecoilValueLoadable.
// It is also used by useRecoilStateNoThrow, waitForNone, and waitForAny
testRecoil('useRecoilValueLoadable - loadable with value', async () => {
const valueSel = constSelector('VALUE');
let promise;
function ReadLoadable() {
const loadable = useRecoilValueLoadable(valueSel);
expect(loadable.state).toBe('hasValue');
expect(loadable.contents).toBe('VALUE');
expect(loadable.getValue()).toBe('VALUE');
// eslint-disable-next-line jest/valid-expect
promise = expect(loadable.toPromise()).resolves.toBe('VALUE');
expect(loadable.valueMaybe()).toBe('VALUE');
expect(loadable.valueOrThrow()).toBe('VALUE');
expect(loadable.errorMaybe()).toBe(undefined);
expect(() => loadable.errorOrThrow()).toThrow(Error);
expect(loadable.promiseMaybe()).toBe(undefined);
expect(() => loadable.promiseOrThrow()).toThrow(Error);
return loadable.valueOrThrow();
}
const c = renderElements(<ReadLoadable />);
expect(c.textContent).toEqual('VALUE');
await promise;
});
testRecoil('useRecoilValueLoadable - loadable with error', async () => {
const valueSel = errorSelector<$FlowFixMe>('ERROR');
let promise;
function ReadLoadable() {
const loadable = useRecoilValueLoadable(valueSel);
expect(loadable.state).toBe('hasError');
expect(loadable.contents).toBeInstanceOf(Error);
expect(() => loadable.getValue()).toThrow('ERROR');
// eslint-disable-next-line jest/valid-expect
promise = expect(loadable.toPromise()).rejects.toBeInstanceOf(Error);
expect(loadable.valueMaybe()).toBe(undefined);
expect(() => loadable.valueOrThrow()).toThrow(Error);
expect(String(loadable.errorMaybe() ?? {})).toContain('ERROR');
expect(loadable.errorOrThrow()).toBeInstanceOf(Error);
expect(String(loadable.errorOrThrow())).toContain('ERROR');
expect(loadable.promiseMaybe()).toBe(undefined);
expect(() => loadable.promiseOrThrow()).toThrow(Error);
return 'VALUE';
}
const c = renderElements(<ReadLoadable />);
expect(c.textContent).toEqual('VALUE');
await promise;
});
testRecoil('useRecoilValueLoadable - loading loadable', async () => {
const [valueSel, resolve] = asyncSelector<string, _>();
let resolved = false;
const promises = [];
function ReadLoadable() {
const loadable = useRecoilValueLoadable(valueSel);
if (!resolved) {
expect(loadable.state).toBe('loading');
expect(loadable.contents).toBeInstanceOf(Promise);
expect(() => loadable.getValue()).toThrow();
try {
loadable.getValue();
} catch (promise) {
promises.push(promise);
}
promises.push(loadable.toPromise());
expect(loadable.valueMaybe()).toBe(undefined);
expect(() => loadable.valueOrThrow()).toThrow(Error);
expect(loadable.errorMaybe()).toBe(undefined);
expect(() => loadable.errorOrThrow()).toThrow(Error);
expect(loadable.promiseMaybe()).toBeInstanceOf(Promise);
promises.push(loadable.promiseMaybe());
return 'LOADING';
} else {
expect(loadable.state).toBe('hasValue');
expect(loadable.contents).toBe('VALUE');
expect(loadable.getValue()).toBe('VALUE');
promises.push(loadable.toPromise());
expect(loadable.valueMaybe()).toBe('VALUE');
expect(loadable.valueOrThrow()).toBe('VALUE');
expect(loadable.errorMaybe()).toBe(undefined);
expect(() => loadable.errorOrThrow()).toThrow(Error);
expect(loadable.promiseMaybe()).toBe(undefined);
expect(() => loadable.promiseOrThrow()).toThrow(Error);
return loadable.valueOrThrow();
}
}
const c = renderElements(<ReadLoadable />);
expect(c.textContent).toEqual('LOADING');
resolve('VALUE');
resolved = true;
act(() => jest.runAllTimers());
expect(c.textContent).toEqual('VALUE');
await Promise.all(
promises.map(async promise => {
if (!(promise instanceof Promise)) {
// for flow
throw new Error('Expected a promise');
}
const res = await promise;
const val = typeof res === 'string' ? res : res.__value;
expect(val).toBe('VALUE');
}),
);
});
testRecoil(
'useRecoilValueLoadable() with an async throwing selector results in a loadable in error state',
async () => {
const asyncError = selector<mixed>({
key: 'asyncError',
get: async () => {
throw new Error('Test Error');
},
});
const Test = () => {
const loadable = useRecoilValueLoadable(asyncError);
return (
<h1>{loadable?.state === 'hasError' ? 'Has error' : 'No error'}</h1>
);
};
const c = renderElements(<Test />);
await act(() => flushPromisesAndTimers());
expect(c.textContent).toEqual('Has error');
},
);
// Test that an async selector can depend on an async selector dependency
// and include async post-processing.
testRecoil('two level async', async () => {
const level2 = selector({
key: 'useRecoilValueLoadable async level2',
// $FlowFixMe[incompatible-call]
get: () => new Promise(resolve => setTimeout(() => resolve('level2'))),
});
// $FlowFixMe[incompatible-call]
const level1 = selector({
key: 'useRecoilValueLoadable async level1',
get: async ({get}) => {
const level2Value = get(level2);
return await new Promise(resolve =>
// $FlowFixMe[incompatible-call]
// $FlowFixMe[incompatible-type]
setTimeout(() => resolve(`level1 + ${level2Value}`)),
);
},
});
const promises = [];
function ReadPromise() {
const loadable = useRecoilValueLoadable(level1);
promises.push(loadable.toPromise());
return loadable.getValue();
}
const c = renderElements(<ReadPromise />);
expect(c.textContent).toEqual('loading');
await flushPromisesAndTimers();
await flushPromisesAndTimers();
await flushPromisesAndTimers();
await flushPromisesAndTimers();
expect(c.textContent).toEqual('level1 + level2');
await Promise.all(
promises.map(promise => expect(promise).resolves.toBe('level1 + level2')),
);
});

View File

@@ -0,0 +1,270 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {
RecoilState,
RecoilValue,
RecoilValueReadOnly,
} from '../../core/Recoil_RecoilValue';
import type {PersistenceSettings} from '../../recoil_values/Recoil_atom';
import type {NodeKey} from 'Recoil_Keys';
import type {Node} from 'react';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
useState,
act,
atom,
selector,
ReadsAtom,
flushPromisesAndTimers,
renderElements,
renderUnwrappedElements,
useRecoilState,
useRecoilValue,
useSetRecoilState,
useSetUnvalidatedAtomValues,
useTransactionObservation_DEPRECATED;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useState} = require('react'));
({act} = require('ReactTestUtils'));
atom = require('../../recoil_values/Recoil_atom');
selector = require('../../recoil_values/Recoil_selector');
({
ReadsAtom,
flushPromisesAndTimers,
renderElements,
renderUnwrappedElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({
useRecoilState,
useRecoilValue,
useSetRecoilState,
useSetUnvalidatedAtomValues,
} = require('../Recoil_Hooks'));
({useTransactionObservation_DEPRECATED} = require('../Recoil_SnapshotHooks'));
});
let nextID = 0;
function counterAtom(persistence?: PersistenceSettings<number>) {
return atom({
key: `atom${nextID++}`,
default: 0,
persistence_UNSTABLE: persistence,
});
}
function plusOneSelector(dep: RecoilValue<number>) {
const fn = jest.fn(x => x + 1);
const sel = selector({
key: `selector${nextID++}`,
get: ({get}) => fn(get(dep)),
});
return [sel, fn];
}
function plusOneAsyncSelector(
dep: RecoilValue<number>,
): [RecoilValueReadOnly<number>, (number) => void] {
let nextTimeoutAmount = 100;
const fn = jest.fn(x => {
return new Promise(resolve => {
setTimeout(() => {
resolve(x + 1);
}, nextTimeoutAmount);
});
});
const sel = selector({
key: `selector${nextID++}`,
get: ({get}) => fn(get(dep)),
});
return [
// $FlowFixMe[incompatible-return]
sel,
x => {
nextTimeoutAmount = x;
},
];
}
function componentThatWritesAtom<T>(
recoilState: RecoilState<T>,
// flowlint-next-line unclear-type:off
): [any, ((T => T) | T) => void] {
let updateValue;
const Component = jest.fn(() => {
updateValue = useSetRecoilState(recoilState);
return null;
});
// flowlint-next-line unclear-type:off
return [(Component: any), x => updateValue(x)];
}
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
function ObservesTransactions({fn}) {
useTransactionObservation_DEPRECATED(fn);
return null;
}
testRecoil(
'useTransactionObservation_DEPRECATED: Transaction dirty atoms are set',
async () => {
const anAtom = counterAtom({
type: 'url',
validator: x => (x: any), // flowlint-line unclear-type:off
});
const [aSelector, _] = plusOneSelector(anAtom);
// $FlowFixMe[incompatible-call]
const [anAsyncSelector, __] = plusOneAsyncSelector(aSelector);
const [Component, updateValue] = componentThatWritesAtom(anAtom);
const modifiedAtomsList = [];
renderElements(
<>
<Component />
<ReadsAtom atom={aSelector} />
<React.Suspense fallback="loading">
<ReadsAtom atom={anAsyncSelector} />
</React.Suspense>
<ObservesTransactions
fn={({modifiedAtoms}) => {
modifiedAtomsList.push(modifiedAtoms);
}}
/>
</>,
);
await flushPromisesAndTimers();
await flushPromisesAndTimers();
act(() => updateValue(1));
await flushPromisesAndTimers();
expect(modifiedAtomsList.length).toBe(3);
expect(modifiedAtomsList[1].size).toBe(1);
expect(modifiedAtomsList[1].has(anAtom.key)).toBe(true);
for (const modifiedAtoms of modifiedAtomsList) {
expect(modifiedAtoms.has(aSelector.key)).toBe(false);
expect(modifiedAtoms.has(anAsyncSelector.key)).toBe(false);
}
},
);
testRecoil(
'Can restore persisted values before atom def code is loaded',
() => {
let theAtom = null;
let setUnvalidatedAtomValues;
function SetsUnvalidatedAtomValues() {
setUnvalidatedAtomValues = useSetUnvalidatedAtomValues();
return null;
}
let setVisible;
function Switch({children}: $TEMPORARY$object<{children: Node}>) {
const [visible, mySetVisible] = useState(false);
setVisible = mySetVisible;
return visible ? children : null;
}
function MyReadsAtom({
getAtom,
}: $TEMPORARY$object<{getAtom: () => null | RecoilState<number>}>) {
const [value] = useRecoilState((getAtom(): any)); // flowlint-line unclear-type:off
return value;
}
const container = renderElements(
<>
<SetsUnvalidatedAtomValues />
<Switch>
<MyReadsAtom getAtom={() => theAtom} />
</Switch>
</>,
);
act(() => {
setUnvalidatedAtomValues(
new Map<NodeKey, mixed>().set('notDefinedYetAtom', 123),
);
});
const validator = jest.fn(() => 789);
// $FlowFixMe[incompatible-call]
theAtom = atom({
key: 'notDefinedYetAtom',
default: 456,
persistence_UNSTABLE: {
type: 'url',
validator,
},
});
act(() => {
setVisible(true);
});
// $FlowFixMe[invalid-tuple-index]
expect(validator.mock.calls[0][0]).toBe(123);
expect(container.textContent).toBe('789');
},
);
testRecoil(
'useTransactionObservation_DEPRECATED: Nonvalidated atoms are included in transaction observation',
() => {
const anAtom = counterAtom({
type: 'url',
validator: x => (x: any), // flowlint-line unclear-type:off
});
const [Component, updateValue] = componentThatWritesAtom(anAtom);
let setUnvalidatedAtomValues;
function SetsUnvalidatedAtomValues() {
setUnvalidatedAtomValues = useSetUnvalidatedAtomValues();
return null;
}
let values: Map<NodeKey, mixed> = new Map();
renderElements(
<>
<Component />
<SetsUnvalidatedAtomValues />
<ObservesTransactions
fn={({atomValues}) => {
values = atomValues;
}}
/>
</>,
);
act(() => {
setUnvalidatedAtomValues(
new Map<NodeKey, mixed>().set('someNonvalidatedAtom', 123),
);
});
values = new Map();
act(() => updateValue(1));
expect(values.size).toBe(2);
expect(values.get('someNonvalidatedAtom')).toBe(123);
},
);
testRecoil('Hooks cannot be used outside of RecoilRoot', () => {
const myAtom = atom({key: 'hook outside RecoilRoot', default: 'INVALID'});
function Test() {
useRecoilValue(myAtom);
return 'TEST';
}
// Make sure there is a friendly error message mentioning <RecoilRoot>
expect(() => renderUnwrappedElements(<Test />)).toThrow('<RecoilRoot>');
});

View File

@@ -0,0 +1,407 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
useState,
useTransition,
act,
useRecoilValue,
useRecoilState,
useRecoilValue_TRANSITION_SUPPORT_UNSTABLE,
useRecoilState_TRANSITION_SUPPORT_UNSTABLE,
atom,
selectorFamily,
renderElements,
reactMode,
flushPromisesAndTimers;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useState, useTransition} = React);
({act} = require('ReactTestUtils'));
({
useRecoilValue,
useRecoilState,
useRecoilValue_TRANSITION_SUPPORT_UNSTABLE,
useRecoilState_TRANSITION_SUPPORT_UNSTABLE,
atom,
selectorFamily,
} = require('../../Recoil_index'));
({
renderElements,
flushPromisesAndTimers,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({reactMode} = require('../../core/Recoil_ReactMode'));
});
let nextID = 0;
testRecoil('Works with useTransition', async ({concurrentMode}) => {
if (!reactMode().concurrent || !concurrentMode) {
return;
}
const indexAtom = atom({
key: `index${nextID++}`,
default: 0,
});
// Basic implementation of a cache that suspends:
const cache = new Map<
number,
| {promise: null, state: string, value: string}
| {promise: Promise<void>, state: string, value: null},
>();
const resolvers = [];
function getItem(index: number) {
if (cache.has(index) && cache.get(index)?.state === 'ready') {
return cache.get(index)?.value;
} else if (cache.has(index)) {
throw cache.get(index)?.promise;
} else {
const promise = new Promise(resolve => {
const onComplete = () => {
cache.set(index, {
state: 'ready',
value: `v${index}`,
promise: null,
});
resolve();
};
resolvers.push(onComplete);
});
const newEntry = {
state: 'loading',
value: null,
promise,
};
// $FlowFixMe[incompatible-call]
cache.set(index, newEntry);
throw promise;
}
}
function ItemContents({index}: $TEMPORARY$object<{index: number}>) {
const item = getItem(index);
return (
<div>
Item {index} = {item}
</div>
);
}
function Item({index}: $TEMPORARY$object<{index: number}>) {
return (
<React.Suspense fallback="Suspended">
<ItemContents index={index} />
</React.Suspense>
);
}
let incrementIndex;
function Main() {
const [index, setIndex] = useRecoilState(indexAtom);
const [isPending, startTransition] = useTransition();
incrementIndex = () => {
startTransition(() => {
setIndex(x => x + 1);
});
};
return (
<div>
Index: {index} - {isPending && 'In transition - '}
<Item index={index} />
</div>
);
}
const c = renderElements(<Main />);
// Initial load:
expect(c.textContent).toEqual('Index: 0 - Suspended');
act(() => resolvers[0]());
await flushPromisesAndTimers();
expect(c.textContent).toEqual('Index: 0 - Item 0 = v0');
// Increment index a single time; see previous item in transition, then once
// the new item resolves, see the new item:
act(() => incrementIndex());
expect(c.textContent).toEqual('Index: 0 - In transition - Item 0 = v0');
act(() => resolvers[1]());
await flushPromisesAndTimers();
expect(c.textContent).toEqual('Index: 1 - Item 1 = v1');
// Increment index a second time during transition; see previous item in
// transition, then once the new _second_ item resolves, see that new item:
act(() => incrementIndex());
expect(c.textContent).toEqual('Index: 1 - In transition - Item 1 = v1');
act(() => incrementIndex());
expect(c.textContent).toEqual('Index: 1 - In transition - Item 1 = v1');
act(() => resolvers[2]());
await flushPromisesAndTimers();
expect(c.textContent).toEqual('Index: 1 - In transition - Item 1 = v1');
act(() => resolvers[3]());
await flushPromisesAndTimers();
expect(c.textContent).toEqual('Index: 3 - Item 3 = v3');
});
testRecoil('useRecoilValue()', async ({concurrentMode}) => {
if (useTransition == null) {
return;
}
const myAtom = atom({key: 'useRecoilValue atom', default: 0});
let resolvers: Array<(result: Promise<string> | string) => void> = [];
function resolveSelectors() {
resolvers.forEach(resolve => resolve('RESOLVED'));
resolvers = [];
}
const query = selectorFamily({
key: 'useRecoilValue selector',
get:
(
// $FlowFixMe[missing-local-annot]
param,
) =>
// $FlowFixMe[missing-local-annot]
({get}) => {
const value = get(myAtom);
return new Promise(resolve => {
resolvers.push(resolve);
// $FlowFixMe[incompatible-type]
}).then(str => `${param} ${value} ${str}`);
},
});
function Component({index}: $TEMPORARY$object<{index: number}>) {
const value = useRecoilValue(query(index));
return (
<>
{index} {value}
</>
);
}
let startReactTransition, startRecoilTransition, startBothTransition;
function Main() {
const [reactState, setReactState] = useState(0);
const [recoilState, setRecoilState] = useRecoilState(myAtom);
const [inTransition, startTransition] = useTransition();
startReactTransition = () => {
startTransition(() => {
setReactState(x => x + 1);
});
};
startRecoilTransition = () => {
startTransition(() => {
setRecoilState(x => x + 1);
});
};
startBothTransition = () => {
startTransition(() => {
setReactState(x => x + 1);
setRecoilState(x => x + 1);
});
};
return (
<>
React:{reactState} Recoil:{recoilState}{' '}
{inTransition ? '[IN TRANSITION] ' : ''}|{' '}
<React.Suspense fallback="LOADING">
<Component index={reactState} />
</React.Suspense>
</>
);
}
const c = renderElements(<Main />);
expect(c.textContent).toBe('React:0 Recoil:0 | LOADING');
act(resolveSelectors);
await flushPromisesAndTimers();
expect(c.textContent).toBe('React:0 Recoil:0 | 0 0 0 RESOLVED');
// Transition changing React State
act(startReactTransition);
expect(c.textContent).toBe(
concurrentMode
? 'React:0 Recoil:0 [IN TRANSITION] | 0 0 0 RESOLVED'
: 'React:1 Recoil:0 | LOADING',
);
act(resolveSelectors);
await flushPromisesAndTimers();
expect(c.textContent).toBe('React:1 Recoil:0 | 1 1 0 RESOLVED');
// Transition changing Recoil State
act(startRecoilTransition);
expect(c.textContent).toBe(
concurrentMode && reactMode().concurrent
? 'React:1 Recoil:0 [IN TRANSITION] | 1 1 0 RESOLVED'
: 'React:1 Recoil:1 | LOADING',
);
act(resolveSelectors);
await flushPromisesAndTimers();
expect(c.textContent).toBe('React:1 Recoil:1 | 1 1 1 RESOLVED');
// Second transition changing Recoil State
act(startRecoilTransition);
expect(c.textContent).toBe(
concurrentMode && reactMode().concurrent
? 'React:1 Recoil:1 [IN TRANSITION] | 1 1 1 RESOLVED'
: 'React:1 Recoil:2 | LOADING',
);
act(resolveSelectors);
await flushPromisesAndTimers();
expect(c.textContent).toBe('React:1 Recoil:2 | 1 1 2 RESOLVED');
// Transition with both React and Recoil state
act(startBothTransition);
expect(c.textContent).toBe(
concurrentMode &&
(reactMode().concurrent || reactMode().mode === 'MUTABLE_SOURCE')
? 'React:1 Recoil:2 [IN TRANSITION] | 1 1 2 RESOLVED'
: 'React:2 Recoil:3 | LOADING',
);
act(resolveSelectors);
await flushPromisesAndTimers();
act(resolveSelectors);
await flushPromisesAndTimers();
expect(c.textContent).toBe('React:2 Recoil:3 | 2 2 3 RESOLVED');
});
testRecoil(
'useRecoilValue_TRANSITION_SUPPORT_UNSTABLE()',
async ({concurrentMode}) => {
if (useTransition == null) {
return;
}
const myAtom = atom({key: 'TRANSITION_SUPPORT_UNSTABLE atom', default: 0});
let resolvers: Array<(result: Promise<string> | string) => void> = [];
function resolveSelectors() {
resolvers.forEach(resolve => resolve('RESOLVED'));
resolvers = [];
}
const query = selectorFamily({
key: 'TRANSITION_SUPPORT_UNSTABLE selector',
get:
(
// $FlowFixMe[missing-local-annot]
param,
) =>
// $FlowFixMe[missing-local-annot]
({get}) => {
const value = get(myAtom);
return new Promise(resolve => {
resolvers.push(resolve);
// $FlowFixMe[incompatible-type]
}).then(str => `${param} ${value} ${str}`);
},
});
function Component({index}: $TEMPORARY$object<{index: number}>) {
const value = useRecoilValue_TRANSITION_SUPPORT_UNSTABLE(query(index));
return (
<>
{index} {value}
</>
);
}
let startReactTransition, startRecoilTransition, startBothTransition;
function Main() {
const [reactState, setReactState] = useState(0);
const [recoilState, setRecoilState] =
useRecoilState_TRANSITION_SUPPORT_UNSTABLE(myAtom);
const [inTransition, startTransition] = useTransition();
startReactTransition = () => {
startTransition(() => {
setReactState(x => x + 1);
});
};
startRecoilTransition = () => {
startTransition(() => {
setRecoilState(x => x + 1);
});
};
startBothTransition = () => {
startTransition(() => {
setReactState(x => x + 1);
setRecoilState(x => x + 1);
});
};
return (
<>
React:{reactState} Recoil:{recoilState}{' '}
{inTransition ? '[IN TRANSITION] ' : ''}|{' '}
<React.Suspense fallback="LOADING">
<Component index={reactState} />
</React.Suspense>
</>
);
}
const c = renderElements(<Main />);
expect(c.textContent).toBe('React:0 Recoil:0 | LOADING');
act(resolveSelectors);
await flushPromisesAndTimers();
expect(c.textContent).toBe('React:0 Recoil:0 | 0 0 0 RESOLVED');
// Transition changing React State
act(startReactTransition);
expect(c.textContent).toBe(
concurrentMode
? 'React:0 Recoil:0 [IN TRANSITION] | 0 0 0 RESOLVED'
: 'React:1 Recoil:0 | LOADING',
);
act(resolveSelectors);
await flushPromisesAndTimers();
expect(c.textContent).toBe('React:1 Recoil:0 | 1 1 0 RESOLVED');
// Transition changing Recoil State
act(startRecoilTransition);
expect(c.textContent).toBe(
concurrentMode && reactMode().early
? 'React:1 Recoil:0 [IN TRANSITION] | 1 1 0 RESOLVED'
: 'React:1 Recoil:1 | LOADING',
);
act(resolveSelectors);
await flushPromisesAndTimers();
expect(c.textContent).toBe('React:1 Recoil:1 | 1 1 1 RESOLVED');
// Second transition changing Recoil State
act(startRecoilTransition);
expect(c.textContent).toBe(
concurrentMode && reactMode().early
? 'React:1 Recoil:1 [IN TRANSITION] | 1 1 1 RESOLVED'
: 'React:1 Recoil:2 | LOADING',
);
act(resolveSelectors);
await flushPromisesAndTimers();
expect(c.textContent).toBe('React:1 Recoil:2 | 1 1 2 RESOLVED');
// Transition with both React and Recoil State
act(startBothTransition);
expect(c.textContent).toBe(
concurrentMode
? 'React:1 Recoil:2 [IN TRANSITION] | 1 1 2 RESOLVED'
: 'React:2 Recoil:3 | LOADING',
);
act(resolveSelectors);
await flushPromisesAndTimers();
act(resolveSelectors);
await flushPromisesAndTimers();
expect(c.textContent).toBe('React:2 Recoil:3 | 2 2 3 RESOLVED');
},
);