/** * 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, useEffect, useState, act, freshSnapshot, atom, constSelector, selector, ReadsAtom, asyncSelector, componentThatReadsAndWritesAtom, flushPromisesAndTimers, renderElements, useGotoRecoilSnapshot, useRecoilSnapshot; const testRecoil = getRecoilTestFn(() => { React = require('react'); ({ useEffect, useState } = React); ({ act } = require('ReactTestUtils')); ({ freshSnapshot } = require('../../core/Recoil_Snapshot')); atom = require('../../recoil_values/Recoil_atom'); constSelector = require('../../recoil_values/Recoil_constSelector'); selector = require('../../recoil_values/Recoil_selector'); ({ ReadsAtom, asyncSelector, componentThatReadsAndWritesAtom, flushPromisesAndTimers, renderElements } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); ({ useGotoRecoilSnapshot, useRecoilSnapshot } = require('../Recoil_SnapshotHooks')); }); testRecoil('useRecoilSnapshot - subscribe to updates', ({ strictMode }) => { if (strictMode) { return; } const myAtom = atom({ key: 'useRecoilSnapshot - subscribe', default: 'DEFAULT' }); const [ReadsAndWritesAtom, setAtom, resetAtom] = componentThatReadsAndWritesAtom(myAtom); // $FlowFixMe[incompatible-call] added when improving typing for this parameters const mySelector = constSelector(myAtom); const snapshots = []; declare function RecoilSnapshotAndSubscribe(): any; const c = renderElements(<> ); expect(c.textContent).toEqual('"DEFAULT""DEFAULT"'); act(() => setAtom('SET IN CURRENT')); expect(c.textContent).toEqual('"SET IN CURRENT""SET IN CURRENT"'); act(resetAtom); expect(c.textContent).toEqual('"DEFAULT""DEFAULT"'); expect(snapshots.length).toEqual(3); expect(snapshots[0].getLoadable(myAtom).contents).toEqual('DEFAULT'); expect(snapshots[1].getLoadable(myAtom).contents).toEqual('SET IN CURRENT'); expect(snapshots[1].getLoadable(mySelector).contents).toEqual('SET IN CURRENT'); expect(snapshots[2].getLoadable(myAtom).contents).toEqual('DEFAULT'); }); testRecoil('useRecoilSnapshot - goto snapshots', ({ strictMode }) => { if (strictMode) { return; } const atomA = atom({ key: 'useRecoilSnapshot - goto A', default: 'DEFAULT' }); const [ReadsAndWritesAtomA, setAtomA] = componentThatReadsAndWritesAtom(atomA); const atomB = atom({ key: 'useRecoilSnapshot - goto B', default: 'DEFAULT' }); const [ReadsAndWritesAtomB, setAtomB] = componentThatReadsAndWritesAtom(atomB); const snapshots = []; let gotoSnapshot; declare function RecoilSnapshotAndSubscribe(): any; const c = renderElements(<> ); expect(c.textContent).toEqual('"DEFAULT""DEFAULT"'); act(() => setAtomA(1)); expect(c.textContent).toEqual('1"DEFAULT"'); act(() => setAtomB(2)); expect(c.textContent).toEqual('12'); expect(snapshots.length).toEqual(3); act(() => gotoSnapshot(snapshots[1])); expect(c.textContent).toEqual('1"DEFAULT"'); act(() => gotoSnapshot(snapshots[0])); expect(c.textContent).toEqual('"DEFAULT""DEFAULT"'); // $FlowFixMe[incompatible-call] act(() => gotoSnapshot(snapshots[2].map(({ set }) => set(atomB, 3)))); expect(c.textContent).toEqual('13'); }); testRecoil('useRecoilSnapshot - async selectors', async ({ strictMode, concurrentMode }) => { const [mySelector, resolve] = asyncSelector(); const snapshots = []; declare function RecoilSnapshotAndSubscribe(): any; const c = renderElements(<> ); expect(c.textContent).toEqual('loading'); expect(snapshots.length).toEqual(strictMode && concurrentMode ? 2 : 1); act(() => resolve('RESOLVE')); await flushPromisesAndTimers(); await flushPromisesAndTimers(); expect(c.textContent).toEqual('"RESOLVE"'); expect(snapshots.length).toEqual(strictMode && concurrentMode ? 3 : 2); expect(snapshots[0].getLoadable(mySelector).contents).toEqual('RESOLVE'); }); testRecoil('Subscriptions', async () => { const myAtom = atom({ key: 'useRecoilSnapshot Subscriptions atom', default: 'ATOM' }); const selectorA = selector({ key: 'useRecoilSnapshot Subscriptions A', get: ({ get }) => get(myAtom) }); const selectorB = selector({ key: 'useRecoilSnapshot Subscriptions B', get: ({ get }) => get(selectorA) + get(myAtom) }); const selectorC = selector({ key: 'useRecoilSnapshot Subscriptions C', get: async ({ get }) => { const ret = get(selectorA) + get(selectorB); await Promise.resolve(); return ret; } }); let snapshot = freshSnapshot(); declare function RecoilSnapshot(): any; const c = renderElements(<> ); await flushPromisesAndTimers(); expect(c.textContent).toBe('"ATOMATOMATOM"'); expect(Array.from(snapshot.getInfo_UNSTABLE(myAtom).subscribers.nodes).length).toBe(3); expect(Array.from(snapshot.getInfo_UNSTABLE(myAtom).subscribers.nodes)).toEqual(expect.arrayContaining([selectorA, selectorB, selectorC])); expect(Array.from(snapshot.getInfo_UNSTABLE(selectorA).subscribers.nodes).length).toBe(2); expect(Array.from(snapshot.getInfo_UNSTABLE(selectorA).subscribers.nodes)).toEqual(expect.arrayContaining([selectorB, selectorC])); expect(Array.from(snapshot.getInfo_UNSTABLE(selectorB).subscribers.nodes).length).toBe(1); expect(Array.from(snapshot.getInfo_UNSTABLE(selectorB).subscribers.nodes)).toEqual(expect.arrayContaining([selectorC])); expect(Array.from(snapshot.getInfo_UNSTABLE(selectorC).subscribers.nodes).length).toBe(0); expect(Array.from(snapshot.getInfo_UNSTABLE(selectorC).subscribers.nodes)).toEqual(expect.arrayContaining([])); }); describe('Snapshot Retention', () => { testRecoil('Retained for duration component is mounted', async () => { let retainedDuringEffect = false; let setMount; let checkRetention; declare function UseRecoilSnapshot(): any; declare function Component(): any; renderElements(); expect(retainedDuringEffect).toBe(false); act(() => setMount(true)); expect(retainedDuringEffect).toBe(true); expect(checkRetention?.()).toBe(true); act(() => setMount(false)); await flushPromisesAndTimers(); expect(checkRetention?.()).toBe(false); }); testRecoil('Snapshot auto-release', async ({ gks }) => { let rootFirstCnt = 0; const rootFirstAtom = atom({ key: 'useRecoilSnapshot auto-release root-first', default: 'DEFAULT', effects: [({ setSelf }) => { rootFirstCnt++; setSelf('ROOT'); return () => { rootFirstCnt--; }; }] }); let snapshotFirstCnt = 0; const snapshotFirstAtom = atom({ key: 'useRecoilSnapshot auto-release snapshot-first', default: 'DEFAULT', effects: [({ setSelf }) => { snapshotFirstCnt++; setSelf('SNAPSHOT FIRST'); return () => { snapshotFirstCnt--; }; }] }); let snapshotOnlyCnt = 0; const snapshotOnlyAtom = atom({ key: 'useRecoilSnapshot auto-release snapshot-only', default: 'DEFAULT', effects: [({ setSelf }) => { snapshotOnlyCnt++; setSelf('SNAPSHOT ONLY'); return () => { snapshotOnlyCnt--; }; }] }); let rootOnlyCnt = 0; const rootOnlyAtom = atom({ key: 'useRecoilSnapshot auto-release root-only', default: 'DEFAULT', effects: [({ setSelf }) => { rootOnlyCnt++; setSelf('RETAIN'); return () => { rootOnlyCnt--; }; }] }); declare var setMount: (_: any) => any; declare function UseRecoilSnapshot(): any; declare function Component(): any; const c = renderElements(); expect(c.textContent).toBe('"RETAIN"'); expect(rootOnlyCnt).toBe(1); expect(snapshotOnlyCnt).toBe(0); expect(rootFirstCnt).toBe(0); expect(snapshotFirstCnt).toBe(0); act(() => setMount(true)); expect(c.textContent).toBe('"RETAIN""ROOT"SNAPSHOT FIRSTSNAPSHOT ONLY"SNAPSHOT FIRST"'); await flushPromisesAndTimers(); expect(rootOnlyCnt).toBe(1); expect(snapshotOnlyCnt).toBe(1); expect(rootFirstCnt).toBe(1); expect(snapshotFirstCnt).toBe(2); // Confirm snapshot isn't released until component is unmounted await flushPromisesAndTimers(); expect(rootOnlyCnt).toBe(1); expect(snapshotOnlyCnt).toBe(1); expect(rootFirstCnt).toBe(1); expect(snapshotFirstCnt).toBe(2); // Auto-release snapshot act(() => setMount(false)); await flushPromisesAndTimers(); expect(c.textContent).toBe('"RETAIN"'); expect(rootOnlyCnt).toBe(1); expect(snapshotOnlyCnt).toBe(0); if (gks.includes('recoil_memory_management_2020')) { expect(rootFirstCnt).toBe(0); expect(snapshotFirstCnt).toBe(0); } }); });