/** * Copyright (c) Facebook, Inc. and its affiliates. Confidential and proprietary. * * @emails oncall+recoil * @flow strict-local * @format */ 'use strict'; import type { Store } from '../Recoil_State'; 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' }); // $FlowFixMe[incompatible-call] added when improving typing for this parameters const mySelector = constSelector(myAtom); declare function initializeState(arg0: any): any; const container = renderElements( ); expect(container.textContent).toEqual('"INITIALIZE""INITIALIZE"'); }); testRecoil('initialize selector', () => { const myAtom = atom({ key: 'RecoilRoot - initializeState - selector', default: 'DEFAULT' }); const mySelector = selector({ key: 'RecoilRoot - initializeState - selector selector', get: ({ get }) => get(myAtom), set: ({ set }, newValue) => set(myAtom, newValue) }); declare function initializeState(arg0: any): any; 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++; }; }] }); declare function initializeState(arg0: any): any; 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(); 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', () => { declare var GetStore: (arg0: { children: (Store) => React.Node }) => any; 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')); declare var forceUpdate: () => any; declare var setRootKey: (_: any) => any; declare function MyRoot(): any; 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; declare function Component(): any; 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; declare var NestedRootContainer: () => any; 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"'); }); });