diff --git a/src/components/experiment/configurationTab.tsx b/src/components/experiment/configurationTab.tsx index 703ca6db..4a205260 100644 --- a/src/components/experiment/configurationTab.tsx +++ b/src/components/experiment/configurationTab.tsx @@ -1,6 +1,6 @@ import { Grid } from '@mui/material' import { useExperiment } from '@/context/experiment' -import { useGlobal } from '@/context/global' +import { useSelector } from '@/context/global' import { ValueVariableType, CategoricalVariableType, @@ -9,17 +9,15 @@ import { import Details from '../details' import OptimizerModel from '../input-model/optimizer-model' import OptimizerConfigurator from '../optimizer-configurator' +import { selectAdvancedConfiguration } from '@/context/global/global-selectors' export const ConfigurationTab = () => { const { state: { experiment }, dispatch, } = useExperiment() - const { - state: { - flags: { advancedConfiguration }, - }, - } = useGlobal() + + const advancedConfiguration = useSelector(selectAdvancedConfiguration) const valueVariables = experiment.valueVariables const categoricalVariables = experiment.categoricalVariables diff --git a/src/context/experiment/experiment-context.tsx b/src/context/experiment/experiment-context.tsx index 145ad14d..d8949b6e 100644 --- a/src/context/experiment/experiment-context.tsx +++ b/src/context/experiment/experiment-context.tsx @@ -22,7 +22,7 @@ type ExperimentProviderProps = { children: any } -function ExperimentProvider({ +export function ExperimentProvider({ experimentId, children, }: ExperimentProviderProps) { @@ -63,7 +63,7 @@ function ExperimentProvider({ ) } -function useExperiment() { +export function useExperiment() { const context = React.useContext(ExperimentContext) if (context === undefined) { throw new Error('useExperiment must be used within an ExperimentProvider') @@ -124,9 +124,10 @@ const fetchExperimentResult = async ( return experimentResult } -async function runExperiment(dispatch: Dispatch, experiment: ExperimentType) { +export async function runExperiment( + dispatch: Dispatch, + experiment: ExperimentType +) { const result = await fetchExperimentResult(experiment) dispatch({ type: 'registerResult', payload: result }) } - -export { ExperimentProvider, useExperiment, runExperiment } diff --git a/src/context/experiment/experiment-selectors.test.tsx b/src/context/experiment/experiment-selectors.test.ts similarity index 100% rename from src/context/experiment/experiment-selectors.test.tsx rename to src/context/experiment/experiment-selectors.test.ts diff --git a/src/context/global/global-context.test.tsx b/src/context/global/global-context.test.tsx new file mode 100644 index 00000000..78e8a0ba --- /dev/null +++ b/src/context/global/global-context.test.tsx @@ -0,0 +1,35 @@ +import { renderHook } from '@testing-library/react' +import { FC } from 'react' +import { State } from '@/context/global' +import { GlobalStateProvider, useGlobal, useSelector } from '../global' + +const GlobalWrapper: FC<{ children: React.ReactNode }> = ({ children }) => ( + {children} +) + +describe('useGlobal', () => { + it('fails if called outside provider', async () => { + console.error = jest.fn() + expect(() => renderHook(() => useGlobal())).toThrow( + 'useGlobal must be used within a GlobalStateProvider' + ) + expect(console.error).toHaveBeenCalled() + }) + + it('provides context when called inside provider', async () => { + const { result } = renderHook(() => useGlobal(), { + wrapper: GlobalWrapper, + }) + expect(result.current.state.debug).toBeFalsy() + }) +}) + +describe('useSelector', () => { + it('should bind selector to state', () => { + const testSelector = (state: State) => state.debug + const { result } = renderHook(() => useSelector(testSelector), { + wrapper: GlobalWrapper, + }) + expect(result.current).toBeFalsy + }) +}) diff --git a/src/context/global/global-context.tsx b/src/context/global/global-context.tsx index 1e6cca7f..c6c5a639 100644 --- a/src/context/global/global-context.tsx +++ b/src/context/global/global-context.tsx @@ -17,7 +17,7 @@ interface GlobalStateProviderProps { children: React.ReactNode } -function GlobalStateProvider({ children }: GlobalStateProviderProps) { +export function GlobalStateProvider({ children }: GlobalStateProviderProps) { const [state, dispatch] = useLocalStorageReducer( reducer, initialState, @@ -55,7 +55,7 @@ function GlobalStateProvider({ children }: GlobalStateProviderProps) { ) } -function useGlobal() { +export function useGlobal() { const context = React.useContext(GlobalContext) if (context === undefined) { throw new Error('useGlobal must be used within a GlobalStateProvider') @@ -63,4 +63,10 @@ function useGlobal() { return context } -export { GlobalStateProvider, useGlobal } +export const useSelector = (selector: (state: State) => T) => { + const context = React.useContext(GlobalContext) + if (context === undefined) { + throw new Error('useSelector must be used within an GlobalStateProvider') + } + return selector(context.state) +} diff --git a/src/context/global/global-selectors.test.ts b/src/context/global/global-selectors.test.ts new file mode 100644 index 00000000..8a3b19d8 --- /dev/null +++ b/src/context/global/global-selectors.test.ts @@ -0,0 +1,21 @@ +import { initialState, State } from '@/context/global' +import { selectDebug, selectAdvancedConfiguration } from './global-selectors' + +describe('Experiment selectors', () => { + let state: State + beforeEach(() => { + state = JSON.parse(JSON.stringify(initialState)) + }) + + it('should select debug', () => { + state.debug = true + expect(selectDebug(state)).toBeTruthy() + }) + + describe('Flags', () => { + it('should selectAdvancedConfiguration', () => { + state.flags.advancedConfiguration = true + expect(selectAdvancedConfiguration(state)).toBeTruthy() + }) + }) +}) diff --git a/src/context/global/global-selectors.ts b/src/context/global/global-selectors.ts new file mode 100644 index 00000000..5d8cd107 --- /dev/null +++ b/src/context/global/global-selectors.ts @@ -0,0 +1,8 @@ +import { State } from '@/context/global' + +export const selectDebug = (state: State) => state.debug + +export const selectFlags = (state: State) => state.flags + +export const selectAdvancedConfiguration = (state: State) => + selectFlags(state).advancedConfiguration