recoil
Version:
Recoil - A state management library for React
693 lines (680 loc) • 18 kB
Flow
/**
* 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
*/
;
import type { Store } from '../../core/Recoil_State';
const {
getRecoilTestFn
} = require('../../__test_utils__/Recoil_TestingUtils');
let store: Store, React, Profiler, useState, ReactDOM, act, RecoilRoot, getRecoilValueAsLoadable, setRecoilValue, useRecoilState, useRecoilValue, useSetRecoilState, useSetUnvalidatedAtomValues, ReadsAtom, componentThatReadsAndWritesAtom, flushPromisesAndTimers, renderElements, mutableSourceExists, stableStringify, atom, atomFamily, selectorFamily, pAtom;
const testRecoil = getRecoilTestFn(() => {
const {
makeStore
} = require('../../__test_utils__/Recoil_TestingUtils');
React = require('react');
({
Profiler,
useState
} = require('react'));
ReactDOM = require('ReactDOMLegacy_DEPRECATED');
({
act
} = require('ReactTestUtils'));
({
RecoilRoot
} = require('../../core/Recoil_RecoilRoot.react'));
({
getRecoilValueAsLoadable,
setRecoilValue
} = require('../../core/Recoil_RecoilValueInterface'));
({
useRecoilState,
useRecoilValue,
useSetRecoilState,
useSetUnvalidatedAtomValues
} = require('../../hooks/Recoil_Hooks'));
({
ReadsAtom,
componentThatReadsAndWritesAtom,
flushPromisesAndTimers,
renderElements
} = require('../../__test_utils__/Recoil_TestingUtils'));
({
mutableSourceExists
} = require('../../util/Recoil_mutableSource'));
stableStringify = require('../../util/Recoil_stableStringify');
atom = require('../Recoil_atom');
atomFamily = require('../Recoil_atomFamily');
selectorFamily = require('../Recoil_selectorFamily');
store = makeStore();
pAtom = atomFamily({
key: 'pAtom',
default: 'fallback'
});
});
let fbOnlyTest = test.skip; // @fb-only: fbOnlyTest = testRecoil;
let id = 0;
declare function get(recoilValue: any): any;
declare function set(recoilValue: any, value: any): any;
testRecoil('Read fallback by default', () => {
expect(get(pAtom({
k: 'x'
}))).toBe('fallback');
});
testRecoil('Uses value for parameter', () => {
set(pAtom({
k: 'x'
}), 'xValue');
set(pAtom({
k: 'y'
}), 'yValue');
expect(get(pAtom({
k: 'x'
}))).toBe('xValue');
expect(get(pAtom({
k: 'y'
}))).toBe('yValue');
expect(get(pAtom({
k: 'z'
}))).toBe('fallback');
});
testRecoil('Works with non-overlapping sets', () => {
set(pAtom({
x: 'x'
}), 'xValue');
set(pAtom({
y: 'y'
}), 'yValue');
expect(get(pAtom({
x: 'x'
}))).toBe('xValue');
expect(get(pAtom({
y: 'y'
}))).toBe('yValue');
});
testRecoil('Works with atom default', () => {
const fallbackAtom = atom({
key: 'fallback',
default: 0
});
const hasFallback = atomFamily({
key: 'hasFallback',
default: fallbackAtom
});
expect(get(hasFallback({
k: 'x'
}))).toBe(0);
set(fallbackAtom, 1);
expect(get(hasFallback({
k: 'x'
}))).toBe(1);
set(hasFallback({
k: 'x'
}), 2);
expect(get(hasFallback({
k: 'x'
}))).toBe(2);
expect(get(hasFallback({
k: 'y'
}))).toBe(1);
});
testRecoil('Works with parameterized default', () => {
const paramDefaultAtom = atomFamily({
key: 'parameterized default',
default: ({
num
}) => num
});
expect(get(paramDefaultAtom({
num: 1
}))).toBe(1);
expect(get(paramDefaultAtom({
num: 2
}))).toBe(2);
set(paramDefaultAtom({
num: 1
}), 3);
expect(get(paramDefaultAtom({
num: 1
}))).toBe(3);
expect(get(paramDefaultAtom({
num: 2
}))).toBe(2);
});
testRecoil('Works with date as parameter', () => {
const dateAtomFamily = atomFamily({
key: 'dateFamily',
default: date => 0
}); // $FlowFixMe[incompatible-call] added when improving typing for this parameters
expect(get(dateAtomFamily(new Date(2021, 2, 25)))).toBe(0); // $FlowFixMe[incompatible-call] added when improving typing for this parameters
expect(get(dateAtomFamily(new Date(2021, 2, 26)))).toBe(0); // $FlowFixMe[incompatible-call] added when improving typing for this parameters
set(dateAtomFamily(new Date(2021, 2, 25)), 1); // $FlowFixMe[incompatible-call] added when improving typing for this parameters
expect(get(dateAtomFamily(new Date(2021, 2, 25)))).toBe(1); // $FlowFixMe[incompatible-call] added when improving typing for this parameters
expect(get(dateAtomFamily(new Date(2021, 2, 26)))).toBe(0);
});
testRecoil('Works with parameterized fallback', () => {
const fallbackAtom = atomFamily({
key: 'parameterized fallback default',
default: ({
num
}) => num * 10
});
const paramFallbackAtom = atomFamily({
key: 'parameterized fallback',
default: fallbackAtom
});
expect(get(paramFallbackAtom({
num: 1
}))).toBe(10);
expect(get(paramFallbackAtom({
num: 2
}))).toBe(20);
set(paramFallbackAtom({
num: 1
}), 3);
expect(get(paramFallbackAtom({
num: 1
}))).toBe(3);
expect(get(paramFallbackAtom({
num: 2
}))).toBe(20);
set(fallbackAtom({
num: 2
}), 200);
expect(get(paramFallbackAtom({
num: 2
}))).toBe(200);
set(fallbackAtom({
num: 1
}), 100);
expect(get(paramFallbackAtom({
num: 1
}))).toBe(3);
expect(get(paramFallbackAtom({
num: 2
}))).toBe(200);
});
testRecoil('atomFamily async fallback', async () => {
const paramFallback = atomFamily({
key: 'paramaterizedAtom async Fallback',
default: Promise.resolve(42)
});
const container = renderElements(<ReadsAtom atom={paramFallback({})} />);
expect(container.textContent).toEqual('loading');
act(() => jest.runAllTimers());
await flushPromisesAndTimers();
expect(container.textContent).toEqual('42');
});
testRecoil('Parameterized fallback with atom and async', async () => {
const paramFallback = atomFamily({
key: 'parameterized async Fallback',
default: ({
param
}) => ({
value: 'value',
atom: atom({
key: `param async fallback atom ${id++}`,
default: 'atom'
}),
async: Promise.resolve('async')
})[param]
});
const valueCont = renderElements(<ReadsAtom atom={paramFallback({
param: 'value'
})} />);
expect(valueCont.textContent).toEqual('"value"');
const atomCont = renderElements(<ReadsAtom atom={paramFallback({
param: 'atom'
})} />);
expect(atomCont.textContent).toEqual('"atom"');
const asyncCont = renderElements(<ReadsAtom atom={paramFallback({
param: 'async'
})} />);
expect(asyncCont.textContent).toEqual('loading');
act(() => jest.runAllTimers());
await flushPromisesAndTimers();
expect(asyncCont.textContent).toEqual('"async"');
});
fbOnlyTest('atomFamily with scope', () => {
const scopeForParamAtom = atom<string>({
key: 'scope atom for atomFamily',
default: 'foo'
});
const paramAtomWithScope = atomFamily<string, {
k: string
}>({
key: 'parameterized atom with scope',
default: 'default',
scopeRules_APPEND_ONLY_READ_THE_DOCS: [[scopeForParamAtom]]
});
expect(get(paramAtomWithScope({
k: 'x'
}))).toBe('default');
expect(get(paramAtomWithScope({
k: 'y'
}))).toBe('default');
set(paramAtomWithScope({
k: 'x'
}), 'xValue1');
expect(get(paramAtomWithScope({
k: 'x'
}))).toBe('xValue1');
expect(get(paramAtomWithScope({
k: 'y'
}))).toBe('default');
set(paramAtomWithScope({
k: 'y'
}), 'yValue1');
expect(get(paramAtomWithScope({
k: 'x'
}))).toBe('xValue1');
expect(get(paramAtomWithScope({
k: 'y'
}))).toBe('yValue1');
set(scopeForParamAtom, 'bar');
expect(get(paramAtomWithScope({
k: 'x'
}))).toBe('default');
expect(get(paramAtomWithScope({
k: 'y'
}))).toBe('default');
set(paramAtomWithScope({
k: 'x'
}), 'xValue2');
expect(get(paramAtomWithScope({
k: 'x'
}))).toBe('xValue2');
expect(get(paramAtomWithScope({
k: 'y'
}))).toBe('default');
set(paramAtomWithScope({
k: 'y'
}), 'yValue2');
expect(get(paramAtomWithScope({
k: 'x'
}))).toBe('xValue2');
expect(get(paramAtomWithScope({
k: 'y'
}))).toBe('yValue2');
});
fbOnlyTest('atomFamily with parameterized scope', () => {
const paramScopeForParamAtom = atomFamily<string, {
namespace: string
}>({
key: 'scope atom for atomFamily with parameterized scope',
default: ({
namespace
}) => namespace
});
const paramAtomWithParamScope = atomFamily<string, {
k: string,
n: string,
}>({
key: 'parameterized atom with parameterized scope',
default: 'default',
scopeRules_APPEND_ONLY_READ_THE_DOCS: [[({
n
}) => paramScopeForParamAtom({
namespace: n
})]]
});
expect(get(paramScopeForParamAtom({
namespace: 'foo'
}))).toBe('foo');
expect(get(paramAtomWithParamScope({
n: 'foo',
k: 'x'
}))).toBe('default');
expect(get(paramAtomWithParamScope({
n: 'foo',
k: 'y'
}))).toBe('default');
set(paramAtomWithParamScope({
n: 'foo',
k: 'x'
}), 'xValue1');
expect(get(paramAtomWithParamScope({
n: 'foo',
k: 'x'
}))).toBe('xValue1');
expect(get(paramAtomWithParamScope({
n: 'foo',
k: 'y'
}))).toBe('default');
set(paramAtomWithParamScope({
n: 'foo',
k: 'y'
}), 'yValue1');
expect(get(paramAtomWithParamScope({
n: 'foo',
k: 'x'
}))).toBe('xValue1');
expect(get(paramAtomWithParamScope({
n: 'foo',
k: 'y'
}))).toBe('yValue1');
set(paramScopeForParamAtom({
namespace: 'foo'
}), 'eggs');
expect(get(paramScopeForParamAtom({
namespace: 'foo'
}))).toBe('eggs');
expect(get(paramAtomWithParamScope({
n: 'foo',
k: 'x'
}))).toBe('default');
expect(get(paramAtomWithParamScope({
n: 'foo',
k: 'y'
}))).toBe('default');
set(paramAtomWithParamScope({
n: 'foo',
k: 'x'
}), 'xValue2');
expect(get(paramAtomWithParamScope({
n: 'foo',
k: 'x'
}))).toBe('xValue2');
expect(get(paramAtomWithParamScope({
n: 'foo',
k: 'y'
}))).toBe('default');
set(paramAtomWithParamScope({
n: 'foo',
k: 'y'
}), 'yValue2');
expect(get(paramAtomWithParamScope({
n: 'foo',
k: 'x'
}))).toBe('xValue2');
expect(get(paramAtomWithParamScope({
n: 'foo',
k: 'y'
}))).toBe('yValue2');
expect(get(paramScopeForParamAtom({
namespace: 'bar'
}))).toBe('bar');
expect(get(paramAtomWithParamScope({
n: 'bar',
k: 'x'
}))).toBe('default');
expect(get(paramAtomWithParamScope({
n: 'bar',
k: 'y'
}))).toBe('default');
set(paramAtomWithParamScope({
n: 'bar',
k: 'x'
}), 'xValue3');
expect(get(paramAtomWithParamScope({
n: 'bar',
k: 'x'
}))).toBe('xValue3');
expect(get(paramAtomWithParamScope({
n: 'bar',
k: 'y'
}))).toBe('default');
set(paramAtomWithParamScope({
n: 'bar',
k: 'y'
}), 'yValue3');
expect(get(paramAtomWithParamScope({
n: 'bar',
k: 'x'
}))).toBe('xValue3');
expect(get(paramAtomWithParamScope({
n: 'bar',
k: 'y'
}))).toBe('yValue3');
set(paramScopeForParamAtom({
namespace: 'bar'
}), 'spam');
expect(get(paramScopeForParamAtom({
namespace: 'bar'
}))).toBe('spam');
expect(get(paramAtomWithParamScope({
n: 'bar',
k: 'x'
}))).toBe('default');
expect(get(paramAtomWithParamScope({
n: 'bar',
k: 'y'
}))).toBe('default');
set(paramAtomWithParamScope({
n: 'bar',
k: 'x'
}), 'xValue4');
expect(get(paramAtomWithParamScope({
n: 'bar',
k: 'x'
}))).toBe('xValue4');
expect(get(paramAtomWithParamScope({
n: 'bar',
k: 'y'
}))).toBe('default');
set(paramAtomWithParamScope({
n: 'bar',
k: 'y'
}), 'yValue4');
expect(get(paramAtomWithParamScope({
n: 'bar',
k: 'x'
}))).toBe('xValue4');
expect(get(paramAtomWithParamScope({
n: 'bar',
k: 'y'
}))).toBe('yValue4');
});
testRecoil('Returns the fallback for parameterized atoms', () => {
let theAtom = null;
let setUnvalidatedAtomValues;
let setAtomParam;
let setAtomValue;
declare function SetsUnvalidatedAtomValues(): any;
let setVisible;
declare function Switch(arg0: any): any;
declare function MyReadsAtom(arg0: any): any;
const container = renderElements(<>
<SetsUnvalidatedAtomValues />
<Switch>
<MyReadsAtom getAtom={() => theAtom} />
</Switch>
</>);
act(() => {
setUnvalidatedAtomValues(new Map().set('notDefinedYetAtomFamilyWithFallback', 123));
});
const fallback = atom<number>({
key: 'fallback for atomFamily',
default: 222
});
theAtom = atomFamily({
key: 'notDefinedYetAtomFamilyWithFallback',
default: fallback,
persistence_UNSTABLE: {
type: 'url',
validator: (_, returnFallback) => returnFallback
}
});
act(() => {
setVisible(true);
});
expect(container.textContent).toBe('222');
act(() => {
setAtomValue(111);
});
expect(container.textContent).toBe('111');
act(() => {
setAtomParam({
num: 2
});
});
expect(container.textContent).toBe('222');
});
testRecoil('Returns the fallback for parameterized atoms with a selector as the fallback', () => {
let theAtom = null;
let setUnvalidatedAtomValues;
let setAtomParam;
let setAtomValue;
declare function SetsUnvalidatedAtomValues(): any;
let setVisible;
declare function Switch(arg0: any): any;
declare function MyReadsAtom(arg0: any): any;
const container = renderElements(<>
<SetsUnvalidatedAtomValues />
<Switch>
<MyReadsAtom getAtom={() => theAtom} />
</Switch>
</>);
act(() => {
setUnvalidatedAtomValues(new Map().set('notDefinedYetAtomFamilyFallbackSel', 123));
});
theAtom = atomFamily({
key: 'notDefinedYetAtomFamilyFallbackSel',
default: selectorFamily({
key: 'notDefinedYetAtomFamilyFallbackSelFallback',
get: ({
num
}) => () => num === 1 ? 456 : 789
}),
persistence_UNSTABLE: {
type: 'url',
validator: (_, notValid) => notValid
}
});
act(() => {
setVisible(true);
});
expect(container.textContent).toBe('789');
act(() => {
setAtomValue(111);
});
expect(container.textContent).toBe('111');
act(() => {
setAtomParam({
num: 1
});
});
expect(container.textContent).toBe('456');
});
testRecoil('Independent atom subscriptions', gks => {
const BASE_CALLS = mutableSourceExists() || gks.includes('recoil_suppress_rerender_in_callback') ? 0 : 1;
const myAtom = atomFamily({
key: 'atomFamily/independent subscriptions',
default: 'DEFAULT'
});
declare var TrackingComponent: (param: any) => any;
const [ComponentA, setValueA, getNumUpdatesA] = TrackingComponent('A');
const [ComponentB, setValueB, getNumUpdatesB] = TrackingComponent('B');
const container = renderElements(<>
<ComponentA />
<ComponentB />
</>); // Initial:
expect(container.textContent).toBe('"DEFAULT""DEFAULT"');
expect(getNumUpdatesA()).toBe(BASE_CALLS + 1);
expect(getNumUpdatesB()).toBe(BASE_CALLS + 1); // After setting at parameter A, component A should update:
act(() => setValueA(1));
expect(container.textContent).toBe('1"DEFAULT"');
expect(getNumUpdatesA()).toBe(BASE_CALLS + 2);
expect(getNumUpdatesB()).toBe(BASE_CALLS + 1); // After setting at parameter B, component B should update:
act(() => setValueB(2));
expect(container.textContent).toBe('12');
expect(getNumUpdatesA()).toBe(BASE_CALLS + 2);
expect(getNumUpdatesB()).toBe(BASE_CALLS + 2);
});
describe('Effects', () => {
testRecoil('Initialization', () => {
let inited = 0;
const myFamily = atomFamily<string, number>({
key: 'atomFamily effect init',
default: 'DEFAULT',
effects_UNSTABLE: [({
setSelf
}) => {
inited++;
setSelf('INIT');
}]
});
expect(inited).toEqual(0);
expect(get(myFamily(1))).toEqual('INIT');
expect(inited).toEqual(1);
set(myFamily(2));
expect(inited).toEqual(2);
const [ReadsWritesAtom, _, reset] = componentThatReadsAndWritesAtom(myFamily(1));
const c = renderElements(<ReadsWritesAtom />);
expect(c.textContent).toEqual('"INIT"');
act(reset);
expect(c.textContent).toEqual('"DEFAULT"');
});
testRecoil('Parameterized Initialization', () => {
const myFamily = atomFamily({
key: 'atomFamily effect parameterized init',
default: 'DEFAULT',
effects_UNSTABLE: param => [({
setSelf
}) => setSelf(param)]
});
expect(get(myFamily(1))).toEqual(1);
expect(get(myFamily(2))).toEqual(2);
});
testRecoil('Cleanup Handlers - when root unmounted', () => {
const refCounts: {
[string]: number
} = {
A: 0,
B: 0
};
const atoms = atomFamily({
key: 'atomFamily effect cleanup',
default: p => p,
effects_UNSTABLE: p => [() => {
refCounts[p]++;
return () => {
refCounts[p]--;
};
}]
});
let setNumRoots;
declare function App(): any;
const c = document.createElement('div');
act(() => {
ReactDOM.render(<App />, c);
});
expect(c.textContent).toBe('');
expect(refCounts).toEqual({
A: 0,
B: 0
});
act(() => setNumRoots(1));
expect(c.textContent).toBe('"A""B"');
expect(refCounts).toEqual({
A: 1,
B: 1
});
act(() => setNumRoots(2));
expect(c.textContent).toBe('"A""B""A""B"');
expect(refCounts).toEqual({
A: 2,
B: 2
});
act(() => setNumRoots(1));
expect(c.textContent).toBe('"A""B"');
expect(refCounts).toEqual({
A: 1,
B: 1
});
act(() => setNumRoots(0));
expect(c.textContent).toBe('');
expect(refCounts).toEqual({
A: 0,
B: 0
});
});
}); // TODO add non-current-entry tests
// TODO add persistence tests