recoil
Version:
Recoil - A state management library for React
986 lines (963 loc) • 28.7 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 { Loadable } from '../../adt/Recoil_Loadable';
import type { RecoilValue } from '../../core/Recoil_RecoilValue';
const {
getRecoilTestFn
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let store, atom, noWait, act, constSelector, errorSelector, getRecoilValueAsLoadable, setRecoilValue, selector, asyncSelector, resolvingAsyncSelector, flushPromisesAndTimers, DefaultValue, freshSnapshot;
const testRecoil = getRecoilTestFn(() => {
const {
makeStore
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
({
act
} = require('ReactTestUtils'));
atom = require('../Recoil_atom');
constSelector = require('../Recoil_constSelector');
errorSelector = require('../Recoil_errorSelector');
({
getRecoilValueAsLoadable,
setRecoilValue
} = require('../../core/Recoil_RecoilValueInterface'));
selector = require('../Recoil_selector');
({
freshSnapshot
} = require('../../core/Recoil_Snapshot'));
({
asyncSelector,
resolvingAsyncSelector,
flushPromisesAndTimers
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({
noWait
} = require('../Recoil_WaitFor'));
({
DefaultValue
} = require('../../core/Recoil_Node'));
store = makeStore();
});
declare function getLoadable<T>(recoilValue: RecoilValue<T>): Loadable<T>;
declare function getValue<T>(recoilValue: RecoilValue<T>): T;
declare function getPromise<T>(recoilValue: RecoilValue<T>): Promise<T>;
declare function getError(recoilValue: any): Error;
declare function setValue(recoilState: any, value: any): any;
declare function resetValue(recoilState: any): any;
testRecoil('selector get', () => {
const staticSel = constSelector('HELLO');
const selectorRO = selector({
key: 'selector/get',
get: ({
get
}) => get(staticSel)
});
expect(getValue(selectorRO)).toEqual('HELLO');
});
testRecoil('selector set', () => {
const myAtom = atom({
key: 'selector/set/atom',
default: 'DEFAULT'
});
const selectorRW = selector({
key: 'selector/set',
get: ({
get
}) => get(myAtom),
set: ({
set
}, newValue) => set(myAtom, newValue instanceof DefaultValue ? newValue : 'SET: ' + newValue)
});
expect(getValue(selectorRW)).toEqual('DEFAULT');
setValue(myAtom, 'SET ATOM');
expect(getValue(selectorRW)).toEqual('SET ATOM');
setValue(selectorRW, 'SET SELECTOR');
expect(getValue(selectorRW)).toEqual('SET: SET SELECTOR');
resetValue(selectorRW);
expect(getValue(selectorRW)).toEqual('DEFAULT');
});
testRecoil('selector reset', () => {
const myAtom = atom({
key: 'selector/reset/atom',
default: 'DEFAULT'
});
const selectorRW = selector({
key: 'selector/reset',
get: ({
get
}) => get(myAtom),
set: ({
reset
}) => reset(myAtom)
});
expect(getValue(selectorRW)).toEqual('DEFAULT');
setValue(myAtom, 'SET ATOM');
expect(getValue(selectorRW)).toEqual('SET ATOM');
setValue(selectorRW, 'SET SELECTOR');
expect(getValue(selectorRW)).toEqual('DEFAULT');
});
testRecoil('selector - evaluate to RecoilValue', () => {
const atomA = atom({
key: 'selector/const atom A',
default: 'A'
});
const atomB = atom({
key: 'selector/const atom B',
default: 'B'
});
const inputAtom = atom({
key: 'selector/input',
default: 'a'
});
const mySelector = selector<string>({
key: 'selector/output recoil value',
get: ({
get
}) => get(inputAtom) === 'a' ? atomA : atomB
});
expect(getValue(mySelector)).toEqual('A');
setValue(inputAtom, 'b');
expect(getValue(mySelector)).toEqual('B');
});
describe('Catching Deps', () => {
testRecoil('selector - catching exceptions', () => {
const throwingSel = errorSelector('MY ERROR');
expect(getValue(throwingSel)).toBeInstanceOf(Error);
const catchingSelector = selector({
key: 'selector/catching selector',
get: ({
get
}) => {
try {
return get(throwingSel);
} catch (e) {
expect(e instanceof Error).toBe(true);
expect(e.message).toContain('MY ERROR');
return 'CAUGHT';
}
}
});
expect(getValue(catchingSelector)).toEqual('CAUGHT');
});
testRecoil('selector - catching exception (non Error)', () => {
const throwingSel = selector({
key: '__error/non Error thrown',
get: () => {
// eslint-disable-next-line no-throw-literal
throw 'MY ERROR';
}
});
const catchingSelector = selector({
key: 'selector/catching selector',
get: ({
get
}) => {
try {
return get(throwingSel);
} catch (e) {
expect(e).toBe('MY ERROR');
return 'CAUGHT';
}
}
});
expect(getValue(catchingSelector)).toEqual('CAUGHT');
});
testRecoil('selector - catching loads', () => {
const loadingSel = resolvingAsyncSelector('READY');
expect(getValue(loadingSel) instanceof Promise).toBe(true);
const blockedSelector = selector({
key: 'selector/blocked selector',
get: ({
get
}) => get(loadingSel)
});
expect(getValue(blockedSelector) instanceof Promise).toBe(true);
const bypassSelector = selector({
key: 'selector/bypassing selector',
get: ({
get
}) => {
try {
return get(loadingSel);
} catch (promise) {
expect(promise instanceof Promise).toBe(true);
return 'BYPASS';
}
}
});
expect(getValue(bypassSelector)).toBe('BYPASS');
act(() => jest.runAllTimers());
expect(getValue(bypassSelector)).toEqual('READY');
});
});
describe('Dependencies', () => {
// Test that Recoil will throw an error with a useful debug message instead of
// infinite recurssion when there is a circular dependency
testRecoil('Detect circular dependencies', () => {
const selectorA = selector({
key: 'circular/A',
get: ({
get
}) => get(selectorC)
});
const selectorB = selector({
key: 'circular/B',
get: ({
get
}) => get(selectorA)
});
const selectorC = selector({
key: 'circular/C',
get: ({
get
}) => get(selectorB)
});
const devStatus = window.__DEV__;
window.__DEV__ = true;
expect(getValue(selectorC)).toBeInstanceOf(Error);
expect(getError(selectorC).message).toEqual(expect.stringContaining('circular/A'));
window.__DEV__ = devStatus;
});
testRecoil('distinct loading dependencies are treated as distinct', async () => {
const upstreamAtom = atom({
key: 'distinct loading dependencies/upstreamAtom',
default: 0
});
const upstreamAsyncSelector = selector({
key: 'distinct loading dependencies/upstreamAsyncSelector',
get: ({
get
}) => Promise.resolve(get(upstreamAtom))
});
const directSelector = selector({
key: 'distinct loading dependencies/directSelector',
get: ({
get
}) => get(upstreamAsyncSelector)
});
expect(getValue(directSelector) instanceof Promise).toBe(true);
act(() => jest.runAllTimers());
expect(getValue(directSelector)).toEqual(0);
setValue(upstreamAtom, 1);
expect(getValue(directSelector) instanceof Promise).toBe(true);
act(() => jest.runAllTimers());
expect(getValue(directSelector)).toEqual(1);
});
testRecoil('selector - kite pattern runs only necessary selectors', async () => {
const aNode = atom({
key: 'aNode',
default: true
});
let bNodeRunCount = 0;
const bNode = selector({
key: 'bNode',
get: ({
get
}) => {
bNodeRunCount++;
const a = get(aNode);
return String(a);
}
});
let cNodeRunCount = 0;
const cNode = selector({
key: 'cNode',
get: ({
get
}) => {
cNodeRunCount++;
const a = get(aNode);
return String(Number(a));
}
});
let dNodeRunCount = 0;
const dNode = selector({
key: 'dNode',
get: ({
get
}) => {
dNodeRunCount++;
const value = get(aNode) ? get(bNode) : get(cNode);
return value.toUpperCase();
}
});
let dNodeValue = getValue(dNode);
expect(dNodeValue).toEqual('TRUE');
expect(bNodeRunCount).toEqual(1);
expect(cNodeRunCount).toEqual(0);
expect(dNodeRunCount).toEqual(1);
setValue(aNode, false);
dNodeValue = getValue(dNode);
expect(dNodeValue).toEqual('0');
expect(bNodeRunCount).toEqual(1);
expect(cNodeRunCount).toEqual(1);
expect(dNodeRunCount).toEqual(2);
});
testRecoil('selector does not re-run to completion when one of its async deps resolves to a previously cached value', async () => {
const testSnapshot = freshSnapshot();
testSnapshot.retain();
const atomA = atom({
key: 'atomc-rerun-opt-test',
default: -3
});
const selectorB = selector({
key: 'selb-rerun-opt-test',
get: async ({
get
}) => {
await Promise.resolve();
return Math.abs(get(atomA));
}
});
let numTimesCStartedToRun = 0;
let numTimesCRanToCompletion = 0;
const selectorC = selector({
key: 'sela-rerun-opt-test',
get: ({
get
}) => {
numTimesCStartedToRun++;
const ret = get(selectorB);
/**
* The placement of numTimesCRan is important as this optimization
* prevents the execution of selectorC _after_ the point where the
* selector execution hits a known path of dependencies. In other words,
* the lines prior to the get(selectorB) will run twice, but the lines
* following get(selectorB) should only run once given that we are
* setting up this test so that selectorB resolves to a previously seen
* value the second time that it runs.
*/
numTimesCRanToCompletion++;
return ret;
}
});
testSnapshot.getLoadable(selectorC);
/**
* Run selector chain so that selectorC is cached with a dep of selectorB
* set to "3"
*/
await flushPromisesAndTimers();
const loadableA = testSnapshot.getLoadable(selectorC);
expect(loadableA.contents).toBe(3);
expect(numTimesCRanToCompletion).toBe(1);
/**
* It's expected that C started to run twice so far (the first is the first
* time that the selector was called and suspended, the second was when B
* resolved and C re-ran because of the async dep resolution)
*/
expect(numTimesCStartedToRun).toBe(2);
const mappedSnapshot = testSnapshot.map(({
set
}) => {
set(atomA, 3);
});
mappedSnapshot.getLoadable(selectorC);
/**
* Run selector chain so that selectorB recalculates as a result of atomA
* being changed to "3"
*/
mappedSnapshot.retain();
await flushPromisesAndTimers();
const loadableB = mappedSnapshot.getLoadable(selectorC);
expect(loadableB.contents).toBe(3);
/**
* If selectors are correctly optimized, selectorC will not re-run because
* selectorB resolved to "3", which is a value that selectorC has previously
* cached for its selectorB dependency.
*/
expect(numTimesCRanToCompletion).toBe(1);
/**
* TODO:
* in the ideal case this should be:
*
* expect(numTimesCStartedToRun).toBe(2);
*/
expect(numTimesCStartedToRun).toBe(3);
});
testRecoil('async dep that changes from loading to value triggers re-execution', async () => {
const baseAtom = atom({
key: 'baseAtom',
default: 0
});
const asyncSel = selector({
key: 'asyncSel',
get: ({
get
}) => {
const atomVal = get(baseAtom);
if (atomVal === 0) {
return new Promise(() => {});
}
return atomVal;
}
});
const selWithAsyncDep = selector({
key: 'selWithAsyncDep',
get: ({
get
}) => {
return get(asyncSel);
}
});
const snapshot = freshSnapshot();
snapshot.retain();
const loadingValLoadable = snapshot.getLoadable(selWithAsyncDep);
expect(loadingValLoadable.state).toBe('loading');
const mappedSnapshot = snapshot.map(({
set
}) => {
set(baseAtom, 10);
});
const newAtomVal = mappedSnapshot.getLoadable(baseAtom);
expect(newAtomVal.valueMaybe()).toBe(10);
const valLoadable = mappedSnapshot.getLoadable(selWithAsyncDep);
expect(valLoadable.valueMaybe()).toBe(10);
});
});
testRecoil('async set not supported', async () => {
const myAtom = atom({
key: 'selector / async not supported / other atom',
default: 'DEFAULT'
});
const mySelector = selector({
key: 'selector / async set not supported / async set method',
get: () => myAtom,
// Set should not be async, this test checks that it throws an error.
// $FlowExpectedError
set: async ({
set,
reset
}, newVal) => {
await Promise.resolve();
newVal instanceof DefaultValue ? reset(myAtom) : set(myAtom, 'SET');
}
});
let setAttempt, resetAttempt;
const mySelector2 = selector({
key: 'selector / async set not supported / async upstream call',
get: () => myAtom,
set: ({
set,
reset
}, newVal) => {
if (newVal instanceof DefaultValue) {
resetAttempt = new Promise.resolve().then(() => {
reset(myAtom);
});
} else {
setAttempt = new Promise.resolve().then(() => {
set(myAtom, 'SET');
});
}
}
});
const testSnapshot = freshSnapshot();
testSnapshot.retain();
expect(() => testSnapshot.map(({
set
}) => {
set(mySelector, 'SET');
})).toThrow();
expect(() => testSnapshot.map(({
reset
}) => {
reset(mySelector);
})).toThrow();
const setSnapshot = testSnapshot.map(({
set,
reset
}) => {
set(mySelector2, 'SET');
reset(mySelector2);
});
setSnapshot.retain();
await flushPromisesAndTimers();
expect(setSnapshot.getLoadable(mySelector2).contents).toEqual('DEFAULT');
await expect(setAttempt).rejects.toThrowError();
await expect(resetAttempt).rejects.toThrowError();
});
describe('User-thrown promises', () => {
testRecoil('selectors with user-thrown loadable promises execute to completion as expected', async () => {
const [asyncDep, resolveAsyncDep] = asyncSelector<string, void>();
const selWithUserThrownPromise = selector({
key: 'selWithUserThrownPromise',
get: ({
get
}) => {
const loadable = get(noWait(asyncDep));
if (loadable.state === 'loading') {
throw loadable.toPromise();
}
return loadable.valueOrThrow();
}
});
const loadable = getLoadable(selWithUserThrownPromise);
const promise = loadable.toPromise();
expect(loadable.state).toBe('loading');
resolveAsyncDep('RESOLVED');
await flushPromisesAndTimers();
const val: mixed = await promise;
expect(val).toBe('RESOLVED');
});
testRecoil('selectors with user-thrown loadable promises execute to completion as expected', async () => {
const myAtomA = atom({
key: 'myatoma selectors user-thrown promise',
default: 'A'
});
const myAtomB = atom({
key: 'myatomb selectors user-thrown promise',
default: 'B'
});
let isResolved = false;
declare var resolve: () => any;
const myPromise = new Promise(localResolve => {
resolve = () => {
isResolved = true;
localResolve();
};
});
const selWithUserThrownPromise = selector({
key: 'selWithUserThrownPromise',
get: ({
get
}) => {
const a = get(myAtomA);
if (!isResolved) {
throw myPromise;
}
const b = get(myAtomB);
return `${a}${b}`;
}
});
const loadable = getLoadable(selWithUserThrownPromise);
const promise = loadable.toPromise();
expect(loadable.state).toBe('loading');
resolve();
await flushPromisesAndTimers();
const val: mixed = await promise;
expect(val).toBe('AB');
});
testRecoil('selectors with nested user-thrown loadable promises execute to completion as expected', async () => {
const [asyncDep, resolveAsyncDep] = asyncSelector<string, void>();
const selWithUserThrownPromise = selector({
key: 'selWithUserThrownPromise',
get: ({
get
}) => {
const loadable = get(noWait(asyncDep));
if (loadable.state === 'loading') {
throw loadable.toPromise();
}
return loadable.valueOrThrow();
}
});
const selThatDependsOnSelWithUserThrownPromise = selector({
key: 'selThatDependsOnSelWithUserThrownPromise',
get: ({
get
}) => get(selWithUserThrownPromise)
});
const loadable = getLoadable(selThatDependsOnSelWithUserThrownPromise);
const promise = loadable.toPromise();
expect(loadable.state).toBe('loading');
resolveAsyncDep('RESOLVED');
await flushPromisesAndTimers();
const val: mixed = await promise;
expect(val).toBe('RESOLVED');
});
});
testRecoil('selectors cannot mutate values in get() or set()', () => {
const devStatus = window.__DEV__;
window.__DEV__ = true;
const userState = atom({
key: 'userState',
default: {
name: 'john',
address: {
road: '103 road',
nested: {
value: 'someNestedValue'
}
}
}
});
const userSelector = selector({
key: 'userSelector',
get: ({
get
}) => {
const user = get(userState);
user.address.road = '301 road';
return user;
},
set: ({
set,
get
}) => {
const user = get(userState);
user.address.road = 'narrow road';
return set(userState, user);
}
});
const testSnapshot = freshSnapshot();
testSnapshot.retain();
expect(() => testSnapshot.map(({
set
}) => {
set(userSelector, {
name: 'matt',
address: {
road: '103 road',
nested: {
value: 'someNestedValue'
}
}
});
})).toThrow();
expect(testSnapshot.getLoadable(userSelector).state).toBe('hasError');
window.__DEV__ = devStatus;
});
describe('getCallback', () => {
testRecoil('Selector getCallback', async () => {
const myAtom = atom({
key: 'selector - getCallback atom',
default: 'DEFAULT'
});
const mySelector = selector({
key: 'selector - getCallback',
get: ({
getCallback
}) => {
return {
onClick: getCallback(({
snapshot
}) => async () => await snapshot.getPromise(myAtom))
};
}
});
const menuItem = getValue(mySelector);
expect(getValue(myAtom)).toEqual('DEFAULT');
await expect(menuItem.onClick()).resolves.toEqual('DEFAULT');
act(() => setValue(myAtom, 'SET'));
expect(getValue(myAtom)).toEqual('SET');
await expect(menuItem.onClick()).resolves.toEqual('SET');
act(() => setValue(myAtom, 'SET2'));
expect(getValue(myAtom)).toEqual('SET2');
await expect(menuItem.onClick()).resolves.toEqual('SET2');
});
testRecoil('snapshot', async () => {
const otherSelector = constSelector('VALUE');
const mySelector = selector({
key: 'selector getCallback snapshot',
get: ({
getCallback
}) => getCallback(({
snapshot
}) => param => ({
param,
loadable: snapshot.getLoadable(otherSelector),
promise: snapshot.getPromise(otherSelector)
}))
});
expect(getValue(mySelector)(123).param).toBe(123);
expect(getValue(mySelector)(123).loadable.getValue()).toBe('VALUE');
await expect(getValue(mySelector)(123).promise).resolves.toBe('VALUE');
});
testRecoil('set', () => {
const myAtom = atom({
key: 'selector getCallback set atom',
default: 'DEFAULT'
});
const setSelector = selector({
key: 'selector getCallback set',
get: ({
getCallback
}) => getCallback(({
set
}) => param => {
set(myAtom, param);
})
});
const resetSelector = selector({
key: 'selector getCallback reset',
get: ({
getCallback
}) => getCallback(({
reset
}) => () => {
reset(myAtom);
})
});
expect(getValue(myAtom)).toBe('DEFAULT');
getValue(setSelector)('SET');
expect(getValue(myAtom)).toBe('SET');
getValue(resetSelector)();
expect(getValue(myAtom)).toBe('DEFAULT');
});
testRecoil('transaction', () => {
const myAtom = atom({
key: 'selector getCallback transact atom',
default: 'DEFAULT'
});
const setSelector = selector({
key: 'selector getCallback transact set',
get: ({
getCallback
}) => getCallback(({
transact_UNSTABLE
}) => param => {
transact_UNSTABLE(({
set,
get
}) => {
expect(get(myAtom)).toBe('DEFAULT');
set(myAtom, 'TMP');
expect(get(myAtom)).toBe('TMP');
set(myAtom, param);
});
})
});
const resetSelector = selector({
key: 'selector getCallback transact',
get: ({
getCallback
}) => getCallback(({
transact_UNSTABLE
}) => () => {
transact_UNSTABLE(({
reset
}) => reset(myAtom));
})
});
expect(getValue(myAtom)).toBe('DEFAULT');
getValue(setSelector)('SET');
expect(getValue(myAtom)).toBe('SET');
getValue(resetSelector)();
expect(getValue(myAtom)).toBe('DEFAULT');
});
testRecoil('node', () => {
const mySelector = selector({
key: 'selector getCallback node',
get: ({
getCallback
}) => getCallback(({
node,
snapshot
}) => param => ({
param,
loadable: snapshot.getLoadable(node),
promise: snapshot.getPromise(node)
}))
});
expect(getValue(mySelector)(123).param).toBe(123);
expect(getValue(mySelector)(123).loadable.getValue()(456).param).toBe(456);
});
testRecoil('refresh', async () => {
let externalValue = 0;
const mySelector = selector({
key: 'selector getCallback node refresh',
get: ({
getCallback
}) => {
const cachedExternalValue = externalValue;
return getCallback(({
node,
refresh
}) => () => ({
cached: cachedExternalValue,
current: externalValue,
refresh: () => refresh(node)
}));
}
});
expect(getValue(mySelector)().current).toBe(0);
expect(getValue(mySelector)().cached).toBe(0);
externalValue = 1;
expect(getValue(mySelector)().current).toBe(1);
expect(getValue(mySelector)().cached).toBe(0);
getValue(mySelector)().refresh();
expect(getValue(mySelector)().current).toBe(1);
expect(getValue(mySelector)().cached).toBe(1);
});
testRecoil('Guard against calling during selector evaluation', async () => {
const mySelector = selector({
key: 'selector getCallback guard',
get: ({
getCallback
}) => {
const callback = getCallback(() => () => {});
expect(() => callback()).toThrow();
return 'THROW';
}
});
expect(getValue(mySelector)).toBe('THROW');
const myAsyncSelector = selector({
key: 'selector getCallback guard async',
get: async ({
getCallback
}) => {
const callback = getCallback(() => () => {});
await Promise.resolve();
expect(() => callback()).toThrow();
return 'THROW';
}
});
await expect(getPromise(myAsyncSelector)).resolves.toBe('THROW');
});
testRecoil('Callback can be used from thrown error', async () => {
const mySelector = selector({
key: 'selector getCallback from error',
get: ({
getCallback
}) => {
// eslint-disable-next-line no-throw-literal
throw {
callback: getCallback(() => x => x)
};
}
}); // $FlowExpectedError[incompatible-use]]
expect(getLoadable(mySelector).errorOrThrow().callback(123)).toEqual(123);
const myAsyncSelector = selector({
key: 'selector getCallback from error async',
get: ({
getCallback
}) => {
return Promise.reject({
callback: getCallback(() => x => x)
});
}
});
await expect(getPromise(myAsyncSelector).catch(({
callback
}) => callback(123))).resolves.toEqual(123);
});
});
testRecoil('Selector values are frozen', async () => {
const devStatus = window.__DEV__;
window.__DEV__ = true;
const frozenSelector = selector({
key: 'selector frozen',
get: () => ({
state: 'frozen',
nested: {
state: 'frozen'
}
})
});
expect(Object.isFrozen(getValue(frozenSelector))).toBe(true);
expect(Object.isFrozen(getValue(frozenSelector).nested)).toBe(true);
const thawedSelector = selector({
key: 'selector frozen thawed',
get: () => ({
state: 'thawed',
nested: {
state: 'thawed'
}
}),
dangerouslyAllowMutability: true
});
expect(Object.isFrozen(getValue(thawedSelector))).toBe(false);
expect(Object.isFrozen(getValue(thawedSelector).nested)).toBe(false);
const asyncFrozenSelector = selector({
key: 'selector frozen async',
get: () => Promise.resolve({
state: 'frozen',
nested: {
state: 'frozen'
}
})
});
await expect(getPromise(asyncFrozenSelector).then(x => Object.isFrozen(x))).resolves.toBe(true);
expect(Object.isFrozen(getValue(asyncFrozenSelector).nested)).toBe(true);
const asyncThawedSelector = selector({
key: 'selector frozen thawed async',
get: () => Promise.resolve({
state: 'thawed',
nested: {
state: 'thawed'
}
}),
dangerouslyAllowMutability: true
});
await expect(getPromise(asyncThawedSelector).then(x => Object.isFrozen(x))).resolves.toBe(false);
expect(Object.isFrozen(getValue(asyncThawedSelector).nested)).toBe(false);
const upstreamFrozenSelector = selector({
key: 'selector frozen upstream',
get: () => ({
state: 'frozen',
nested: {
state: 'frozen'
}
})
});
const fwdFrozenSelector = selector({
key: 'selector frozen fwd',
get: () => upstreamFrozenSelector
});
expect(Object.isFrozen(getValue(fwdFrozenSelector))).toBe(true);
expect(Object.isFrozen(getValue(fwdFrozenSelector).nested)).toBe(true);
const upstreamThawedSelector = selector({
key: 'selector frozen thawed upstream',
get: () => ({
state: 'thawed',
nested: {
state: 'thawed'
}
}),
dangerouslyAllowMutability: true
});
const fwdThawedSelector = selector({
key: 'selector frozen thawed fwd',
get: () => upstreamThawedSelector,
dangerouslyAllowMutability: true
});
expect(Object.isFrozen(getValue(fwdThawedSelector))).toBe(false);
expect(Object.isFrozen(getValue(fwdThawedSelector).nested)).toBe(false); // Selectors should not freeze their upstream dependencies
const upstreamMixedSelector = selector({
key: 'selector frozen mixed upstream',
get: () => ({
state: 'thawed',
nested: {
state: 'thawed'
}
}),
dangerouslyAllowMutability: true
});
const fwdMixedSelector = selector({
key: 'selector frozen mixed fwd',
get: ({
get
}) => {
get(upstreamMixedSelector);
return {
state: 'frozen'
};
}
});
expect(Object.isFrozen(getValue(fwdMixedSelector))).toBe(true);
expect(Object.isFrozen(getValue(upstreamMixedSelector))).toBe(false);
expect(Object.isFrozen(getValue(upstreamMixedSelector).nested)).toBe(false);
window.__DEV__ = devStatus;
});
testRecoil('Required options are provided when creating selectors', () => {
const devStatus = window.__DEV__;
window.__DEV__ = true; // $FlowExpectedError[incompatible-call]
expect(() => selector({
get: () => {}
})).toThrow(); // $FlowExpectedError[incompatible-call]
expect(() => selector({
get: false
})).toThrow(); // $FlowExpectedError[incompatible-call]
expect(() => selector({
key: 'MISSING GET'
})).toThrow();
window.__DEV__ = devStatus;
});