/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails oncall+recoil
* @flow strict-local
* @format
*/
'use strict';
const {
getRecoilTestFn
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React, useRef, useState, act, useStoreRef, atom, atomFamily, selector, useRecoilCallback, useRecoilValue, useRecoilState, useSetRecoilState, useResetRecoilState, ReadsAtom, flushPromisesAndTimers, renderElements, invariant;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({
useRef,
useState
} = require('react'));
({
act
} = require('ReactTestUtils'));
({
useStoreRef
} = require('../../core/Recoil_RecoilRoot'));
({
atom,
atomFamily,
selector,
useRecoilCallback,
useSetRecoilState,
useResetRecoilState,
useRecoilValue,
useRecoilState
} = require('../../Recoil_index'));
({
ReadsAtom,
flushPromisesAndTimers,
renderElements
} = 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.reject(new Error("Callback didn't resolve"));
let cb;
declare function Component(): any;
renderElements();
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;
declare function Component(): any;
renderElements();
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.reject(new Error("Callback didn't resolve"));
declare function Component(): any;
const container = renderElements(<>
>);
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;
declare function Component(): any;
const container = renderElements(<>
>);
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 = [];
declare function Component(): any;
const container = renderElements([, ]);
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;
declare var delay: () => any; // no delay initially
declare function Component(): any; // It sees an update flushed after the cb is created:
renderElements();
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;
declare var resumeCallback: () => any;
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 current state', () => {
const myAtom = atom({
key: 'useRecoilCallback updater',
default: 'DEFAULT'
});
let setAtom;
let cb;
declare function Component(): any;
const c = renderElements(<>
>);
expect(c.textContent).toEqual('"DEFAULT"'); // Set then callback in the same transaction
act(() => {
setAtom('SET');
cb('SET');
cb('UPDATE AGAIN');
});
expect(c.textContent).toEqual('"UPDATE AGAIN"');
});
testRecoil('goes to snapshot', async () => {
const myAtom = atom({
key: 'Goto Snapshot From Callback',
default: 'DEFAULT'
});
let cb;
declare function RecoilCallback(): any;
const c = renderElements(<>
>);
expect(c.textContent).toEqual('"DEFAULT"');
act(() => void cb());
await flushPromisesAndTimers();
expect(c.textContent).toEqual('"SET IN SNAPSHOT"');
});
testRecoil('Updates are batched', () => {
const family = atomFamily({
key: 'useRecoilCallback/batching/family',
default: 0
});
let cb;
declare function RecoilCallback(): any;
let store: any; // flowlint-line unclear-type:off
declare function GetStore(): any;
renderElements(<>
>);
invariant(store, 'store should be initialized');
const originalReplaceState = store.replaceState; // $FlowFixMe[cannot-write]
store.replaceState = jest.fn(originalReplaceState);
expect(store.replaceState).toHaveBeenCalledTimes(0);
act(() => cb());
expect(store.replaceState).toHaveBeenCalledTimes(1); // $FlowFixMe[cannot-write]
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;
declare var Component: () => any;
const out = renderElements();
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;
declare var Component: () => any;
const c = renderElements();
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++;
}]
});
declare var Component: () => any;
const c = renderElements();
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;
declare var Component: () => any;
const c = renderElements();
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');
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;
declare function Component(): any;
const container = renderElements();
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;
const myselector = selector({
key: 'useRecoilCallback refresh entire cache selector',
get: ({
get
}) => [get(myatom), i++]
});
let setMyAtom;
let refreshSelector;
declare function Component(): any;
const container = renderElements();
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 cache', () => {
testRecoil('Snapshot is cached', () => {
const myAtom = atom({
key: 'useRecoilCallback snapshot cached',
default: 'DEFAULT'
});
let getSnapshot;
let setMyAtom, resetMyAtom;
declare function Component(): any;
renderElements();
declare var getAtom: (snapshot: any) => any;
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 = atom({
key: 'useRecoilCallback snapshot cache retained',
default: 'DEFAULT'
});
let getSnapshot;
let setMyAtom;
declare function Component(): any;
renderElements();
declare var getAtom: (snapshot: any) => any;
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);
});
});