/**
* 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);
}
});
});