UNPKG

recoil

Version:

Recoil - A state management library for React

743 lines (668 loc) 23.8 kB
/** * 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. * * @flow strict-local * @format * @oncall recoil */ 'use strict'; import type {Store} from '../../core/Recoil_State'; import type {Parameter} from 'Recoil_atomFamily'; import type {StoreID as StoreIDType} from 'Recoil_Keys'; import type {RecoilState} from 'Recoil_RecoilValue'; import type {Node} from 'react'; const { getRecoilTestFn, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); let store: Store, React, Profiler, useState, act, RecoilRoot, getRecoilValueAsLoadable, setRecoilValue, useRecoilState, useRecoilValue, useSetRecoilState, useSetUnvalidatedAtomValues, useRecoilStoreID, ReadsAtom, componentThatReadsAndWritesAtom, flushPromisesAndTimers, renderElements, reactMode, stableStringify, atom, atomFamily, selectorFamily, RecoilLoadable, pAtom; const testRecoil = getRecoilTestFn(() => { const { makeStore, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils'); React = require('react'); ({Profiler, useState} = require('react')); ({act} = require('ReactTestUtils')); ({RecoilRoot, useRecoilStoreID} = require('../../core/Recoil_RecoilRoot')); ({ getRecoilValueAsLoadable, setRecoilValue, } = require('../../core/Recoil_RecoilValueInterface')); ({ useRecoilState, useRecoilValue, useSetRecoilState, useSetUnvalidatedAtomValues, } = require('../../hooks/Recoil_Hooks')); ({ ReadsAtom, componentThatReadsAndWritesAtom, flushPromisesAndTimers, renderElements, } = require('recoil-shared/__test_utils__/Recoil_TestingUtils')); ({reactMode} = require('../../core/Recoil_ReactMode')); stableStringify = require('recoil-shared/util/Recoil_stableStringify'); atom = require('../Recoil_atom'); atomFamily = require('../Recoil_atomFamily'); selectorFamily = require('../Recoil_selectorFamily'); ({RecoilLoadable} = require('../../adt/Recoil_Loadable')); store = makeStore(); pAtom = atomFamily({ key: 'pAtom', default: 'fallback', }); }); let fbOnlyTest = test.skip; // $FlowFixMe[prop-missing] // $FlowFixMe[incompatible-type] // @fb-only: fbOnlyTest = testRecoil; let id = 0; /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ function get(recoilValue) { return getRecoilValueAsLoadable(store, recoilValue).contents; } /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ function getLoadable(recoilValue) { return getRecoilValueAsLoadable(store, recoilValue); } function set( /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ recoilValue, value: | void | number | $TEMPORARY$string<'VALUE'> | $TEMPORARY$string<'bar'> | $TEMPORARY$string<'eggs'> | $TEMPORARY$string<'spam'> | $TEMPORARY$string<'xValue'> | $TEMPORARY$string<'xValue1'> | $TEMPORARY$string<'xValue2'> | $TEMPORARY$string<'xValue3'> | $TEMPORARY$string<'xValue4'> | $TEMPORARY$string<'yValue'> | $TEMPORARY$string<'yValue1'> | $TEMPORARY$string<'yValue2'> | $TEMPORARY$string<'yValue3'> | $TEMPORARY$string<'yValue4'>, ) { setRecoilValue(store, recoilValue, value); } testRecoil('Read fallback by default', () => { expect(get(pAtom({k: 'x'}))).toBe('fallback'); }); testRecoil('Uses value for parameter', () => { set(pAtom({k: 'x'}), 'xValue'); set(pAtom({k: 'y'}), 'yValue'); expect(get(pAtom({k: 'x'}))).toBe('xValue'); expect(get(pAtom({k: 'y'}))).toBe('yValue'); expect(get(pAtom({k: 'z'}))).toBe('fallback'); }); testRecoil('Works with non-overlapping sets', () => { set(pAtom({x: 'x'}), 'xValue'); set(pAtom({y: 'y'}), 'yValue'); expect(get(pAtom({x: 'x'}))).toBe('xValue'); expect(get(pAtom({y: 'y'}))).toBe('yValue'); }); describe('Default', () => { testRecoil('default is optional', () => { const myAtom = atom({key: 'atom without default'}); expect(getLoadable(myAtom).state).toBe('loading'); act(() => set(myAtom, 'VALUE')); expect(get(myAtom)).toBe('VALUE'); }); testRecoil('Works with atom default', () => { const fallbackAtom = atom({key: 'fallback', default: 0}); const hasFallback = atomFamily({ key: 'hasFallback', default: fallbackAtom, }); expect(get(hasFallback({k: 'x'}))).toBe(0); set(fallbackAtom, 1); expect(get(hasFallback({k: 'x'}))).toBe(1); set(hasFallback({k: 'x'}), 2); expect(get(hasFallback({k: 'x'}))).toBe(2); expect(get(hasFallback({k: 'y'}))).toBe(1); }); testRecoil('Works with parameterized default', () => { const paramDefaultAtom = atomFamily({ key: 'parameterized default', default: ({num}) => num, }); expect(get(paramDefaultAtom({num: 1}))).toBe(1); expect(get(paramDefaultAtom({num: 2}))).toBe(2); set(paramDefaultAtom({num: 1}), 3); expect(get(paramDefaultAtom({num: 1}))).toBe(3); expect(get(paramDefaultAtom({num: 2}))).toBe(2); }); testRecoil('Parameterized async default', async () => { const paramDefaultAtom = atomFamily({ key: 'parameterized async default', default: ({num}) => num === 1 ? Promise.reject(num) : Promise.resolve(num), }); await expect(get(paramDefaultAtom({num: 1}))).rejects.toBe(1); await expect(get(paramDefaultAtom({num: 2}))).resolves.toBe(2); set(paramDefaultAtom({num: 1}), 3); expect(get(paramDefaultAtom({num: 1}))).toBe(3); expect(get(paramDefaultAtom({num: 2}))).toBe(2); }); testRecoil('Parameterized loadable default', async () => { const paramDefaultAtom = atomFamily({ key: 'parameterized loadable default', default: ({num}) => num === 1 ? RecoilLoadable.error(num) : RecoilLoadable.of(num), }); expect(getLoadable(paramDefaultAtom({num: 1})).state).toBe('hasError'); expect(getLoadable(paramDefaultAtom({num: 1})).contents).toBe(1); expect(getLoadable(paramDefaultAtom({num: 2})).state).toBe('hasValue'); expect(getLoadable(paramDefaultAtom({num: 2})).contents).toBe(2); set(paramDefaultAtom({num: 1}), 3); expect(getLoadable(paramDefaultAtom({num: 1})).state).toBe('hasValue'); expect(getLoadable(paramDefaultAtom({num: 1})).contents).toBe(3); expect(getLoadable(paramDefaultAtom({num: 2})).state).toBe('hasValue'); expect(getLoadable(paramDefaultAtom({num: 2})).contents).toBe(2); }); }); testRecoil('Works with date as parameter', () => { const dateAtomFamily = atomFamily({ key: 'dateFamily', default: _date => 0, }); expect(get(dateAtomFamily(new Date(2021, 2, 25)))).toBe(0); expect(get(dateAtomFamily(new Date(2021, 2, 26)))).toBe(0); set(dateAtomFamily(new Date(2021, 2, 25)), 1); expect(get(dateAtomFamily(new Date(2021, 2, 25)))).toBe(1); expect(get(dateAtomFamily(new Date(2021, 2, 26)))).toBe(0); }); testRecoil('Works with parameterized fallback', () => { const fallbackAtom = atomFamily({ key: 'parameterized fallback default', default: ({num}) => num * 10, }); const paramFallbackAtom = atomFamily({ key: 'parameterized fallback', default: fallbackAtom, }); expect(get(paramFallbackAtom({num: 1}))).toBe(10); expect(get(paramFallbackAtom({num: 2}))).toBe(20); set(paramFallbackAtom({num: 1}), 3); expect(get(paramFallbackAtom({num: 1}))).toBe(3); expect(get(paramFallbackAtom({num: 2}))).toBe(20); set(fallbackAtom({num: 2}), 200); expect(get(paramFallbackAtom({num: 2}))).toBe(200); set(fallbackAtom({num: 1}), 100); expect(get(paramFallbackAtom({num: 1}))).toBe(3); expect(get(paramFallbackAtom({num: 2}))).toBe(200); }); testRecoil('atomFamily async fallback', async () => { const paramFallback = atomFamily({ key: 'paramaterizedAtom async Fallback', default: Promise.resolve(42), }); const container = renderElements(<ReadsAtom atom={paramFallback({})} />); expect(container.textContent).toEqual('loading'); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(container.textContent).toEqual('42'); }); testRecoil('Parameterized fallback with atom and async', async () => { const paramFallback = atomFamily({ key: 'parameterized async Fallback', default: ({param}) => ({ value: 'value', atom: atom({key: `param async fallback atom ${id++}`, default: 'atom'}), async: Promise.resolve('async'), }[param]), }); const valueCont = renderElements( <ReadsAtom atom={paramFallback({param: 'value'})} />, ); expect(valueCont.textContent).toEqual('"value"'); const atomCont = renderElements( <ReadsAtom atom={paramFallback({param: 'atom'})} />, ); expect(atomCont.textContent).toEqual('"atom"'); const asyncCont = renderElements( <ReadsAtom atom={paramFallback({param: 'async'})} />, ); expect(asyncCont.textContent).toEqual('loading'); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(asyncCont.textContent).toEqual('"async"'); }); fbOnlyTest('atomFamily with scope', () => { const scopeForParamAtom = atom<string>({ key: 'scope atom for atomFamily', default: 'foo', }); const paramAtomWithScope = atomFamily<string, {k: string}>({ key: 'parameterized atom with scope', default: 'default', scopeRules_APPEND_ONLY_READ_THE_DOCS: [[scopeForParamAtom]], }); expect(get(paramAtomWithScope({k: 'x'}))).toBe('default'); expect(get(paramAtomWithScope({k: 'y'}))).toBe('default'); set(paramAtomWithScope({k: 'x'}), 'xValue1'); expect(get(paramAtomWithScope({k: 'x'}))).toBe('xValue1'); expect(get(paramAtomWithScope({k: 'y'}))).toBe('default'); set(paramAtomWithScope({k: 'y'}), 'yValue1'); expect(get(paramAtomWithScope({k: 'x'}))).toBe('xValue1'); expect(get(paramAtomWithScope({k: 'y'}))).toBe('yValue1'); set(scopeForParamAtom, 'bar'); expect(get(paramAtomWithScope({k: 'x'}))).toBe('default'); expect(get(paramAtomWithScope({k: 'y'}))).toBe('default'); set(paramAtomWithScope({k: 'x'}), 'xValue2'); expect(get(paramAtomWithScope({k: 'x'}))).toBe('xValue2'); expect(get(paramAtomWithScope({k: 'y'}))).toBe('default'); set(paramAtomWithScope({k: 'y'}), 'yValue2'); expect(get(paramAtomWithScope({k: 'x'}))).toBe('xValue2'); expect(get(paramAtomWithScope({k: 'y'}))).toBe('yValue2'); }); fbOnlyTest('atomFamily with parameterized scope', () => { const paramScopeForParamAtom = atomFamily<string, {namespace: string}>({ key: 'scope atom for atomFamily with parameterized scope', default: ({namespace}) => namespace, }); const paramAtomWithParamScope = atomFamily<string, {k: string, n: string}>({ key: 'parameterized atom with parameterized scope', default: 'default', scopeRules_APPEND_ONLY_READ_THE_DOCS: [ [({n}) => paramScopeForParamAtom({namespace: n})], ], }); expect(get(paramScopeForParamAtom({namespace: 'foo'}))).toBe('foo'); expect(get(paramAtomWithParamScope({n: 'foo', k: 'x'}))).toBe('default'); expect(get(paramAtomWithParamScope({n: 'foo', k: 'y'}))).toBe('default'); set(paramAtomWithParamScope({n: 'foo', k: 'x'}), 'xValue1'); expect(get(paramAtomWithParamScope({n: 'foo', k: 'x'}))).toBe('xValue1'); expect(get(paramAtomWithParamScope({n: 'foo', k: 'y'}))).toBe('default'); set(paramAtomWithParamScope({n: 'foo', k: 'y'}), 'yValue1'); expect(get(paramAtomWithParamScope({n: 'foo', k: 'x'}))).toBe('xValue1'); expect(get(paramAtomWithParamScope({n: 'foo', k: 'y'}))).toBe('yValue1'); set(paramScopeForParamAtom({namespace: 'foo'}), 'eggs'); expect(get(paramScopeForParamAtom({namespace: 'foo'}))).toBe('eggs'); expect(get(paramAtomWithParamScope({n: 'foo', k: 'x'}))).toBe('default'); expect(get(paramAtomWithParamScope({n: 'foo', k: 'y'}))).toBe('default'); set(paramAtomWithParamScope({n: 'foo', k: 'x'}), 'xValue2'); expect(get(paramAtomWithParamScope({n: 'foo', k: 'x'}))).toBe('xValue2'); expect(get(paramAtomWithParamScope({n: 'foo', k: 'y'}))).toBe('default'); set(paramAtomWithParamScope({n: 'foo', k: 'y'}), 'yValue2'); expect(get(paramAtomWithParamScope({n: 'foo', k: 'x'}))).toBe('xValue2'); expect(get(paramAtomWithParamScope({n: 'foo', k: 'y'}))).toBe('yValue2'); expect(get(paramScopeForParamAtom({namespace: 'bar'}))).toBe('bar'); expect(get(paramAtomWithParamScope({n: 'bar', k: 'x'}))).toBe('default'); expect(get(paramAtomWithParamScope({n: 'bar', k: 'y'}))).toBe('default'); set(paramAtomWithParamScope({n: 'bar', k: 'x'}), 'xValue3'); expect(get(paramAtomWithParamScope({n: 'bar', k: 'x'}))).toBe('xValue3'); expect(get(paramAtomWithParamScope({n: 'bar', k: 'y'}))).toBe('default'); set(paramAtomWithParamScope({n: 'bar', k: 'y'}), 'yValue3'); expect(get(paramAtomWithParamScope({n: 'bar', k: 'x'}))).toBe('xValue3'); expect(get(paramAtomWithParamScope({n: 'bar', k: 'y'}))).toBe('yValue3'); set(paramScopeForParamAtom({namespace: 'bar'}), 'spam'); expect(get(paramScopeForParamAtom({namespace: 'bar'}))).toBe('spam'); expect(get(paramAtomWithParamScope({n: 'bar', k: 'x'}))).toBe('default'); expect(get(paramAtomWithParamScope({n: 'bar', k: 'y'}))).toBe('default'); set(paramAtomWithParamScope({n: 'bar', k: 'x'}), 'xValue4'); expect(get(paramAtomWithParamScope({n: 'bar', k: 'x'}))).toBe('xValue4'); expect(get(paramAtomWithParamScope({n: 'bar', k: 'y'}))).toBe('default'); set(paramAtomWithParamScope({n: 'bar', k: 'y'}), 'yValue4'); expect(get(paramAtomWithParamScope({n: 'bar', k: 'x'}))).toBe('xValue4'); expect(get(paramAtomWithParamScope({n: 'bar', k: 'y'}))).toBe('yValue4'); }); testRecoil('Returns the fallback for parameterized atoms', () => { let theAtom = null; let setUnvalidatedAtomValues; let setAtomParam; let setAtomValue; function SetsUnvalidatedAtomValues() { setUnvalidatedAtomValues = useSetUnvalidatedAtomValues(); return null; } let setVisible; function Switch({children}: $TEMPORARY$object<{children: Node}>) { const [visible, mySetVisible] = useState(false); setVisible = mySetVisible; return visible ? children : null; } function MyReadsAtom({ getAtom, }: $TEMPORARY$object<{ getAtom: () => null | (Parameter => RecoilState<number>), }>) { const [param, setParam] = useState({num: 1}); setAtomParam = setParam; // flowlint-next-line unclear-type:off const myAtom: any = getAtom(); const [value, setValue] = useRecoilState(myAtom(param)); setAtomValue = setValue; return value; } const container = renderElements( <> <SetsUnvalidatedAtomValues /> <Switch> <MyReadsAtom getAtom={() => theAtom} /> </Switch> </>, ); act(() => { setUnvalidatedAtomValues( new Map().set('notDefinedYetAtomFamilyWithFallback', 123), ); }); const fallback = atom<number>({ key: 'fallback for atomFamily', default: 222, }); theAtom = atomFamily({ key: 'notDefinedYetAtomFamilyWithFallback', default: fallback, persistence_UNSTABLE: { type: 'url', validator: (_, returnFallback) => returnFallback, }, }); act(() => { setVisible(true); }); expect(container.textContent).toBe('222'); act(() => { setAtomValue(111); }); expect(container.textContent).toBe('111'); act(() => { setAtomParam({num: 2}); }); expect(container.textContent).toBe('222'); }); testRecoil( 'Returns the fallback for parameterized atoms with a selector as the fallback', () => { let theAtom = null; let setUnvalidatedAtomValues; let setAtomParam; let setAtomValue; function SetsUnvalidatedAtomValues() { setUnvalidatedAtomValues = useSetUnvalidatedAtomValues(); return null; } let setVisible; function Switch({children}: $TEMPORARY$object<{children: Node}>) { const [visible, mySetVisible] = useState(false); setVisible = mySetVisible; return visible ? children : null; } /* $FlowFixMe[missing-local-annot] The type annotation(s) required by * Flow's LTI update could not be added via codemod */ function MyReadsAtom({getAtom}) { const [param, setParam] = useState({num: 10}); setAtomParam = setParam; // flowlint-next-line unclear-type:off const myAtom: any = getAtom(); const [value, setValue] = useRecoilState(myAtom(param)); setAtomValue = setValue; return value; } const container = renderElements( <> <SetsUnvalidatedAtomValues /> <Switch> <MyReadsAtom getAtom={() => theAtom} /> </Switch> </>, ); act(() => { setUnvalidatedAtomValues( new Map().set('notDefinedYetAtomFamilyFallbackSel', 123), ); }); theAtom = atomFamily({ key: 'notDefinedYetAtomFamilyFallbackSel', default: selectorFamily({ key: 'notDefinedYetAtomFamilyFallbackSelFallback', get: ({num}) => () => num === 1 ? 456 : 789, }), persistence_UNSTABLE: { type: 'url', validator: (_, notValid) => notValid, }, }); act(() => { setVisible(true); }); expect(container.textContent).toBe('789'); act(() => { setAtomValue(111); }); expect(container.textContent).toBe('111'); act(() => { setAtomParam({num: 1}); }); expect(container.textContent).toBe('456'); }, ); testRecoil('Independent atom subscriptions', ({gks}) => { const BASE_CALLS = reactMode().mode === 'LEGACY' && !gks.includes('recoil_suppress_rerender_in_callback') ? 1 : 0; const myAtom = atomFamily({ key: 'atomFamily/independent subscriptions', default: 'DEFAULT', }); const TrackingComponent = ( param: $TEMPORARY$string<'A'> | $TEMPORARY$string<'B'>, ) => { let numUpdates = 0; let setValue; const Component = () => { setValue = useSetRecoilState(myAtom(param)); return ( <Profiler id="test" onRender={() => { numUpdates++; }}> {stableStringify(useRecoilValue(myAtom(param)))} </Profiler> ); }; return [Component, (value: number) => setValue(value), () => numUpdates]; }; const [ComponentA, setValueA, getNumUpdatesA] = TrackingComponent('A'); const [ComponentB, setValueB, getNumUpdatesB] = TrackingComponent('B'); const container = renderElements( <> <ComponentA /> <ComponentB /> </>, ); // Initial: expect(container.textContent).toBe('"DEFAULT""DEFAULT"'); expect(getNumUpdatesA()).toBe(BASE_CALLS + 1); expect(getNumUpdatesB()).toBe(BASE_CALLS + 1); // After setting at parameter A, component A should update: act(() => setValueA(1)); expect(container.textContent).toBe('1"DEFAULT"'); expect(getNumUpdatesA()).toBe(BASE_CALLS + 2); expect(getNumUpdatesB()).toBe(BASE_CALLS + 1); // After setting at parameter B, component B should update: act(() => setValueB(2)); expect(container.textContent).toBe('12'); expect(getNumUpdatesA()).toBe(BASE_CALLS + 2); expect(getNumUpdatesB()).toBe(BASE_CALLS + 2); }); describe('Effects', () => { testRecoil('Initialization', () => { let inited = 0; const myFamily = atomFamily<string, number>({ key: 'atomFamily effect init', default: 'DEFAULT', effects: [ ({setSelf}) => { inited++; setSelf('INIT'); }, ], }); expect(inited).toEqual(0); expect(get(myFamily(1))).toEqual('INIT'); expect(inited).toEqual(1); set(myFamily(2)); expect(inited).toEqual(2); const [ReadsWritesAtom, _, reset] = componentThatReadsAndWritesAtom( myFamily(1), ); const c = renderElements(<ReadsWritesAtom />); expect(c.textContent).toEqual('"INIT"'); act(reset); expect(c.textContent).toEqual('"DEFAULT"'); }); testRecoil('Parameterized Initialization', () => { const myFamily = atomFamily({ key: 'atomFamily effect parameterized init', default: 'DEFAULT', effects: param => [({setSelf}) => setSelf(param)], }); expect(get(myFamily(1))).toEqual(1); expect(get(myFamily(2))).toEqual(2); }); testRecoil('Cleanup Handlers - when root unmounted', () => { const refCounts: {[string]: number} = {A: 0, B: 0}; const atoms = atomFamily({ key: 'atomFamily effect cleanup', default: p => p, effects: p => [ () => { refCounts[p]++; return () => { refCounts[p]--; }; }, ], }); let setNumRoots; function App() { const [numRoots, _setNumRoots] = useState(0); setNumRoots = _setNumRoots; return ( <div> {Array(numRoots) .fill(null) .map((_, idx) => ( <RecoilRoot key={idx}> <ReadsAtom atom={atoms('A')} /> <ReadsAtom atom={atoms('B')} /> </RecoilRoot> ))} </div> ); } const c = renderElements(<App />); expect(c.textContent).toBe(''); expect(refCounts).toEqual({A: 0, B: 0}); act(() => setNumRoots(1)); expect(c.textContent).toBe('"A""B"'); expect(refCounts).toEqual({A: 1, B: 1}); act(() => setNumRoots(2)); expect(c.textContent).toBe('"A""B""A""B"'); expect(refCounts).toEqual({A: 2, B: 2}); act(() => setNumRoots(1)); expect(c.textContent).toBe('"A""B"'); expect(refCounts).toEqual({A: 1, B: 1}); act(() => setNumRoots(0)); expect(c.textContent).toBe(''); expect(refCounts).toEqual({A: 0, B: 0}); }); testRecoil('storeID matches <RecoilRoot>', async () => { const atoms = atomFamily({ key: 'atomFamily effect - storeID', default: 'DEFAULT', effects: rootKey => [ ({storeID, setSelf}) => { expect(storeID).toEqual(storeIDs[rootKey]); setSelf(rootKey); }, ], }); const storeIDs: {[string]: StoreIDType} = {}; function StoreID({ rootKey, }: | $TEMPORARY$object<{rootKey: $TEMPORARY$string<'A'>}> | $TEMPORARY$object<{rootKey: $TEMPORARY$string<'A1'>}> | $TEMPORARY$object<{rootKey: $TEMPORARY$string<'A2'>}> | $TEMPORARY$object<{rootKey: $TEMPORARY$string<'B'>}>) { const storeID = useRecoilStoreID(); storeIDs[rootKey] = storeID; return null; } function MyApp() { return ( <div> <RecoilRoot> <StoreID rootKey="A" /> <ReadsAtom atom={atoms('A')} /> <RecoilRoot> <StoreID rootKey="A1" /> <ReadsAtom atom={atoms('A1')} /> </RecoilRoot> <RecoilRoot override={false}> <StoreID rootKey="A2" /> <ReadsAtom atom={atoms('A2')} /> </RecoilRoot> </RecoilRoot> <RecoilRoot> <StoreID rootKey="B" /> <ReadsAtom atom={atoms('B')} /> </RecoilRoot> </div> ); } const c = renderElements(<MyApp />); expect(c.textContent).toEqual('"A""A1""A2""B"'); expect('A' in storeIDs).toEqual(true); expect('A1' in storeIDs).toEqual(true); expect('A2' in storeIDs).toEqual(true); expect('B' in storeIDs).toEqual(true); expect(storeIDs.A).not.toEqual(storeIDs.B); expect(storeIDs.A).not.toEqual(storeIDs.A1); expect(storeIDs.A).toEqual(storeIDs.A2); expect(storeIDs.B).not.toEqual(storeIDs.A1); expect(storeIDs.B).not.toEqual(storeIDs.A2); }); });