UNPKG

recoil

Version:

Recoil - A state management library for React

693 lines (680 loc) 18 kB
/** * 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'; 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