UNPKG

recoil

Version:

Recoil - A state management library for React

1,105 lines (1,068 loc) 34.7 kB
/** * Copyright (c) Facebook, Inc. and its affiliates. Confidential and proprietary. * * @emails oncall+recoil * @flow strict-local * @format */ 'use strict'; import type { RecoilValue } from '../../core/Recoil_RecoilValue'; const { getRecoilTestFn } = require('../../__test_utils__/Recoil_TestingUtils'); let React, useState, Profiler, ReactDOM, act, DEFAULT_VALUE, DefaultValue, RecoilRoot, getRecoilValueAsLoadable, setRecoilValue, useRecoilState, useRecoilCallback, useRecoilValue, selector, useRecoilTransactionObserver, useResetRecoilState, ReadsAtom, componentThatReadsAndWritesAtom, flushPromisesAndTimers, renderElements, atom, immutable, store; const testRecoil = getRecoilTestFn(() => { const { makeStore } = require('../../__test_utils__/Recoil_TestingUtils'); React = require('react'); ({ useState, Profiler } = require('react')); ReactDOM = require('ReactDOMLegacy_DEPRECATED'); ({ act } = require('ReactTestUtils')); ({ DEFAULT_VALUE, DefaultValue } = require('../../core/Recoil_Node')); ({ RecoilRoot } = require('../../core/Recoil_RecoilRoot.react')); ({ getRecoilValueAsLoadable, setRecoilValue } = require('../../core/Recoil_RecoilValueInterface')); ({ useRecoilState, useResetRecoilState, useRecoilValue } = require('../../hooks/Recoil_Hooks')); ({ useRecoilTransactionObserver } = require('../../hooks/Recoil_SnapshotHooks')); useRecoilCallback = require('../../hooks/Recoil_useRecoilCallback'); ({ ReadsAtom, componentThatReadsAndWritesAtom, flushPromisesAndTimers, renderElements } = require('../../__test_utils__/Recoil_TestingUtils')); atom = require('../Recoil_atom'); selector = require('../Recoil_selector'); immutable = require('immutable'); store = makeStore(); }); declare function getValue<T>(recoilValue: RecoilValue<T>): T; declare function getRecoilStateLoadable(recoilValue: any): any; declare function getRecoilStatePromise(recoilValue: any): any; declare function set(recoilValue: any, value: mixed): any; declare function reset(recoilValue: any): any; testRecoil('atom can read and write value', () => { const myAtom = atom<string>({ key: 'atom with default', default: 'DEFAULT' }); expect(getValue(myAtom)).toBe('DEFAULT'); act(() => set(myAtom, 'VALUE')); expect(getValue(myAtom)).toBe('VALUE'); }); describe('Valid values', () => { testRecoil('atom can store null and undefined', () => { const myAtom = atom<?string>({ key: 'atom with default for null and undefined', default: 'DEFAULT' }); expect(getValue(myAtom)).toBe('DEFAULT'); act(() => set(myAtom, 'VALUE')); expect(getValue(myAtom)).toBe('VALUE'); act(() => set(myAtom, null)); expect(getValue(myAtom)).toBe(null); act(() => set(myAtom, undefined)); expect(getValue(myAtom)).toBe(undefined); act(() => set(myAtom, 'VALUE')); expect(getValue(myAtom)).toBe('VALUE'); }); testRecoil('atom can store a circular reference object', () => { declare class Circular { self: Circular, constructor(): any, } const circular = new Circular(); const myAtom = atom<?Circular>({ key: 'atom', default: undefined }); expect(getValue(myAtom)).toBe(undefined); act(() => set(myAtom, circular)); expect(getValue(myAtom)).toBe(circular); }); }); describe('Async Defaults', () => { testRecoil('default promise', async () => { const myAtom = atom<string>({ key: 'atom async default', default: Promise.resolve('RESOLVE') }); const container = renderElements(<ReadsAtom atom={myAtom} />); expect(container.textContent).toEqual('loading'); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(container.textContent).toEqual('"RESOLVE"'); }); testRecoil('default promise overwritten before resolution', () => { let resolveAtom; const myAtom = atom<string>({ key: 'atom async default overwritten', default: new Promise(resolve => { resolveAtom = resolve; }) }); const [ReadsWritesAtom, setAtom, resetAtom] = componentThatReadsAndWritesAtom(myAtom); const container = renderElements(<ReadsWritesAtom />); expect(container.textContent).toEqual('loading'); act(() => setAtom('SET')); act(() => jest.runAllTimers()); expect(container.textContent).toEqual('"SET"'); act(() => resolveAtom('RESOLVE')); expect(container.textContent).toEqual('"SET"'); act(() => resetAtom()); act(() => jest.runAllTimers()); expect(container.textContent).toEqual('"RESOLVE"'); }); // NOTE: This test intentionally throws an error testRecoil('default promise rejection', async () => { const myAtom = atom<string>({ key: 'atom async default', default: Promise.reject('REJECT') }); const container = renderElements(<ReadsAtom atom={myAtom} />); expect(container.textContent).toEqual('loading'); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(container.textContent).toEqual('error'); }); }); testRecoil("Updating with same value doesn't rerender", () => { const myAtom = atom({ key: 'atom same value rerender', default: 'DEFAULT' }); let setAtom; let resetAtom; let renders = 0; declare function AtomComponent(): any; expect(renders).toEqual(0); const c = renderElements(<Profiler id="test" onRender={() => { renders++; }}> <AtomComponent /> </Profiler>); // Initial render happens one time in www and 2 times in oss. // resetting the counter to 1 after the initial render to make them // the same in both repos. 2 renders probably need to be looked into. renders = 1; expect(c.textContent).toEqual('DEFAULT'); act(() => setAtom('SET')); expect(renders).toEqual(2); expect(c.textContent).toEqual('SET'); act(() => setAtom('SET')); expect(renders).toEqual(2); expect(c.textContent).toEqual('SET'); act(() => setAtom('CHANGE')); expect(renders).toEqual(3); expect(c.textContent).toEqual('CHANGE'); act(resetAtom); expect(renders).toEqual(4); expect(c.textContent).toEqual('DEFAULT'); act(resetAtom); expect(renders).toEqual(4); expect(c.textContent).toEqual('DEFAULT'); }); describe('Effects', () => { testRecoil('effect error', () => { const ERROR = new Error('ERROR'); const myAtom = atom({ key: 'atom effect error', default: 'DEFAULT', effects_UNSTABLE: [() => { throw ERROR; }] }); const mySelector = selector({ key: 'atom effect error selector', get: ({ get }) => { try { return get(myAtom); } catch (e) { return e.message; } } }); const container = renderElements(<ReadsAtom atom={mySelector} />); expect(container.textContent).toEqual('"ERROR"'); }); testRecoil('initialization', () => { let inited = false; const myAtom = atom({ key: 'atom effect', default: 'DEFAULT', effects_UNSTABLE: [({ node, trigger, setSelf }) => { inited = true; expect(trigger).toEqual('get'); expect(node).toBe(myAtom); setSelf('INIT'); }] }); expect(getValue(myAtom)).toEqual('INIT'); expect(inited).toEqual(true); }); testRecoil('async default', () => { let inited = false; const myAtom = atom<string>({ key: 'atom effect async default', default: Promise.resolve('RESOLVE'), effects_UNSTABLE: [({ setSelf, onSet }) => { inited = true; setSelf('INIT'); onSet(newValue => { expect(newValue).toBe('RESOLVE'); }); }] }); expect(inited).toEqual(false); const [ReadsWritesAtom, _, resetAtom] = componentThatReadsAndWritesAtom(myAtom); const c = renderElements(<ReadsWritesAtom />); expect(inited).toEqual(true); expect(c.textContent).toEqual('"INIT"'); act(resetAtom); expect(c.textContent).toEqual('loading'); act(() => jest.runAllTimers()); expect(c.textContent).toEqual('"RESOLVE"'); }); testRecoil('order of effects', () => { const myAtom = atom({ key: 'atom effect order', default: 'DEFAULT', effects_UNSTABLE: [({ setSelf }) => { setSelf(x => { expect(x).toEqual('DEFAULT'); return 'EFFECT 1a'; }); setSelf(x => { expect(x).toEqual('EFFECT 1a'); return 'EFFECT 1b'; }); }, ({ setSelf }) => { setSelf(x => { expect(x).toEqual('EFFECT 1b'); return 'EFFECT 2'; }); }, () => {}] }); expect(getValue(myAtom)).toEqual('EFFECT 2'); }); testRecoil('reset during init', () => { const myAtom = atom({ key: 'atom effect reset', default: 'DEFAULT', effects_UNSTABLE: [({ setSelf }) => setSelf('INIT'), ({ resetSelf }) => resetSelf()] }); expect(getValue(myAtom)).toEqual('DEFAULT'); }); testRecoil('init to undefined', () => { const myAtom = atom({ key: 'atom effect init undefined', default: 'DEFAULT', effects_UNSTABLE: [({ setSelf }) => setSelf('INIT'), ({ setSelf }) => setSelf()] }); expect(getValue(myAtom)).toEqual(undefined); }); testRecoil('init on set', () => { let inited = 0; const myAtom = atom({ key: 'atom effect - init on set', default: 'DEFAULT', effects_UNSTABLE: [({ setSelf, trigger }) => { inited++; setSelf('INIT'); expect(trigger).toEqual('set'); }] }); set(myAtom, 'SET'); expect(getValue(myAtom)).toEqual('SET'); expect(inited).toEqual(1); reset(myAtom); expect(getValue(myAtom)).toEqual('DEFAULT'); expect(inited).toEqual(1); }); testRecoil('async set', () => { let setAtom, resetAtom; const myAtom = atom({ key: 'atom effect init set', default: 'DEFAULT', effects_UNSTABLE: [({ setSelf, resetSelf }) => { setAtom = setSelf; resetAtom = resetSelf; setSelf(x => { expect(x).toEqual('DEFAULT'); return 'INIT'; }); }] }); const c = renderElements(<ReadsAtom atom={myAtom} />); expect(c.textContent).toEqual('"INIT"'); // Test async set act(() => setAtom(value => { expect(value).toEqual('INIT'); return 'SET'; })); expect(c.textContent).toEqual('"SET"'); // Test async change act(() => setAtom(value => { expect(value).toEqual('SET'); return 'CHANGE'; })); expect(c.textContent).toEqual('"CHANGE"'); // Test reset act(resetAtom); expect(c.textContent).toEqual('"DEFAULT"'); // Test setting to undefined act(() => setAtom(value => { expect(value).toEqual('DEFAULT'); return undefined; })); expect(c.textContent).toEqual(''); }); testRecoil('set promise', async () => { let resolveAtom; let validated; const onSetForSameEffect = jest.fn(() => {}); const myAtom = atom({ key: 'atom effect init set promise', default: 'DEFAULT', effects_UNSTABLE: [({ setSelf, onSet }) => { setSelf(new Promise(resolve => { resolveAtom = resolve; })); onSet(onSetForSameEffect); }, ({ onSet }) => { onSet(value => { expect(value).toEqual('RESOLVE'); validated = true; }); }] }); const c = renderElements(<ReadsAtom atom={myAtom} />); expect(c.textContent).toEqual('loading'); act(() => resolveAtom?.('RESOLVE')); await flushPromisesAndTimers(); act(() => undefined); expect(c.textContent).toEqual('"RESOLVE"'); expect(validated).toEqual(true); // onSet() should not be called for this hook's setSelf() expect(onSetForSameEffect).toHaveBeenCalledTimes(0); }); testRecoil('set default promise', async () => { let setValue = 'RESOLVE_DEFAULT'; const onSetHandler = jest.fn(newValue => { expect(newValue).toBe(setValue); }); let resolveDefault; const myAtom = atom({ key: 'atom effect default promise', default: new Promise(resolve => { resolveDefault = resolve; }), effects_UNSTABLE: [({ onSet }) => { onSet(onSetHandler); }] }); const [ReadsWritesAtom, setAtom, resetAtom] = componentThatReadsAndWritesAtom(myAtom); const c = renderElements(<ReadsWritesAtom />); expect(c.textContent).toEqual('loading'); act(() => resolveDefault?.('RESOLVE_DEFAULT')); await flushPromisesAndTimers(); expect(c.textContent).toEqual('"RESOLVE_DEFAULT"'); expect(onSetHandler).toHaveBeenCalledTimes(1); setValue = 'SET'; act(() => setAtom('SET')); expect(c.textContent).toEqual('"SET"'); expect(onSetHandler).toHaveBeenCalledTimes(2); setValue = 'RESOLVE_DEFAULT'; act(resetAtom); expect(c.textContent).toEqual('"RESOLVE_DEFAULT"'); expect(onSetHandler).toHaveBeenCalledTimes(3); }); testRecoil('when setSelf is called in onSet, then onSet is not triggered again', () => { let set1 = false; const valueToSet1 = 'value#1'; const transformedBySetSelf = 'transformed after value#1'; const myAtom = atom({ key: 'atom setSelf with set-updater', default: 'DEFAULT', effects_UNSTABLE: [({ setSelf, onSet }) => { onSet(newValue => { expect(set1).toBe(false); if (newValue === valueToSet1) { setSelf(transformedBySetSelf); set1 = true; } }); }] }); const [ReadsWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); const c = renderElements(<ReadsWritesAtom />); expect(c.textContent).toEqual('"DEFAULT"'); act(() => setAtom(valueToSet1)); expect(c.textContent).toEqual(`"${transformedBySetSelf}"`); }); // NOTE: This test throws an expected error testRecoil('reject promise', async () => { let rejectAtom; let validated = false; const myAtom = atom({ key: 'atom effect init reject promise', default: 'DEFAULT', effects_UNSTABLE: [({ setSelf, onSet }) => { setSelf(new Promise((_resolve, reject) => { rejectAtom = reject; })); onSet(() => { validated = true; }); }] }); const c = renderElements(<ReadsAtom atom={myAtom} />); expect(c.textContent).toEqual('loading'); act(() => rejectAtom?.(new Error('REJECT'))); await flushPromisesAndTimers(); act(() => undefined); expect(c.textContent).toEqual('error'); expect(validated).toEqual(false); }); testRecoil('overwrite promise', async () => { let resolveAtom; let validated; const myAtom = atom({ key: 'atom effect init overwrite promise', default: 'DEFAULT', effects_UNSTABLE: [({ setSelf, onSet }) => { setSelf(new Promise(resolve => { resolveAtom = resolve; })); onSet(value => { expect(value).toEqual('OVERWRITE'); validated = true; }); }] }); const [ReadsWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); const c = renderElements(<ReadsWritesAtom />); expect(c.textContent).toEqual('loading'); act(() => setAtom('OVERWRITE')); await flushPromisesAndTimers(); expect(c.textContent).toEqual('"OVERWRITE"'); // Resolving after atom is set to another value will be ignored. act(() => resolveAtom?.('RESOLVE')); await flushPromisesAndTimers(); expect(c.textContent).toEqual('"OVERWRITE"'); expect(validated).toEqual(true); }); testRecoil('abort promise init', async () => { let resolveAtom; let validated; const myAtom = atom({ key: 'atom effect abort promise init', default: 'DEFAULT', effects_UNSTABLE: [({ setSelf, onSet }) => { setSelf(new Promise(resolve => { resolveAtom = resolve; })); onSet(value => { expect(value).toBe('DEFAULT'); validated = true; }); }] }); const c = renderElements(<ReadsAtom atom={myAtom} />); expect(c.textContent).toEqual('loading'); act(() => resolveAtom?.(new DefaultValue())); await flushPromisesAndTimers(); act(() => undefined); expect(c.textContent).toEqual('"DEFAULT"'); expect(validated).toEqual(true); }); testRecoil('once per root', () => { let inited = 0; const myAtom = atom({ key: 'atom effect once per root', default: 'DEFAULT', effects_UNSTABLE: [({ setSelf }) => { inited++; setSelf('INIT'); }] }); const [ReadsWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom); // effect is called once per <RecoilRoot> const c1 = renderElements(<ReadsWritesAtom />); const c2 = renderElements(<ReadsAtom atom={myAtom} />); expect(c1.textContent).toEqual('"INIT"'); expect(c2.textContent).toEqual('"INIT"'); act(() => setAtom('SET')); expect(c1.textContent).toEqual('"SET"'); expect(c2.textContent).toEqual('"INIT"'); expect(inited).toEqual(2); }); testRecoil('onSet', () => { const oldSets = { a: 0, b: 0 }; const newSets = { a: 0, b: 0 }; declare var observer: (key: any) => any; const atomA = atom({ key: 'atom effect onSet A', default: 0, effects_UNSTABLE: [({ onSet }) => onSet(observer('a'))] }); const atomB = atom({ key: 'atom effect onSet B', default: 0, effects_UNSTABLE: [({ onSet }) => onSet(observer('b'))] }); const [AtomA, setA, resetA] = componentThatReadsAndWritesAtom(atomA); const [AtomB, setB] = componentThatReadsAndWritesAtom(atomB); const c = renderElements(<> <AtomA /> <AtomB /> </>); expect(oldSets).toEqual({ a: 0, b: 0 }); expect(c.textContent).toEqual('00'); newSets.a = 1; act(() => setA(1)); expect(c.textContent).toEqual('10'); newSets.a = 2; act(() => setA(2)); expect(c.textContent).toEqual('20'); newSets.b = 1; act(() => setB(1)); expect(c.textContent).toEqual('21'); newSets.a = 0; act(() => resetA()); expect(c.textContent).toEqual('01'); }); testRecoil('onSet ordering', () => { let set1 = false; let set2 = false; let globalObserver = false; const myAtom = atom({ key: 'atom effect onSet ordering', default: 'DEFAULT', effects_UNSTABLE: [({ onSet }) => { onSet(() => { expect(set2).toBe(false); set1 = true; }); onSet(() => { expect(set1).toBe(true); set2 = true; }); }] }); declare function TransactionObserver(arg0: any): any; const [AtomA, setA] = componentThatReadsAndWritesAtom(myAtom); const c = renderElements(<> <AtomA /> <TransactionObserver callback={() => { expect(set1).toBe(true); expect(set2).toBe(true); globalObserver = true; }} /> </>); expect(set1).toEqual(false); expect(set2).toEqual(false); act(() => setA(1)); expect(set1).toEqual(true); expect(set2).toEqual(true); expect(globalObserver).toEqual(true); expect(c.textContent).toEqual('1'); }); testRecoil('onSet History', () => { const history: Array<() => void> = []; // Array of undo functions declare function historyEffect(arg0: any): any; const atomA = atom({ key: 'atom effect onSte history A', default: 'DEFAULT_A', effects_UNSTABLE: [historyEffect] }); const atomB = atom({ key: 'atom effect onSte history B', default: 'DEFAULT_B', effects_UNSTABLE: [historyEffect] }); const [AtomA, setA, resetA] = componentThatReadsAndWritesAtom(atomA); const [AtomB, setB] = componentThatReadsAndWritesAtom(atomB); const c = renderElements(<> <AtomA /> <AtomB /> </>); expect(c.textContent).toEqual('"DEFAULT_A""DEFAULT_B"'); act(() => setA('SET_A')); expect(c.textContent).toEqual('"SET_A""DEFAULT_B"'); act(() => setB('SET_B')); expect(c.textContent).toEqual('"SET_A""SET_B"'); act(() => setB('CHANGE_B')); expect(c.textContent).toEqual('"SET_A""CHANGE_B"'); act(resetA); expect(c.textContent).toEqual('"DEFAULT_A""CHANGE_B"'); expect(history.length).toEqual(4); act(() => history.pop()()); expect(c.textContent).toEqual('"SET_A""CHANGE_B"'); act(() => history.pop()()); expect(c.textContent).toEqual('"SET_A""SET_B"'); act(() => history.pop()()); expect(c.textContent).toEqual('"SET_A""DEFAULT_B"'); act(() => history.pop()()); expect(c.textContent).toEqual('"DEFAULT_A""DEFAULT_B"'); }); testRecoil('Cleanup Handlers - when root unmounted', () => { const refCountsA = [0, 0]; const refCountsB = [0, 0]; const atomA = atom({ key: 'atom effect cleanup - A', default: 'A', effects_UNSTABLE: [() => { refCountsA[0]++; return () => { refCountsA[0]--; }; }, () => { refCountsA[1]++; return () => { refCountsA[1]--; }; }] }); const atomB = atom({ key: 'atom effect cleanup - B', default: 'B', effects_UNSTABLE: [() => { refCountsB[0]++; return () => { refCountsB[0]--; }; }, () => { refCountsB[1]++; return () => { refCountsB[1]--; }; }] }); let setNumRoots; declare function App(): any; const c = document.createElement('div'); act(() => { ReactDOM.render(<App />, c); }); expect(c.textContent).toBe(''); expect(refCountsA).toEqual([0, 0]); expect(refCountsB).toEqual([0, 0]); act(() => setNumRoots(1)); expect(c.textContent).toBe('"A""B"'); expect(refCountsA).toEqual([1, 1]); expect(refCountsB).toEqual([1, 1]); act(() => setNumRoots(2)); expect(c.textContent).toBe('"A""B""A""B"'); expect(refCountsA).toEqual([2, 2]); expect(refCountsB).toEqual([2, 2]); act(() => setNumRoots(1)); expect(c.textContent).toBe('"A""B"'); expect(refCountsA).toEqual([1, 1]); expect(refCountsB).toEqual([1, 1]); act(() => setNumRoots(0)); expect(c.textContent).toBe(''); expect(refCountsA).toEqual([0, 0]); expect(refCountsB).toEqual([0, 0]); }); // Test that effects can initialize state when an atom is first used after an // action that also updated another atom's state. // This corner case was reported by multiple customers. testRecoil('initialze concurrent with state update', () => { const myAtom = atom({ key: 'atom effect - concurrent update', default: 'DEFAULT', effects_UNSTABLE: [({ setSelf }) => setSelf('INIT')] }); const otherAtom = atom({ key: 'atom effect - concurrent update / other atom', default: 'OTHER_DEFAULT' }); const [OtherAtom, setOtherAtom] = componentThatReadsAndWritesAtom(otherAtom); declare function NewPage(): any; let renderPage; declare function App(): any; const c = renderElements(<App />); // <NewPage> is not yet rendered expect(c.textContent).toEqual('"OTHER_DEFAULT"'); // Render <NewPage> which initializes myAtom via effect while also // updating an unrelated atom. act(() => { renderPage(); setOtherAtom('OTHER'); }); expect(c.textContent).toEqual('"OTHER""INIT"'); }); /** * See github issue #1107 item #1 */ testRecoil('atom effect runs twice when selector that depends on that atom is read from a snapshot and the atom is read for first time in that snapshot', () => { let numTimesEffectInit = 0; declare var latestSetSelf: (a: any) => any; const atomWithEffect = atom({ key: 'atomWithEffect', default: 0, effects_UNSTABLE: [({ setSelf }) => { latestSetSelf = setSelf; setSelf(1); // to accurately reproduce minimal reproducible example based on GitHub issue numTimesEffectInit++; }] }); const selThatDependsOnAtom = selector({ key: 'selThatDependsOnAtom', get: ({ get }) => get(atomWithEffect) }); declare var Component: () => any; const c = renderElements(<Component />); expect(c.textContent).toBe('1'); act(() => latestSetSelf(100)); expect(c.textContent).toBe('100'); expect(numTimesEffectInit).toBe(2); }); describe('Other Atoms', () => { test('init from other atom', () => { const myAtom = atom({ key: 'atom effect - init from other atom', default: 'DEFAULT', effects_UNSTABLE: [({ node, setSelf, getLoadable, getInfo_UNSTABLE }) => { const otherValue = getLoadable(otherAtom).contents; expect(otherValue).toEqual('OTHER'); expect(getInfo_UNSTABLE(node).isSet).toBe(false); expect(getInfo_UNSTABLE(otherAtom).isSet).toBe(false); expect(getInfo_UNSTABLE(otherAtom).loadable?.contents).toBe('OTHER'); setSelf(otherValue); }] }); const otherAtom = atom({ key: 'atom effect - other atom', default: 'OTHER' }); expect(getValue(myAtom)).toEqual('OTHER'); }); test('init from other atom async', async () => { const myAtom = atom({ key: 'atom effect - init from other atom async', default: 'DEFAULT', effects_UNSTABLE: [({ setSelf, getPromise }) => { const otherValue = getPromise(otherAtom); setSelf(otherValue); }] }); const otherAtom = atom({ key: 'atom effect - other atom async', default: Promise.resolve('OTHER') }); await expect(getRecoilStateLoadable(myAtom).promiseOrThrow()).resolves.toEqual('OTHER'); }); test('async get other atoms', async () => { let initTest1 = new Promise(() => {}); let initTest2 = new Promise(() => {}); let initTest3 = new Promise(() => {}); let initTest4 = new Promise(() => {}); let initTest5 = new Promise(() => {}); let initTest6 = new Promise(() => {}); let setTest = new Promise(() => {}); const myAtom = atom({ key: 'atom effect - async get', default: 'DEFAULT', effects_UNSTABLE: [// Test we can get default values ({ node, getLoadable, getPromise, getInfo_UNSTABLE }) => { expect(getLoadable(node).contents).toEqual('DEFAULT'); expect(getInfo_UNSTABLE(node).isSet).toBe(false); expect(getInfo_UNSTABLE(node).loadable?.contents).toBe('DEFAULT'); // eslint-disable-next-line jest/valid-expect initTest1 = expect(getPromise(asyncAtom)).resolves.toEqual('ASYNC'); }, ({ setSelf }) => { setSelf('INIT'); }, // Test we can get value from previous initialization ({ node, getLoadable, getInfo_UNSTABLE }) => { expect(getLoadable(node).contents).toEqual('INIT'); expect(getInfo_UNSTABLE(node).isSet).toBe(true); expect(getInfo_UNSTABLE(node).loadable?.contents).toBe('INIT'); }, // Test we can asynchronouse get "current" values of both self and other atoms // This will be executed when myAtom is set, but checks both atoms. ({ onSet, getLoadable, getPromise, getInfo_UNSTABLE }) => { onSet(x => { expect(x).toEqual('SET_ATOM'); expect(getLoadable(myAtom).contents).toEqual(x); expect(getInfo_UNSTABLE(myAtom).isSet).toBe(true); expect(getInfo_UNSTABLE(myAtom).loadable?.contents).toBe('SET_ATOM'); // eslint-disable-next-line jest/valid-expect setTest = expect(getPromise(asyncAtom)).resolves.toEqual('SET_OTHER'); }); }] }); const asyncAtom = atom({ key: 'atom effect - other atom async get', default: Promise.resolve('ASYNC_DEFAULT'), effects_UNSTABLE: [({ setSelf }) => void setSelf(Promise.resolve('ASYNC')), ({ getPromise, getInfo_UNSTABLE }) => { expect(getInfo_UNSTABLE(asyncAtom).isSet).toBe(true); // eslint-disable-next-line jest/valid-expect initTest2 = expect(getInfo_UNSTABLE(asyncAtom).loadable?.toPromise()).resolves.toBe('ASYNC'); // eslint-disable-next-line jest/valid-expect initTest3 = expect(getPromise(asyncAtom)).resolves.toEqual('ASYNC'); }, // Test that we can read default for an aborted initialization ({ setSelf }) => void setSelf(Promise.resolve(new DefaultValue())), ({ getPromise, getInfo_UNSTABLE }) => { expect(getInfo_UNSTABLE(asyncAtom).isSet).toBe(true); // TODO sketchy... // eslint-disable-next-line jest/valid-expect initTest4 = expect(getInfo_UNSTABLE(asyncAtom).loadable?.toPromise()).resolves.toBe('ASYNC_DEFAULT'); // eslint-disable-next-line jest/valid-expect initTest5 = expect(getPromise(asyncAtom)).resolves.toEqual('ASYNC_DEFAULT'); }, // Test initializing to async value and other atom can read it ({ setSelf }) => void setSelf(Promise.resolve('ASYNC')), // Test we can also read it ourselves ({ getInfo_UNSTABLE }) => { expect(getInfo_UNSTABLE(asyncAtom).isSet).toBe(true); // eslint-disable-next-line jest/valid-expect initTest6 = expect(getInfo_UNSTABLE(asyncAtom).loadable?.toPromise()).resolves.toBe('ASYNC'); }] }); const [MyAtom, setMyAtom] = componentThatReadsAndWritesAtom(myAtom); const [AsyncAtom, setAsyncAtom] = componentThatReadsAndWritesAtom(asyncAtom); const c = renderElements(<> <MyAtom /> <AsyncAtom /> </>); await flushPromisesAndTimers(); expect(c.textContent).toBe('"INIT""ASYNC"'); await initTest1; await initTest2; await initTest3; await initTest4; await initTest5; await initTest6; act(() => setAsyncAtom('SET_OTHER')); act(() => setMyAtom('SET_ATOM')); await setTest; }); }); }); testRecoil('object is frozen when stored in atom', async () => { const devStatus = window.__DEV__; window.__DEV__ = true; const anAtom = atom<{ x: mixed, ... }>({ key: 'atom frozen', default: { x: 0 } }); declare function valueAfterSettingInAtom<T>(value: T): T; declare function isFrozen(value: any, getter: any): any; expect(isFrozen({ y: 0 })).toBe(true); // React elements are not deep-frozen (they are already shallow-frozen on creation): const element = { ...<div />, _owner: { ifThisWereAReactFiberItShouldNotBeFrozen: true } }; expect(isFrozen(element, x => (x: any)._owner)).toBe(false); // flowlint-line unclear-type:off // Immutable stuff is not frozen: expect(isFrozen(immutable.List())).toBe(false); expect(isFrozen(immutable.Map())).toBe(false); expect(isFrozen(immutable.OrderedMap())).toBe(false); expect(isFrozen(immutable.Set())).toBe(false); expect(isFrozen(immutable.OrderedSet())).toBe(false); expect(isFrozen(immutable.Seq())).toBe(false); expect(isFrozen(immutable.Stack())).toBe(false); expect(isFrozen(immutable.Range())).toBe(false); expect(isFrozen(immutable.Repeat())).toBe(false); expect(isFrozen(new (immutable.Record({}))())).toBe(false); // Default values are frozen const defaultFrozenAtom = atom({ key: 'atom frozen default', default: { state: 'frozen', nested: { state: 'frozen' } } }); expect(Object.isFrozen(getValue(defaultFrozenAtom))).toBe(true); expect(Object.isFrozen(getValue(defaultFrozenAtom).nested)).toBe(true); // Async Default values are frozen const defaultFrozenAsyncAtom = atom({ key: 'atom frozen default async', default: Promise.resolve({ state: 'frozen', nested: { state: 'frozen' } }) }); await expect(getRecoilStatePromise(defaultFrozenAsyncAtom).then(x => Object.isFrozen(x))).resolves.toBe(true); expect(Object.isFrozen(getValue(defaultFrozenAsyncAtom).nested)).toBe(true); // Initialized values are frozen const initializedValueInAtom = atom({ key: 'atom frozen initialized', default: { nested: 'DEFAULT' }, effects_UNSTABLE: [({ setSelf }) => setSelf({ state: 'frozen', nested: { state: 'frozen' } })] }); expect(Object.isFrozen(getValue(initializedValueInAtom))).toBe(true); expect(Object.isFrozen(getValue(initializedValueInAtom).nested)).toBe(true); // Async Initialized values are frozen const initializedAsyncValueInAtom = atom<{ state: string, nested: {...}, ... }>({ key: 'atom frozen initialized async', default: { state: 'DEFAULT', nested: { state: 'DEFAULT' } }, effects_UNSTABLE: [({ setSelf }) => setSelf(Promise.resolve({ state: 'frozen', nested: { state: 'frozen' } }))] }); await expect(getRecoilStatePromise(initializedAsyncValueInAtom).then(x => Object.isFrozen(x))).resolves.toBe(true); expect(Object.isFrozen(getValue(initializedAsyncValueInAtom).nested)).toBe(true); expect(getValue(initializedAsyncValueInAtom).nested).toEqual({ state: 'frozen' }); // dangerouslyAllowMutability const thawedAtom = atom({ key: 'atom frozen thawed', default: { state: 'thawed', nested: { state: 'thawed' } }, dangerouslyAllowMutability: true }); expect(Object.isFrozen(getValue(thawedAtom))).toBe(false); expect(Object.isFrozen(getValue(thawedAtom).nested)).toBe(false); window.__DEV__ = devStatus; }); testRecoil('Required options are provided when creating atoms', () => { const devStatus = window.__DEV__; window.__DEV__ = true; // $FlowExpectedError[prop-missing] expect(() => atom({ default: undefined })).toThrow(); // $FlowExpectedError[prop-missing] expect(() => atom({ key: 'MISSING DEFAULT' })).toThrow(); window.__DEV__ = devStatus; });