recoil
Version:
Recoil - A state management library for React
505 lines (481 loc) • 18.3 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+obviz
* @flow strict-local
* @format
*/
'use strict';
const {
getRecoilTestFn
} = require('../../testing/Recoil_TestingUtils');
let React, act, useGotoRecoilSnapshot, useRecoilTransactionObserver, atom, constSelector, selector, ReadsAtom, asyncSelector, componentThatReadsAndWritesAtom, renderElements, Snapshot, freshSnapshot;
const testRecoil = getRecoilTestFn(() => {
React = require('React');
({
act
} = require('ReactTestUtils'));
({
useGotoRecoilSnapshot,
useRecoilTransactionObserver
} = require('../../hooks/Recoil_Hooks'));
atom = require('../../recoil_values/Recoil_atom');
constSelector = require('../../recoil_values/Recoil_constSelector');
selector = require('../../recoil_values/Recoil_selector');
({
ReadsAtom,
asyncSelector,
componentThatReadsAndWritesAtom,
renderElements
} = require('../../testing/Recoil_TestingUtils'));
({
Snapshot,
freshSnapshot
} = require('../Recoil_Snapshot'));
}); // Test first since we are testing all registered nodes
testRecoil('getNodes', () => {
const snapshot = freshSnapshot();
const {
getNodes_UNSTABLE
} = snapshot;
expect(Array.from(getNodes_UNSTABLE()).length).toEqual(0);
expect(Array.from(getNodes_UNSTABLE({
isInitialized: true
})).length).toEqual(0); // expect(Array.from(getNodes_UNSTABLE({isSet: true})).length).toEqual(0);
// Test atoms
const myAtom = atom({
key: 'snapshot getNodes atom',
default: 'DEFAULT'
});
expect(Array.from(getNodes_UNSTABLE()).length).toEqual(1);
expect(Array.from(getNodes_UNSTABLE({
isInitialized: true
})).length).toEqual(0);
expect(snapshot.getLoadable(myAtom).contents).toEqual('DEFAULT');
const nodesAfterGet = Array.from(getNodes_UNSTABLE());
expect(nodesAfterGet.length).toEqual(1);
expect(nodesAfterGet[0]).toBe(myAtom);
expect(snapshot.getLoadable(nodesAfterGet[0]).contents).toEqual('DEFAULT'); // Test selectors
const mySelector = selector({
key: 'snapshot getNodes selector',
get: ({
get
}) => get(myAtom) + '-SELECTOR'
});
expect(Array.from(getNodes_UNSTABLE()).length).toEqual(2);
expect(Array.from(getNodes_UNSTABLE({
isInitialized: true
})).length).toEqual(1);
expect(snapshot.getLoadable(mySelector).contents).toEqual('DEFAULT-SELECTOR');
expect(Array.from(getNodes_UNSTABLE({
isInitialized: true
})).length).toEqual(2); // expect(Array.from(getNodes_UNSTABLE({types: ['atom']})).length).toEqual(1);
// const selectorNodes = Array.from(getNodes_UNSTABLE({types: ['selector']}));
// expect(selectorNodes.length).toEqual(1);
// expect(selectorNodes[0]).toBe(mySelector);
// Test dirty atoms
expect(Array.from(getNodes_UNSTABLE()).length).toEqual(2); // expect(Array.from(getNodes_UNSTABLE({isSet: true})).length).toEqual(0);
expect(Array.from(snapshot.getNodes_UNSTABLE({
isModified: true
})).length).toEqual(0);
const updatedSnapshot = snapshot.map(({
set
}) => set(myAtom, 'SET'));
expect(Array.from(snapshot.getNodes_UNSTABLE({
isModified: true
})).length).toEqual(0);
expect(Array.from(updatedSnapshot.getNodes_UNSTABLE({
isModified: true
})).length).toEqual(1); // expect(
// Array.from(snapshot.getNodes_UNSTABLE({isSet: true})).length,
// ).toEqual(0);
// expect(
// Array.from(updatedSnapshot.getNodes_UNSTABLE({isSet: true})).length,
// ).toEqual(1);
const dirtyAtom = Array.from(updatedSnapshot.getNodes_UNSTABLE({
isModified: true
}))[0];
expect(snapshot.getLoadable(dirtyAtom).contents).toEqual('DEFAULT');
expect(updatedSnapshot.getLoadable(dirtyAtom).contents).toEqual('SET'); // Test reset
const resetSnapshot = updatedSnapshot.map(({
reset
}) => reset(myAtom));
expect(Array.from(resetSnapshot.getNodes_UNSTABLE({
isModified: true
})).length).toEqual(1); // expect(
// Array.from(resetSnapshot.getNodes_UNSTABLE({isSet: true})).length,
// ).toEqual(0);
// TODO Test dirty selectors
});
testRecoil('State ID after going to snapshot matches the ID of the snapshot', () => {
const seenIDs = new Set();
const snapshots = [];
let expectedSnapshotID = null;
const myAtom = atom({
key: 'Snapshot ID atom',
default: 0
});
const mySelector = constSelector(myAtom); // For read-only testing below
declare var transactionObserver: (arg0: any) => any;
declare function TransactionObserver(): any;
let gotoSnapshot;
declare function GotoSnapshot(): any;
const [WriteAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom);
const c = renderElements(<>
<TransactionObserver />
<GotoSnapshot />
<WriteAtom />
<ReadsAtom atom={mySelector} />
</>);
expect(c.textContent).toBe('00'); // Test changing state produces a new state version
act(() => setAtom(1));
act(() => setAtom(2));
expect(snapshots.length).toBe(2);
expect(seenIDs.size).toBe(2); // Test going to a previous snapshot re-uses the state ID
expectedSnapshotID = snapshots[0].snapshotID;
act(() => gotoSnapshot(snapshots[0].snapshot)); // Test changing state after going to a previous snapshot uses a new version
expectedSnapshotID = null;
act(() => setAtom(3)); // Test mutating a snapshot creates a new version
const transactionSnapshot = snapshots[0].snapshot.map(({
set
}) => {
set(myAtom, 4);
});
act(() => gotoSnapshot(transactionSnapshot));
expect(seenIDs.size).toBe(4);
expect(snapshots.length).toBe(5); // Test that added read-only selector doesn't cause an issue getting the
// current version to see the current deps of the selector since we mutated a
// state after going to a snapshot, so that version may not be known by the store.
// If there was a problem, then the component may throw an error when evaluating the selector.
expect(c.textContent).toBe('44');
});
testRecoil('Read default loadable from snapshot', () => {
const snapshot: Snapshot = freshSnapshot();
const myAtom = atom({
key: 'Snapshot Atom Default',
default: 'DEFAULT'
});
const atomLoadable = snapshot.getLoadable(myAtom);
expect(atomLoadable.state).toEqual('hasValue');
expect(atomLoadable.contents).toEqual('DEFAULT');
const mySelector = constSelector(myAtom);
const selectorLoadable = snapshot.getLoadable(mySelector);
expect(selectorLoadable.state).toEqual('hasValue');
expect(selectorLoadable.contents).toEqual('DEFAULT');
});
testRecoil('Read async selector from snapshot', async () => {
const snapshot = freshSnapshot();
const otherA = freshSnapshot();
const otherB = freshSnapshot();
const [asyncSel, resolve] = asyncSelector();
const nestSel = constSelector(asyncSel);
expect(snapshot.getLoadable(asyncSel).state).toEqual('loading');
expect(snapshot.getLoadable(nestSel).state).toEqual('loading');
expect(otherA.getLoadable(nestSel).state).toEqual('loading');
const otherC = snapshot.map(() => {}); // eslint-disable-next-line jest/valid-expect
const ptest = expect(snapshot.getPromise(asyncSel)).resolves.toEqual('SET VALUE');
act(() => resolve('SET VALUE'));
await ptest;
await expect(snapshot.getPromise(asyncSel)).resolves.toEqual('SET VALUE');
expect(snapshot.getLoadable(asyncSel).contents).toEqual('SET VALUE');
await expect(snapshot.getPromise(nestSel)).resolves.toEqual('SET VALUE');
await expect(otherA.getPromise(nestSel)).resolves.toEqual('SET VALUE');
await expect(otherB.getPromise(nestSel)).resolves.toEqual('SET VALUE');
await expect(otherC.getPromise(nestSel)).resolves.toEqual('SET VALUE');
});
testRecoil('Sync map of snapshot', () => {
const snapshot = freshSnapshot();
const myAtom = atom({
key: 'Snapshot Map Sync',
default: 'DEFAULT'
});
const mySelector = constSelector(myAtom);
const atomLoadable = snapshot.getLoadable(myAtom);
expect(atomLoadable.state).toEqual('hasValue');
expect(atomLoadable.contents).toEqual('DEFAULT');
const selectorLoadable = snapshot.getLoadable(mySelector);
expect(selectorLoadable.state).toEqual('hasValue');
expect(selectorLoadable.contents).toEqual('DEFAULT');
const setSnapshot = snapshot.map(({
set
}) => {
set(myAtom, 'SET');
});
const setAtomLoadable = setSnapshot.getLoadable(myAtom);
expect(setAtomLoadable.state).toEqual('hasValue');
expect(setAtomLoadable.contents).toEqual('SET');
const setSelectorLoadable = setSnapshot.getLoadable(myAtom);
expect(setSelectorLoadable.state).toEqual('hasValue');
expect(setSelectorLoadable.contents).toEqual('SET');
const resetSnapshot = snapshot.map(({
reset
}) => {
reset(myAtom);
});
const resetAtomLoadable = resetSnapshot.getLoadable(myAtom);
expect(resetAtomLoadable.state).toEqual('hasValue');
expect(resetAtomLoadable.contents).toEqual('DEFAULT');
const resetSelectorLoadable = resetSnapshot.getLoadable(myAtom);
expect(resetSelectorLoadable.state).toEqual('hasValue');
expect(resetSelectorLoadable.contents).toEqual('DEFAULT');
});
testRecoil('Async map of snapshot', async () => {
const snapshot = freshSnapshot();
const myAtom = atom({
key: 'Snapshot Map Async',
default: 'DEFAULT'
});
const [asyncSel, resolve] = asyncSelector();
const newSnapshotPromise = snapshot.asyncMap(async ({
getPromise,
set
}) => {
const value = await getPromise(asyncSel);
expect(value).toEqual('VALUE');
set(myAtom, value);
});
act(() => resolve('VALUE'));
const newSnapshot = await newSnapshotPromise;
const value = await newSnapshot.getPromise(myAtom);
expect(value).toEqual('VALUE');
});
testRecoil('getDeps', () => {
const snapshot = freshSnapshot();
const myAtom = atom<string>({
key: 'snapshot getDeps atom',
default: 'ATOM'
});
const selectorA = selector({
key: 'getDepsA',
get: ({
get
}) => get(myAtom)
});
const selectorB = selector({
key: 'getDepsB',
get: ({
get
}) => get(selectorA) + get(myAtom)
});
const selectorC = selector({
key: 'getDepsC',
get: async ({
get
}) => {
const ret = get(selectorA) + get(selectorB);
await Promise.resolve();
return ret;
}
});
expect(Array.from(snapshot.getDeps_UNSTABLE(myAtom))).toEqual([]);
expect(Array.from(snapshot.getDeps_UNSTABLE(selectorA))).toEqual(expect.arrayContaining([myAtom]));
expect(Array.from(snapshot.getDeps_UNSTABLE(selectorB))).toEqual(expect.arrayContaining([selectorA, myAtom]));
expect(Array.from(snapshot.getDeps_UNSTABLE(selectorC))).toEqual(expect.arrayContaining([selectorA, selectorB]));
});
describe('getSubscriptions', () => {
testRecoil('nodes', () => {
const snapshot = freshSnapshot();
const myAtom = atom<string>({
key: 'snapshot getSubscriptions atom',
default: 'ATOM'
});
const selectorA = selector({
key: 'getSubscriptions A',
get: ({
get
}) => get(myAtom)
});
const selectorB = selector({
key: 'getSubscriptions B',
get: ({
get
}) => get(selectorA) + get(myAtom)
});
const selectorC = selector({
key: 'getSubscriptions C',
get: async ({
get
}) => {
const ret = get(selectorA) + get(selectorB);
await Promise.resolve();
return ret;
}
}); // No initial subscribers
expect(Array.from(snapshot.getSubscribers_UNSTABLE(myAtom).nodes)).toEqual([]);
expect(Array.from(snapshot.getSubscribers_UNSTABLE(selectorC).nodes)).toEqual([]); // Evaluate selectorC to update all of its upstream node subscriptions
snapshot.getLoadable(selectorC);
expect(Array.from(snapshot.getSubscribers_UNSTABLE(myAtom).nodes)).toEqual(expect.arrayContaining([selectorA, selectorB, selectorC]));
expect(Array.from(snapshot.getSubscribers_UNSTABLE(selectorA).nodes)).toEqual(expect.arrayContaining([selectorB, selectorC]));
expect(Array.from(snapshot.getSubscribers_UNSTABLE(selectorB).nodes)).toEqual(expect.arrayContaining([selectorC]));
expect(Array.from(snapshot.getSubscribers_UNSTABLE(selectorC).nodes)).toEqual([]);
});
});
testRecoil('getInfo', () => {
const snapshot = freshSnapshot();
const myAtom = atom<string>({
key: 'snapshot getInfo atom',
default: 'DEFAULT'
});
const selectorA = selector({
key: 'getInfo A',
get: ({
get
}) => get(myAtom)
});
const selectorB = selector({
key: 'getInfo B',
get: ({
get
}) => get(selectorA) + get(myAtom)
}); // Initial status
expect(snapshot.getInfo_UNSTABLE(myAtom)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULT'
}),
isActive: false,
isSet: false,
isModified: false,
type: undefined
});
expect(Array.from(snapshot.getInfo_UNSTABLE(myAtom).deps)).toEqual([]);
expect(Array.from(snapshot.getInfo_UNSTABLE(myAtom).subscribers.nodes)).toEqual([]);
expect(snapshot.getInfo_UNSTABLE(selectorA)).toMatchObject({
loadable: undefined,
isActive: false,
isSet: false,
isModified: false,
type: undefined
});
expect(Array.from(snapshot.getInfo_UNSTABLE(selectorA).deps)).toEqual([]);
expect(Array.from(snapshot.getInfo_UNSTABLE(selectorA).subscribers.nodes)).toEqual([]);
expect(snapshot.getInfo_UNSTABLE(selectorB)).toMatchObject({
loadable: undefined,
isActive: false,
isSet: false,
isModified: false,
type: undefined
});
expect(Array.from(snapshot.getInfo_UNSTABLE(selectorB).deps)).toEqual([]);
expect(Array.from(snapshot.getInfo_UNSTABLE(selectorB).subscribers.nodes)).toEqual([]); // After reading values
snapshot.getLoadable(selectorB);
expect(snapshot.getInfo_UNSTABLE(myAtom)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULT'
}),
isActive: true,
isSet: false,
isModified: false,
type: 'atom'
});
expect(Array.from(snapshot.getInfo_UNSTABLE(myAtom).deps)).toEqual([]);
expect(Array.from(snapshot.getInfo_UNSTABLE(myAtom).subscribers.nodes)).toEqual(expect.arrayContaining([selectorA, selectorB]));
expect(snapshot.getInfo_UNSTABLE(selectorA)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULT'
}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector'
});
expect(Array.from(snapshot.getInfo_UNSTABLE(selectorA).deps)).toEqual(expect.arrayContaining([myAtom]));
expect(Array.from(snapshot.getInfo_UNSTABLE(selectorA).subscribers.nodes)).toEqual(expect.arrayContaining([selectorB]));
expect(snapshot.getInfo_UNSTABLE(selectorB)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULTDEFAULT'
}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector'
});
expect(Array.from(snapshot.getInfo_UNSTABLE(selectorB).deps)).toEqual(expect.arrayContaining([myAtom, selectorA]));
expect(Array.from(snapshot.getInfo_UNSTABLE(selectorB).subscribers.nodes)).toEqual([]); // After setting a value
const setSnapshot = snapshot.map(({
set
}) => set(myAtom, 'SET'));
setSnapshot.getLoadable(selectorB); // Read value to prime
expect(setSnapshot.getInfo_UNSTABLE(myAtom)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'SET'
}),
isActive: true,
isSet: true,
isModified: true,
type: 'atom'
});
expect(Array.from(setSnapshot.getInfo_UNSTABLE(myAtom).deps)).toEqual([]);
expect(Array.from(setSnapshot.getInfo_UNSTABLE(myAtom).subscribers.nodes)).toEqual(expect.arrayContaining([selectorA, selectorB]));
expect(setSnapshot.getInfo_UNSTABLE(selectorA)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'SET'
}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector'
});
expect(Array.from(setSnapshot.getInfo_UNSTABLE(selectorA).deps)).toEqual(expect.arrayContaining([myAtom]));
expect(Array.from(setSnapshot.getInfo_UNSTABLE(selectorA).subscribers.nodes)).toEqual(expect.arrayContaining([selectorB]));
expect(setSnapshot.getInfo_UNSTABLE(selectorB)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'SETSET'
}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector'
});
expect(Array.from(setSnapshot.getInfo_UNSTABLE(selectorB).deps)).toEqual(expect.arrayContaining([myAtom, selectorA]));
expect(Array.from(setSnapshot.getInfo_UNSTABLE(selectorB).subscribers.nodes)).toEqual([]); // After reseting a value
const resetSnapshot = setSnapshot.map(({
reset
}) => reset(myAtom));
resetSnapshot.getLoadable(selectorB); // prime snapshot
expect(resetSnapshot.getInfo_UNSTABLE(myAtom)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULT'
}),
isActive: true,
isSet: false,
isModified: true,
type: 'atom'
});
expect(Array.from(resetSnapshot.getInfo_UNSTABLE(myAtom).deps)).toEqual([]);
expect(Array.from(resetSnapshot.getInfo_UNSTABLE(myAtom).subscribers.nodes)).toEqual(expect.arrayContaining([selectorA, selectorB]));
expect(resetSnapshot.getInfo_UNSTABLE(selectorA)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULT'
}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector'
});
expect(Array.from(resetSnapshot.getInfo_UNSTABLE(selectorA).deps)).toEqual(expect.arrayContaining([myAtom]));
expect(Array.from(resetSnapshot.getInfo_UNSTABLE(selectorA).subscribers.nodes)).toEqual(expect.arrayContaining([selectorB]));
expect(resetSnapshot.getInfo_UNSTABLE(selectorB)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULTDEFAULT'
}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector'
});
expect(Array.from(resetSnapshot.getInfo_UNSTABLE(selectorB).deps)).toEqual(expect.arrayContaining([myAtom, selectorA]));
expect(Array.from(resetSnapshot.getInfo_UNSTABLE(selectorB).subscribers.nodes)).toEqual([]);
});