UNPKG

recoil

Version:

Recoil - A state management library for React

433 lines (411 loc) 13.5 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'; 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(<Component />); 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(<Component />); 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(<> <Component /> <ReadsAtom atom={anAtom} /> </>); 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(<> <Component /> <ReadsAtom atom={anAtom} /> </>); 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([<Component />, <ReadsAtom atom={anAtom} />]); 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(<Component />); 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(<> <ReadsAtom atom={myAtom} /> <Component /> </>); 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(<> <ReadsAtom atom={myAtom} /> <RecoilCallback /> </>); 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(<> <RecoilCallback /> <GetStore /> </>); 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(<Component />); 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(<Component />); 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(<Component />); 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(<Component />); 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(<Component />); 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(<Component />); 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(<Component />); 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(<Component />); 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); }); });