Skip to content
Snippets Groups Projects
Commit 29618c1f authored by Adrian Orłów's avatar Adrian Orłów :fire:
Browse files

Merge branch 'MIN-165-graphics-full-tab-business-logic' into 'development'

feat: add graphics full tab business logic (MIN-165)

Closes MIN-165

See merge request !113
parents 459df684 359c31b8
No related branches found
No related tags found
2 merge requests!223reset the pin numbers before search results are fetch (so the results will be...,!113feat: add graphics full tab business logic (MIN-165)
Pipeline #85229 passed
Showing
with 648 additions and 72 deletions
......@@ -43,6 +43,7 @@
"@commitlint/config-conventional": "^17.7.0",
"@testing-library/jest-dom": "^6.1.3",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.5",
"@types/react-redux": "^7.1.26",
"@types/redux-mock-store": "^1.0.6",
......@@ -2150,6 +2151,19 @@
"react-dom": "^18.0.0"
}
},
"node_modules/@testing-library/user-event": {
"version": "14.5.2",
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz",
"integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==",
"dev": true,
"engines": {
"node": ">=12",
"npm": ">=6"
},
"peerDependencies": {
"@testing-library/dom": ">=7.21.4"
}
},
"node_modules/@tootallnate/once": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
......@@ -15495,6 +15509,13 @@
"@types/react-dom": "^18.0.0"
}
},
"@testing-library/user-event": {
"version": "14.5.2",
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz",
"integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==",
"dev": true,
"requires": {}
},
"@tootallnate/once": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
......
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { fireEvent, render, screen } from '@testing-library/react';
import { CheckboxFilter } from './CheckboxFilter.component';
......@@ -9,14 +8,16 @@ const options = [
{ id: '3', label: 'Option 3' },
];
const currentOptions = [{ id: '2', label: 'Option 2' }];
describe('CheckboxFilter - component', () => {
it('should render CheckboxFilter properly', () => {
render(<CheckboxFilter options={options} />);
render(<CheckboxFilter options={options} currentOptions={[]} />);
expect(screen.getByTestId('search')).toBeInTheDocument();
});
it('should filter options based on search term', async () => {
render(<CheckboxFilter options={options} />);
render(<CheckboxFilter options={options} currentOptions={[]} />);
const searchInput = screen.getByLabelText('search-input');
fireEvent.change(searchInput, { target: { value: `Option 1` } });
......@@ -28,7 +29,26 @@ describe('CheckboxFilter - component', () => {
it('should handle checkbox value change', async () => {
const onCheckedChange = jest.fn();
render(<CheckboxFilter options={options} onCheckedChange={onCheckedChange} />);
render(
<CheckboxFilter currentOptions={[]} options={options} onCheckedChange={onCheckedChange} />,
);
const checkbox = screen.getByLabelText('Option 1');
fireEvent.click(checkbox);
expect(onCheckedChange).toHaveBeenCalledWith([{ id: '1', label: 'Option 1' }]);
});
it('should handle radio value change', async () => {
const onCheckedChange = jest.fn();
render(
<CheckboxFilter
currentOptions={[]}
type="radio"
options={options}
onCheckedChange={onCheckedChange}
/>,
);
const checkbox = screen.getByLabelText('Option 1');
fireEvent.click(checkbox);
......@@ -38,7 +58,9 @@ describe('CheckboxFilter - component', () => {
it('should call onFilterChange when searching new term', async () => {
const onFilterChange = jest.fn();
render(<CheckboxFilter options={options} onFilterChange={onFilterChange} />);
render(
<CheckboxFilter currentOptions={[]} options={options} onFilterChange={onFilterChange} />,
);
const searchInput = screen.getByLabelText('search-input');
fireEvent.change(searchInput, { target: { value: 'Option 1' } });
......@@ -46,7 +68,7 @@ describe('CheckboxFilter - component', () => {
expect(onFilterChange).toHaveBeenCalledWith([{ id: '1', label: 'Option 1' }]);
});
it('should display message when no elements are found', async () => {
render(<CheckboxFilter options={options} />);
render(<CheckboxFilter currentOptions={[]} options={options} />);
const searchInput = screen.getByLabelText('search-input');
fireEvent.change(searchInput, { target: { value: 'Nonexistent Option' } });
......@@ -55,13 +77,15 @@ describe('CheckboxFilter - component', () => {
});
it('should display message when options are empty', () => {
const onFilterChange = jest.fn();
render(<CheckboxFilter options={[]} onFilterChange={onFilterChange} />);
render(<CheckboxFilter currentOptions={[]} options={[]} onFilterChange={onFilterChange} />);
expect(screen.getByText('No matching elements found.')).toBeInTheDocument();
});
it('should handle multiple checkbox selection', () => {
const onCheckedChange = jest.fn();
render(<CheckboxFilter options={options} onCheckedChange={onCheckedChange} />);
render(
<CheckboxFilter currentOptions={[]} options={options} onCheckedChange={onCheckedChange} />,
);
const checkbox1 = screen.getByLabelText('Option 1');
const checkbox2 = screen.getByLabelText('Option 2');
......@@ -74,9 +98,33 @@ describe('CheckboxFilter - component', () => {
{ id: '2', label: 'Option 2' },
]);
});
it('should handle multiple change of radio selection', () => {
const onCheckedChange = jest.fn();
render(
<CheckboxFilter
currentOptions={[]}
options={options}
onCheckedChange={onCheckedChange}
type="radio"
/>,
);
const checkbox1 = screen.getByLabelText('Option 1');
const checkbox2 = screen.getByLabelText('Option 2');
fireEvent.click(checkbox1);
expect(onCheckedChange).toHaveBeenCalledWith([{ id: '1', label: 'Option 1' }]);
fireEvent.click(checkbox2);
expect(onCheckedChange).toHaveBeenCalledWith([{ id: '2', label: 'Option 2' }]);
});
it('should handle unchecking a checkbox', () => {
const onCheckedChange = jest.fn();
render(<CheckboxFilter options={options} onCheckedChange={onCheckedChange} />);
render(
<CheckboxFilter currentOptions={[]} options={options} onCheckedChange={onCheckedChange} />,
);
const checkbox = screen.getByLabelText('Option 1');
......@@ -86,19 +134,19 @@ describe('CheckboxFilter - component', () => {
expect(onCheckedChange).toHaveBeenCalledWith([]);
});
it('should render search input when isSearchEnabled is true', () => {
render(<CheckboxFilter options={options} />);
render(<CheckboxFilter currentOptions={[]} options={options} />);
const searchInput = screen.getByLabelText('search-input');
expect(searchInput).toBeInTheDocument();
});
it('should not render search input when isSearchEnabled is false', () => {
render(<CheckboxFilter options={options} isSearchEnabled={false} />);
render(<CheckboxFilter currentOptions={[]} options={options} isSearchEnabled={false} />);
const searchInput = screen.queryByLabelText('search-input');
expect(searchInput).not.toBeInTheDocument();
});
it('should not filter options based on search input when isSearchEnabled is false', () => {
render(<CheckboxFilter options={options} isSearchEnabled={false} />);
render(<CheckboxFilter currentOptions={[]} options={options} isSearchEnabled={false} />);
const searchInput = screen.queryByLabelText('search-input');
expect(searchInput).not.toBeInTheDocument();
options.forEach(option => {
......@@ -106,4 +154,15 @@ describe('CheckboxFilter - component', () => {
expect(checkboxLabel).toBeInTheDocument();
});
});
it('should set checked param based on currentOptions prop', async () => {
render(<CheckboxFilter options={options} currentOptions={currentOptions} />);
const option1: HTMLInputElement = screen.getByLabelText('Option 1');
const option2: HTMLInputElement = screen.getByLabelText('Option 2');
const option3: HTMLInputElement = screen.getByLabelText('Option 3');
expect(option1.checked).toBe(false);
expect(option2.checked).toBe(true);
expect(option3.checked).toBe(false);
});
});
/* eslint-disable no-magic-numbers */
import lensIcon from '@/assets/vectors/icons/lens.svg';
import Image from 'next/image';
import React, { useEffect, useState } from 'react';
import lensIcon from '@/assets/vectors/icons/lens.svg';
import { twMerge } from 'tailwind-merge';
export type CheckboxItem = { id: string; label: string };
import { CheckboxItem } from './CheckboxFilter.types';
import { OptionInput } from './OptionInput';
type CheckboxFilterProps = {
options: CheckboxItem[];
currentOptions: CheckboxItem[];
onFilterChange?: (filteredItems: CheckboxItem[]) => void;
onCheckedChange?: (filteredItems: CheckboxItem[]) => void;
isSearchEnabled?: boolean;
type?: 'checkbox' | 'radio';
};
export const CheckboxFilter = ({
options,
currentOptions = [],
onFilterChange,
onCheckedChange,
isSearchEnabled = true,
type = 'checkbox',
}: CheckboxFilterProps): React.ReactNode => {
const [searchTerm, setSearchTerm] = useState('');
const [filteredOptions, setFilteredOptions] = useState<CheckboxItem[]>(options);
......@@ -47,6 +51,11 @@ export const CheckboxFilter = ({
onCheckedChange?.(newCheckedCheckboxes);
};
const handleRadioChange = (option: CheckboxItem): void => {
setCheckedCheckboxes([option]);
onCheckedChange?.([option]);
};
useEffect(() => {
setFilteredOptions(options);
}, [options]);
......@@ -86,15 +95,13 @@ export const CheckboxFilter = ({
<ul className="columns-2 gap-8">
{filteredOptions.map(option => (
<li key={option.id} className="mb-5 flex items-center gap-x-2">
<input
type="checkbox"
id={option.id}
className=" h-4 w-4 shrink-0 accent-primary-500"
onChange={(): void => handleCheckboxChange(option)}
<OptionInput
option={option}
currentOptions={currentOptions}
handleRadioChange={handleRadioChange}
handleCheckboxChange={handleCheckboxChange}
type={type}
/>
<label htmlFor={option.id} className="break-all text-sm">
{option.label}
</label>
</li>
))}
</ul>
......
export type CheckboxItem = { id: string; label: string };
import { twMerge } from 'tailwind-merge';
import { CheckboxItem } from '../CheckboxFilter.types';
interface Props {
option: CheckboxItem;
currentOptions: CheckboxItem[];
type: 'checkbox' | 'radio';
handleCheckboxChange(option: CheckboxItem): void;
handleRadioChange(option: CheckboxItem): void;
}
export const OptionInput = ({
option,
currentOptions = [],
type,
handleCheckboxChange,
handleRadioChange,
}: Props): React.ReactNode => {
const isChecked = Boolean(currentOptions.find(currentOption => currentOption.id === option.id));
const handleChange = (): void => {
switch (type) {
case 'checkbox':
handleCheckboxChange(option);
break;
case 'radio':
handleRadioChange(option);
break;
default:
throw new Error(`${type} is unknown option input type`);
}
};
return (
<label className="flex items-center gap-x-2">
<input
type={type}
className={twMerge(
'h-4 w-4 shrink-0 accent-primary-500',
type === 'radio' && 'rounded-full',
)}
onChange={handleChange}
checked={isChecked}
/>
<div className="break-all text-sm">{option.label}</div>
</label>
);
};
export { OptionInput } from './OptionInput.component';
/* eslint-disable no-magic-numbers */
import { compartmentPathwaysDetailsFixture } from '@/models/fixtures/compartmentPathways';
import { configurationFixture } from '@/models/fixtures/configurationFixture';
import { modelsFixture } from '@/models/fixtures/modelsFixture';
import { statisticsFixture } from '@/models/fixtures/statisticsFixture';
import { apiPath } from '@/redux/apiPath';
import { CONFIGURATION_INITIAL_STORE_MOCK } from '@/redux/configuration/configuration.mock';
import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures';
import { AppDispatch, RootState } from '@/redux/store';
import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse';
import { getReduxStoreWithActionsListener } from '@/utils/testing/getReduxStoreActionsListener';
import { InitialStoreState } from '@/utils/testing/getReduxWrapperWithStore';
import { render, screen } from '@testing-library/react';
import { CONFIGURATION_INITIAL_STORE_MOCK } from '@/redux/configuration/configuration.mock';
import { configurationFixture } from '@/models/fixtures/configurationFixture';
import { HttpStatusCode } from 'axios';
import { act } from 'react-dom/test-utils';
import { statisticsFixture } from '@/models/fixtures/statisticsFixture';
import { compartmentPathwaysDetailsFixture } from '@/models/fixtures/compartmentPathways';
import { MockStoreEnhanced } from 'redux-mock-store';
import { getReduxStoreWithActionsListener } from '@/utils/testing/getReduxStoreActionsListener';
import { modelsFixture } from '@/models/fixtures/modelsFixture';
import { mockNetworkNewAPIResponse } from '@/utils/mockNetworkResponse';
import { apiPath } from '@/redux/apiPath';
import { HttpStatusCode } from 'axios';
import { Elements } from './Elements.component';
import { ELEMENTS_COLUMNS } from '../ExportCompound/ExportCompound.constant';
import { Elements } from './Elements.component';
const mockedAxiosClient = mockNetworkNewAPIResponse();
......@@ -37,6 +38,7 @@ const renderComponent = (
describe('Elements - component', () => {
it('should render all elements sections', () => {
renderComponent({
...INITIAL_STORE_STATE_MOCK,
configuration: {
...CONFIGURATION_INITIAL_STORE_MOCK,
main: {
......@@ -95,6 +97,7 @@ describe('Elements - component', () => {
const SECOND_COMPARMENT_PATHWAY_NAME = compartmentPathwaysDetailsFixture[1].name;
const SECOND_COMPARMENT_PATHWAY_ID = compartmentPathwaysDetailsFixture[1].id;
const { store } = renderComponent({
...INITIAL_STORE_STATE_MOCK,
configuration: {
...CONFIGURATION_INITIAL_STORE_MOCK,
main: {
......
import { useContext } from 'react';
import { ZERO } from '@/constants/common';
import { miramiTypesSelector } from '@/redux/configuration/configuration.selectors';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import {
loadingStatisticsSelector,
statisticsDataSelector,
} from '@/redux/statistics/statistics.selectors';
import { ZERO } from '@/constants/common';
import { miramiTypesSelector } from '@/redux/configuration/configuration.selectors';
import { useContext } from 'react';
import { CheckboxFilter } from '../../CheckboxFilter';
import { CollapsibleSection } from '../../CollapsibleSection';
import { ExportContext } from '../ExportCompound.context';
import { getAnnotationsCheckboxElements } from './Annotations.utils';
import { AnnotationsType } from './Annotations.types';
import { getAnnotationsCheckboxElements } from './Annotations.utils';
type AnnotationsProps = {
type: AnnotationsType;
};
export const Annotations = ({ type }: AnnotationsProps): React.ReactNode => {
const { setAnnotations } = useContext(ExportContext);
const { setAnnotations, data } = useContext(ExportContext);
const currentAnnotations = data.annotations;
const loadingStatistics = useAppSelector(loadingStatisticsSelector);
const statistics = useAppSelector(statisticsDataSelector);
const miramiTypes = useAppSelector(miramiTypesSelector);
......@@ -28,7 +29,11 @@ export const Annotations = ({ type }: AnnotationsProps): React.ReactNode => {
<CollapsibleSection title="Select annotations">
{isPending && <p>Loading...</p>}
{!isPending && checkboxElements && checkboxElements.length > ZERO && (
<CheckboxFilter options={checkboxElements} onCheckedChange={setAnnotations} />
<CheckboxFilter
options={checkboxElements}
currentOptions={currentAnnotations}
onCheckedChange={setAnnotations}
/>
)}
</CollapsibleSection>
);
......
import { Button } from '@/shared/Button';
import { useContext } from 'react';
import { ExportContext } from '../ExportCompound.context';
export const DownloadGraphics = (): React.ReactNode => {
const { handleDownloadGraphics } = useContext(ExportContext);
return (
<div className="mt-6">
<Button onClick={handleDownloadGraphics}>Download</Button>
</div>
);
};
export { DownloadGraphics } from './DownloadGraphics.component';
import { useContext } from 'react';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { ZERO } from '@/constants/common';
import {
compartmentPathwaysDataSelector,
loadingCompartmentPathwaysSelector,
} from '@/redux/compartmentPathways/compartmentPathways.selectors';
import { ZERO } from '@/constants/common';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { useContext } from 'react';
import { CheckboxFilter } from '../../CheckboxFilter';
import { CollapsibleSection } from '../../CollapsibleSection';
import { ExportContext } from '../ExportCompound.context';
import { getCompartmentPathwaysCheckboxElements } from '../utils/getCompartmentPathwaysCheckboxElements';
export const ExcludedCompartmentPathways = (): React.ReactNode => {
const { setExcludedCompartmentPathways } = useContext(ExportContext);
const { setExcludedCompartmentPathways, data } = useContext(ExportContext);
const currentExcludedCompartmentPathways = data.excludedCompartmentPathways;
const loadingCompartmentPathways = useAppSelector(loadingCompartmentPathwaysSelector);
const isPending = loadingCompartmentPathways === 'pending';
const compartmentPathways = useAppSelector(compartmentPathwaysDataSelector);
......@@ -24,6 +25,7 @@ export const ExcludedCompartmentPathways = (): React.ReactNode => {
{isCheckboxFilterVisible && (
<CheckboxFilter
options={checkboxElements}
currentOptions={currentExcludedCompartmentPathways}
onCheckedChange={setExcludedCompartmentPathways}
/>
)}
......
import { ReactNode, useCallback, useMemo, useState } from 'react';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { modelsIdsSelector } from '@/redux/models/models.selectors';
import { FIRST_ARRAY_ELEMENT } from '@/constants/common';
import { currentBackgroundSelector } from '@/redux/backgrounds/background.selectors';
import { downloadElements, downloadNetwork } from '@/redux/export/export.thunks';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { downloadNetwork, downloadElements } from '@/redux/export/export.thunks';
import { CheckboxItem } from '../CheckboxFilter/CheckboxFilter.component';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { modelsDataSelector, modelsIdsSelector } from '@/redux/models/models.selectors';
import { ReactNode, useCallback, useMemo, useState } from 'react';
import { CheckboxItem } from '../CheckboxFilter/CheckboxFilter.types';
import { Annotations } from './Annotations';
import { ExcludedCompartmentPathways } from './ExcludedCompartmentPathways';
import { IncludedCompartmentPathways } from './IncludedCompartmentPathways ';
import { DownloadElements } from './DownloadElements/DownloadElements';
import { ExportContext } from './ExportCompound.context';
import { getNetworkDownloadBodyRequest } from './utils/getNetworkBodyRequest';
import { getDownloadElementsBodyRequest } from './utils/getDownloadElementsBodyRequest';
import { DownloadGraphics } from './DownloadGraphics';
import { DownloadNetwork } from './DownloadNetwork/DownloadNetwork';
import { ExcludedCompartmentPathways } from './ExcludedCompartmentPathways';
import { ELEMENTS_COLUMNS, NETWORK_COLUMNS } from './ExportCompound.constant';
import { ExportContext } from './ExportCompound.context';
import { ImageFormat } from './ImageFormat';
import { ImageSize } from './ImageSize';
import { DEFAULT_IMAGE_SIZE } from './ImageSize/ImageSize.constants';
import { ImageSize as ImageSizeType } from './ImageSize/ImageSize.types';
import { IncludedCompartmentPathways } from './IncludedCompartmentPathways ';
import { Submap } from './Submap';
import { getDownloadElementsBodyRequest } from './utils/getDownloadElementsBodyRequest';
import { getGraphicsDownloadUrl } from './utils/getGraphicsDownloadUrl';
import { getModelExportZoom } from './utils/getModelExportZoom';
import { getNetworkDownloadBodyRequest } from './utils/getNetworkBodyRequest';
type ExportProps = {
children: ReactNode;
......@@ -20,14 +30,19 @@ type ExportProps = {
export const Export = ({ children }: ExportProps): JSX.Element => {
const dispatch = useAppDispatch();
const [annotations, setAnnotations] = useState<CheckboxItem[]>([]);
const modelIds = useAppSelector(modelsIdsSelector);
const currentModels = useAppSelector(modelsDataSelector);
const currentBackground = useAppSelector(currentBackgroundSelector);
const [annotations, setAnnotations] = useState<CheckboxItem[]>([]);
const [includedCompartmentPathways, setIncludedCompartmentPathways] = useState<CheckboxItem[]>(
[],
);
const [excludedCompartmentPathways, setExcludedCompartmentPathways] = useState<CheckboxItem[]>(
[],
);
const [models, setModels] = useState<CheckboxItem[]>([]);
const [imageSize, setImageSize] = useState<ImageSizeType>(DEFAULT_IMAGE_SIZE);
const [imageFormats, setImageFormats] = useState<CheckboxItem[]>([]);
const handleDownloadElements = useCallback(async () => {
const body = getDownloadElementsBodyRequest({
......@@ -53,15 +68,55 @@ export const Export = ({ children }: ExportProps): JSX.Element => {
dispatch(downloadNetwork(data));
}, [modelIds, annotations, includedCompartmentPathways, excludedCompartmentPathways, dispatch]);
const handleDownloadGraphics = useCallback(async () => {
const modelId = models?.[FIRST_ARRAY_ELEMENT]?.id;
const model = currentModels.find(currentModel => currentModel.idObject === Number(modelId));
const url = getGraphicsDownloadUrl({
backgroundId: currentBackground?.id,
modelId: models?.[FIRST_ARRAY_ELEMENT]?.id,
handler: imageFormats?.[FIRST_ARRAY_ELEMENT]?.id,
zoom: getModelExportZoom(imageSize.width, model),
});
if (url) {
window.open(url);
}
}, [models, imageFormats, currentBackground, currentModels, imageSize.width]);
const globalContextDataValue = useMemo(
() => ({
annotations,
includedCompartmentPathways,
excludedCompartmentPathways,
models,
imageSize,
imageFormats,
}),
[
annotations,
includedCompartmentPathways,
excludedCompartmentPathways,
models,
imageSize,
imageFormats,
],
);
const globalContextValue = useMemo(
() => ({
setAnnotations,
setIncludedCompartmentPathways,
setExcludedCompartmentPathways,
setModels,
setImageSize,
setImageFormats,
handleDownloadElements,
handleDownloadNetwork,
handleDownloadGraphics,
data: globalContextDataValue,
}),
[handleDownloadElements, handleDownloadNetwork],
[handleDownloadElements, handleDownloadNetwork, globalContextDataValue, handleDownloadGraphics],
);
return <ExportContext.Provider value={globalContextValue}>{children}</ExportContext.Provider>;
......@@ -71,4 +126,8 @@ Export.Annotations = Annotations;
Export.IncludedCompartmentPathways = IncludedCompartmentPathways;
Export.ExcludedCompartmentPathways = ExcludedCompartmentPathways;
Export.DownloadElements = DownloadElements;
Export.Submap = Submap;
Export.ImageSize = ImageSize;
Export.ImageFormat = ImageFormat;
Export.DownloadNetwork = DownloadNetwork;
Export.DownloadGraphics = DownloadGraphics;
import { ExportContextType } from './ExportCompound.types';
import { DEFAULT_IMAGE_SIZE } from './ImageSize/ImageSize.constants';
export const ANNOTATIONS_TYPE = {
ELEMENTS: 'Elements',
NETWORK: 'Network',
......@@ -43,3 +46,23 @@ export const NETWORK_COLUMNS = [
'modelId',
'mapName',
];
export const EXPORT_CONTEXT_DEFAULT_VALUE: ExportContextType = {
setAnnotations: () => {},
setIncludedCompartmentPathways: () => {},
setExcludedCompartmentPathways: () => {},
setModels: () => {},
setImageSize: () => {},
setImageFormats: () => {},
handleDownloadElements: () => {},
handleDownloadNetwork: () => {},
handleDownloadGraphics: () => {},
data: {
annotations: [],
includedCompartmentPathways: [],
excludedCompartmentPathways: [],
models: [],
imageFormats: [],
imageSize: DEFAULT_IMAGE_SIZE,
},
};
import { createContext } from 'react';
import { CheckboxItem } from '../CheckboxFilter/CheckboxFilter.component';
import { EXPORT_CONTEXT_DEFAULT_VALUE } from './ExportCompound.constant';
import { ExportContextType } from './ExportCompound.types';
export type ExportContextType = {
setAnnotations: React.Dispatch<React.SetStateAction<CheckboxItem[]>>;
setIncludedCompartmentPathways: React.Dispatch<React.SetStateAction<CheckboxItem[]>>;
setExcludedCompartmentPathways: React.Dispatch<React.SetStateAction<CheckboxItem[]>>;
handleDownloadElements: () => void;
handleDownloadNetwork: () => void;
};
export const ExportContext = createContext<ExportContextType>({
setAnnotations: () => {},
setIncludedCompartmentPathways: () => {},
setExcludedCompartmentPathways: () => {},
handleDownloadElements: () => {},
handleDownloadNetwork: () => {},
});
export const ExportContext = createContext<ExportContextType>(EXPORT_CONTEXT_DEFAULT_VALUE);
import { CheckboxItem } from '../CheckboxFilter/CheckboxFilter.types';
import { ImageSize } from './ImageSize/ImageSize.types';
export type ExportContextType = {
setAnnotations: React.Dispatch<React.SetStateAction<CheckboxItem[]>>;
setIncludedCompartmentPathways: React.Dispatch<React.SetStateAction<CheckboxItem[]>>;
setExcludedCompartmentPathways: React.Dispatch<React.SetStateAction<CheckboxItem[]>>;
setModels: React.Dispatch<React.SetStateAction<CheckboxItem[]>>;
setImageSize: React.Dispatch<React.SetStateAction<ImageSize>>;
setImageFormats: React.Dispatch<React.SetStateAction<CheckboxItem[]>>;
handleDownloadElements: () => void;
handleDownloadNetwork: () => void;
handleDownloadGraphics: () => void;
data: {
annotations: CheckboxItem[];
includedCompartmentPathways: CheckboxItem[];
excludedCompartmentPathways: CheckboxItem[];
models: CheckboxItem[];
imageSize: ImageSize;
imageFormats: CheckboxItem[];
};
};
import { configurationFixture } from '@/models/fixtures/configurationFixture';
import {
CONFIGURATION_IMAGE_FORMATS_MOCK,
CONFIGURATION_IMAGE_FORMATS_TYPES_MOCK,
} from '@/models/mocks/configurationFormatsMock';
import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures';
import { StoreType } from '@/redux/store';
import { InitialStoreState } from '@/utils/testing/getReduxStoreActionsListener';
import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
import { act, render, screen, waitFor } from '@testing-library/react';
import { ImageFormat } from './ImageFormat.component';
const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState);
return (
render(
<Wrapper>
<ImageFormat />
</Wrapper>,
),
{
store,
}
);
};
describe('ImageFormat - component', () => {
it('should display formats checkboxes when fetching data is successful', async () => {
renderComponent({
configuration: {
...INITIAL_STORE_STATE_MOCK.configuration,
main: {
...INITIAL_STORE_STATE_MOCK.configuration.main,
data: {
...configurationFixture,
imageFormats: CONFIGURATION_IMAGE_FORMATS_MOCK,
},
},
},
});
expect(screen.queryByTestId('checkbox-filter')).not.toBeVisible();
const navigationButton = screen.getByTestId('accordion-item-button');
act(() => {
navigationButton.click();
});
expect(screen.getByText('Image format')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByTestId('checkbox-filter')).toBeInTheDocument();
CONFIGURATION_IMAGE_FORMATS_TYPES_MOCK.map(formatName =>
expect(screen.getByLabelText(formatName)).toBeInTheDocument(),
);
});
});
it('should not display formats checkboxes when fetching data fails', async () => {
renderComponent({
configuration: {
...INITIAL_STORE_STATE_MOCK.configuration,
main: {
...INITIAL_STORE_STATE_MOCK.configuration.main,
loading: 'failed',
},
},
});
expect(screen.getByText('Image format')).toBeInTheDocument();
const navigationButton = screen.getByTestId('accordion-item-button');
act(() => {
navigationButton.click();
});
expect(screen.queryByTestId('checkbox-filter')).not.toBeInTheDocument();
});
it('should not display formats checkboxes when fetched data is empty', async () => {
renderComponent({
configuration: {
...INITIAL_STORE_STATE_MOCK.configuration,
main: {
...INITIAL_STORE_STATE_MOCK.configuration.main,
data: {
...configurationFixture,
modelFormats: [],
imageFormats: [],
},
},
},
});
expect(screen.getByText('Image format')).toBeInTheDocument();
const navigationButton = screen.getByTestId('accordion-item-button');
act(() => {
navigationButton.click();
});
expect(screen.queryByTestId('checkbox-filter')).not.toBeInTheDocument();
});
it('should display loading message when fetching data is pending', async () => {
renderComponent({
configuration: {
...INITIAL_STORE_STATE_MOCK.configuration,
main: {
...INITIAL_STORE_STATE_MOCK.configuration.main,
loading: 'pending',
},
},
});
expect(screen.getByText('Image format')).toBeInTheDocument();
const navigationButton = screen.getByTestId('accordion-item-button');
act(() => {
navigationButton.click();
});
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
});
import { ZERO } from '@/constants/common';
import {
imageHandlersSelector,
loadingConfigurationMainSelector,
} from '@/redux/configuration/configuration.selectors';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { useContext } from 'react';
import { CheckboxFilter } from '../../CheckboxFilter';
import { CollapsibleSection } from '../../CollapsibleSection';
import { ExportContext } from '../ExportCompound.context';
export const ImageFormat = (): React.ReactNode => {
const { setImageFormats, data } = useContext(ExportContext);
const currentImageFormats = data.imageFormats;
const imageHandlers = useAppSelector(imageHandlersSelector);
const loadingConfigurationMain = useAppSelector(loadingConfigurationMainSelector);
const isPending = loadingConfigurationMain === 'pending';
const mappedElementAnnotations = Object.entries(imageHandlers)
.filter(([, handler]) => Boolean(handler))
.map(([name, handler]) => ({
id: handler,
label: name,
}));
return (
<CollapsibleSection title="Image format">
{isPending && <p>Loading...</p>}
{!isPending && mappedElementAnnotations.length > ZERO && (
<CheckboxFilter
options={mappedElementAnnotations}
currentOptions={currentImageFormats}
onCheckedChange={setImageFormats}
type="radio"
isSearchEnabled={false}
/>
)}
</CollapsibleSection>
);
};
export { ImageFormat } from './ImageFormat.component';
/* eslint-disable no-magic-numbers */
import { StoreType } from '@/redux/store';
import { InitialStoreState } from '@/utils/testing/getReduxStoreActionsListener';
import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
import { fireEvent, render, screen } from '@testing-library/react';
import { Export } from '../ExportCompound.component';
import { ImageSize } from './ImageSize.component';
import { ImageSize as ImageSizeType } from './ImageSize.types';
const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStore);
return (
render(
<Wrapper>
<Export>
<ImageSize />
</Export>
</Wrapper>,
),
{
store,
}
);
};
describe('ImageSize - component', () => {
describe('width input', () => {
it('renders input with valid value', () => {
renderComponent();
const widthInput: HTMLInputElement = screen.getByLabelText('export graphics width input');
expect(widthInput).toBeInTheDocument();
expect(widthInput.value).toBe('600');
});
// MAX_WIDTH 600
// MAX_HEIGHT 200
const widthCases: [number, ImageSizeType][] = [
[
// default
600,
{
width: 600,
height: 200,
},
],
[
// aspect ratio test
100,
{
width: 100,
height: 33,
},
],
[
// transform to integer
120.2137,
{
width: 120,
height: 40,
},
],
[
// max width
997,
{
width: 600,
height: 200,
},
],
];
it.each(widthCases)(
'handles input events by setting correct values',
async (newWidth, newImageSize) => {
renderComponent();
const widthInput: HTMLInputElement = screen.getByLabelText('export graphics width input');
const heightInput: HTMLInputElement = screen.getByLabelText('export graphics height input');
fireEvent.change(widthInput, { target: { value: `${newWidth}` } });
expect(widthInput).toHaveValue(newImageSize.width);
expect(heightInput).toHaveValue(newImageSize.height);
},
);
});
describe('height input', () => {
it('renders input', () => {
renderComponent();
const heightInput: HTMLInputElement = screen.getByLabelText('export graphics height input');
expect(heightInput).toBeInTheDocument();
expect(heightInput.value).toBe('200');
});
// MAX_WIDTH 600
// MAX_HEIGHT 200
const heightCases: [number, ImageSizeType][] = [
[
// default
200,
{
width: 600,
height: 200,
},
],
[
// aspect ratio test
100,
{
width: 300,
height: 100,
},
],
[
// transform to integer
120.2137,
{
width: 361,
height: 120,
},
],
[
// max height
997,
{
width: 600,
height: 200,
},
],
];
it.each(heightCases)(
'handles input events by setting correct values',
async (newHeight, newImageSize) => {
renderComponent();
const widthInput: HTMLInputElement = screen.getByLabelText('export graphics width input');
const heightInput: HTMLInputElement = screen.getByLabelText('export graphics height input');
fireEvent.change(heightInput, { target: { value: `${newHeight}` } });
expect(widthInput).toHaveValue(newImageSize.width);
expect(heightInput).toHaveValue(newImageSize.height);
},
);
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment