/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Store} from '../Recoil_State'; import type {MutableSnapshot} from 'Recoil_Snapshot'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let React, useState, act, useSetRecoilState, atom, constSelector, selector, asyncSelector, ReadsAtom, componentThatReadsAndWritesAtom, flushPromisesAndTimers, renderElements, renderUnwrappedElements, RecoilRoot, useStoreRef; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({useState} = require('react')); ({act} = require('ReactTestUtils')); ({useSetRecoilState} = require('../../hooks/Recoil_Hooks')); atom = require('../../recoil_values/Recoil_atom'); constSelector = require('../../recoil_values/Recoil_constSelector'); selector = require('../../recoil_values/Recoil_selector'); ({ asyncSelector, ReadsAtom, componentThatReadsAndWritesAtom, flushPromisesAndTimers, renderElements, renderUnwrappedElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); ({RecoilRoot, useStoreRef} = require('../Recoil_RecoilRoot')); }); describe('initializeState', () => { testRecoil('initialize atom', () => { const myAtom = atom({ key: 'RecoilRoot - initializeState - atom', default: 'DEFAULT', }); const mySelector = constSelector(myAtom); function initializeState({set, getLoadable}: MutableSnapshot) { expect(getLoadable(myAtom).contents).toEqual('DEFAULT'); expect(getLoadable(mySelector).contents).toEqual('DEFAULT'); set(myAtom, 'INITIALIZE'); expect(getLoadable(myAtom).contents).toEqual('INITIALIZE'); expect(getLoadable(mySelector).contents).toEqual('INITIALIZE'); } const container = renderElements( , ); expect(container.textContent).toEqual('"INITIALIZE""INITIALIZE"'); }); testRecoil('initialize selector', () => { const myAtom = atom({ key: 'RecoilRoot - initializeState - selector', default: 'DEFAULT', }); // $FlowFixMe[incompatible-call] const mySelector = selector({ key: 'RecoilRoot - initializeState - selector selector', get: ({get}) => get(myAtom), // $FlowFixMe[incompatible-call] set: ({set}, newValue) => set(myAtom, newValue), }); function initializeState({set, getLoadable}: MutableSnapshot) { expect(getLoadable(myAtom).contents).toEqual('DEFAULT'); expect(getLoadable(mySelector).contents).toEqual('DEFAULT'); set(mySelector, 'INITIALIZE'); expect(getLoadable(myAtom).contents).toEqual('INITIALIZE'); expect(getLoadable(mySelector).contents).toEqual('INITIALIZE'); } const container = renderElements( , ); expect(container.textContent).toEqual('"INITIALIZE""INITIALIZE"'); }); testRecoil( 'Atom Effects run with global initialization', async ({strictMode, concurrentMode}) => { let effectRan = 0; let effectCleanup = 0; const myAtom = atom({ key: 'RecoilRoot - initializeState - atom effects', default: 'DEFAULT', effects: [ ({setSelf}) => { effectRan++; setSelf('EFFECT'); return () => { effectCleanup++; }; }, ], }); function initializeState({set}: MutableSnapshot) { set(myAtom, current => { // Effects are run first expect(current).toEqual('EFFECT'); return 'INITIALIZE'; }); } expect(effectRan).toEqual(0); const container1 = renderElements( NO READ, ); // Effects are run when initialized with initializeState, even if not read. // Effects are run twice, once before initializeState, then again when rendering. expect(container1.textContent).toEqual('NO READ'); expect(effectRan).toEqual(strictMode ? (concurrentMode ? 4 : 3) : 2); // Auto-release of the initializing snapshot await flushPromisesAndTimers(); expect(effectCleanup).toEqual(strictMode ? (concurrentMode ? 3 : 2) : 1); // Test again when atom is actually used by the root effectRan = 0; effectCleanup = 0; const container2 = renderElements( , ); // Effects takes precedence expect(container2.textContent).toEqual('"EFFECT"'); expect(effectRan).toEqual(strictMode ? (concurrentMode ? 4 : 3) : 2); await flushPromisesAndTimers(); expect(effectCleanup).toEqual(strictMode ? (concurrentMode ? 3 : 2) : 1); }, ); testRecoil( 'onSet() called when atom initialized with initializeState', () => { const setValues = []; const myAtom = atom({ key: 'RecoilRoot - initializeState - onSet', default: 0, effects: [ ({onSet, setSelf}) => { onSet(value => { setValues.push(value); // Confirm setSelf() works when initialized with initializeState setSelf(value + 1); }); }, ], }); const [MyAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); const c = renderElements( set(myAtom, 1)}> , ); expect(c.textContent).toBe('1'); expect(setValues).toEqual([]); act(() => setAtom(2)); expect(setValues).toEqual([2]); expect(c.textContent).toBe('3'); }, ); testRecoil( 'Selectors from global initialization are not canceled', async () => { const [asyncSel, resolve] = asyncSelector(); // $FlowFixMe[incompatible-call] const depSel = selector({ key: 'RecoilRoot - initializeSTate - async selector', get: ({get}) => get(asyncSel), }); const container = renderUnwrappedElements( { getLoadable(asyncSel); getLoadable(depSel); }}> , ); expect(container.textContent).toEqual('loading'); // Wait for any potential auto-release of initializing snapshot await flushPromisesAndTimers(); // Ensure that async selectors resolve and are not canceled act(() => resolve('RESOLVE')); await flushPromisesAndTimers(); expect(container.textContent).toEqual('"RESOLVE""RESOLVE"'); }, ); testRecoil('initialize with nested store', () => { const GetStore = ({children}: {children: Store => React.Node}) => { return children(useStoreRef().current); }; const container = renderElements( {storeA => ( {storeB => { expect(storeA === storeB).toBe(true); return 'NESTED_ROOT/'; }} )} ROOT , ); expect(container.textContent).toEqual('NESTED_ROOT/ROOT'); }); testRecoil('initializeState is only called once', ({strictMode}) => { if (strictMode) { return; } const myAtom = atom({ key: 'RecoilRoot/override/atom', default: 'DEFAULT', }); const [ReadsWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); const initializeState = jest.fn(({set}) => set(myAtom, 'INIT')); let forceUpdate: () => void = () => { throw new Error('not rendered'); }; let setRootKey: number => void = _ => { throw new Error(''); }; function MyRoot() { const [counter, setCounter] = useState(0); forceUpdate = () => setCounter(counter + 1); const [key, setKey] = useState(0); setRootKey = setKey; return ( {counter} ); } const container = renderElements(); expect(container.textContent).toEqual('0"INIT"'); act(forceUpdate); expect(initializeState).toHaveBeenCalledTimes(1); expect(container.textContent).toEqual('1"INIT"'); act(() => setAtom('SET')); expect(initializeState).toHaveBeenCalledTimes(1); expect(container.textContent).toEqual('1"SET"'); act(forceUpdate); expect(initializeState).toHaveBeenCalledTimes(1); expect(container.textContent).toEqual('2"SET"'); act(() => setRootKey(1)); expect(initializeState).toHaveBeenCalledTimes(2); expect(container.textContent).toEqual('2"INIT"'); }); }); testRecoil( 'Impure state updater functions that trigger atom updates are detected', () => { // This test ensures that we throw a clean error rather than mysterious breakage // if the user supplies a state updater function that triggers another update // within its execution. These state updater functions are supposed to be pure. // We can't detect all forms of impurity but this one in particular will make // Recoil break, so we detect it and throw an error. const atomA = atom({ key: 'RecoilRoot/impureUpdater/a', default: 0, }); const atomB = atom({ key: 'RecoilRoot/impureUpdater/b', default: 0, }); let update; function Component() { const updateA = useSetRecoilState(atomA); const updateB = useSetRecoilState(atomB); update = () => { updateA(() => { updateB(1); return 1; }); }; return null; } renderElements(); expect(() => act(() => { update(); }), ).toThrow('pure function'); }, ); describe('override prop', () => { testRecoil( 'RecoilRoots create a new Recoil scope when override is true or undefined', () => { const myAtom = atom({ key: 'RecoilRoot/override/atom', default: 'DEFAULT', }); const [ReadsWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); const container = renderElements( , ); expect(container.textContent).toEqual('"DEFAULT""DEFAULT"'); act(() => setAtom('SET')); expect(container.textContent).toEqual('"DEFAULT""SET"'); }, ); testRecoil( 'A RecoilRoot performs no function if override is false and it has an ancestor RecoilRoot', () => { const myAtom = atom({ key: 'RecoilRoot/override/atom', default: 'DEFAULT', }); const [ReadsWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); const container = renderElements( , ); expect(container.textContent).toEqual('"DEFAULT""DEFAULT""DEFAULT"'); act(() => setAtom('SET')); expect(container.textContent).toEqual('"SET""SET""SET"'); }, ); testRecoil( 'Unmounting a nested RecoilRoot with override set to false does not clean up ancestor Recoil atoms', () => { const myAtom = atom({ key: 'RecoilRoot/override/atom', default: 'DEFAULT', }); const [ReadsWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); let setRenderNestedRoot; const NestedRootContainer = () => { const [renderNestedRoot, _setRenderNestedRoot] = useState(true); setRenderNestedRoot = _setRenderNestedRoot; return ( renderNestedRoot && ( ) ); }; const container = renderElements( , ); expect(container.textContent).toEqual('"DEFAULT""DEFAULT"'); act(() => setAtom('SET')); act(() => setRenderNestedRoot(false)); expect(container.textContent).toEqual('"SET"'); }, ); testRecoil( 'A RecoilRoot functions normally if override is false and it does not have an ancestor RecoilRoot', () => { const myAtom = atom({ key: 'RecoilRoot/override/atom', default: 'DEFAULT', }); const [ReadsWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); const container = renderElements( , ); expect(container.textContent).toEqual('"DEFAULT"'); act(() => setAtom('SET')); expect(container.textContent).toEqual('"SET"'); }, ); });