Skip to content
Snippets Groups Projects
Commit fd76ab81 authored by mateusz-winiarczyk's avatar mateusz-winiarczyk
Browse files

feat(export): MIN-162 select annotation

parent 33770a77
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...,!88feat(export): MIN-162 select annotation
Showing
with 576 additions and 0 deletions
......@@ -7,6 +7,7 @@ import { SearchDrawerWrapper as SearchDrawerContent } from './SearchDrawerWrappe
import { SubmapsDrawer } from './SubmapsDrawer';
import { OverlaysDrawer } from './OverlaysDrawer';
import { BioEntityDrawer } from './BioEntityDrawer/BioEntityDrawer.component';
import { ExportDrawer } from './ExportDrawer';
export const Drawer = (): JSX.Element => {
const { isOpen, drawerName } = useAppSelector(drawerSelector);
......@@ -24,6 +25,7 @@ export const Drawer = (): JSX.Element => {
{isOpen && drawerName === 'reaction' && <ReactionDrawer />}
{isOpen && drawerName === 'overlays' && <OverlaysDrawer />}
{isOpen && drawerName === 'bio-entity' && <BioEntityDrawer />}
{isOpen && drawerName === 'export' && <ExportDrawer />}
</div>
);
};
import { render, screen, waitFor } from '@testing-library/react';
import {
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import { StoreType } from '@/redux/store';
import { statisticsFixture } from '@/models/fixtures/statisticsFixture';
import { act } from 'react-dom/test-utils';
import { Annotations } from './Annotations.component';
const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState);
return (
render(
<Wrapper>
<Annotations />
</Wrapper>,
),
{
store,
}
);
};
describe('Annotations - component', () => {
it('should display annotations checkboxes when fetching data is successful', async () => {
renderComponent({
statistics: {
data: {
...statisticsFixture,
elementAnnotations: {
compartment: 1,
pathway: 0,
},
},
loading: 'succeeded',
error: {
message: '',
name: '',
},
},
});
const navigationButton = screen.getByTestId('accordion-item-button');
act(() => {
navigationButton.click();
});
expect(screen.getByText('Select annotations')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByTestId('checkbox-filter')).toBeInTheDocument();
expect(screen.getByLabelText('compartment')).toBeInTheDocument();
expect(screen.getByLabelText('search-input')).toBeInTheDocument();
});
});
it('should not display annotations checkboxes when fetching data fails', async () => {
renderComponent({
statistics: {
data: undefined,
loading: 'failed',
error: {
message: '',
name: '',
},
},
});
expect(screen.getByText('Select annotations')).toBeInTheDocument();
const navigationButton = screen.getByTestId('accordion-item-button');
act(() => {
navigationButton.click();
});
expect(screen.queryByTestId('checkbox-filter')).not.toBeInTheDocument();
});
it('should not display annotations checkboxes when fetched data is empty object', async () => {
renderComponent({
statistics: {
data: {
...statisticsFixture,
elementAnnotations: {},
},
loading: 'failed',
error: {
message: '',
name: '',
},
},
});
expect(screen.getByText('Select annotations')).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({
statistics: {
data: undefined,
loading: 'pending',
error: {
message: '',
name: '',
},
},
});
expect(screen.getByText('Select annotations')).toBeInTheDocument();
const navigationButton = screen.getByTestId('accordion-item-button');
act(() => {
navigationButton.click();
});
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
});
/* eslint-disable no-magic-numbers */
import {
Accordion,
AccordionItem,
AccordionItemButton,
AccordionItemHeading,
AccordionItemPanel,
} from '@/shared/Accordion';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import {
elementAnnotationsSelector,
loadingStatisticsSelector,
} from '@/redux/statistics/statistics.selectors';
import { CheckboxFilter } from '../CheckboxFilter';
export const Annotations = (): React.ReactNode => {
const loadingStatistics = useAppSelector(loadingStatisticsSelector);
const elementAnnotations = useAppSelector(elementAnnotationsSelector);
const isPending = loadingStatistics === 'pending';
const mappedElementAnnotations = elementAnnotations
? Object.keys(elementAnnotations)?.map(el => ({ id: el, label: el }))
: [];
return (
<Accordion allowZeroExpanded>
<AccordionItem>
<AccordionItemHeading>
<AccordionItemButton>Select annotations</AccordionItemButton>
</AccordionItemHeading>
<AccordionItemPanel>
{isPending && <p>Loading...</p>}
{!isPending && mappedElementAnnotations && mappedElementAnnotations.length > 0 && (
<CheckboxFilter options={mappedElementAnnotations} />
)}
</AccordionItemPanel>
</AccordionItem>
</Accordion>
);
};
export { Annotations } from './Annotations.component';
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { CheckboxFilter } from './CheckboxFilter.component';
const options = [
{ id: '1', label: 'Option 1' },
{ id: '2', label: 'Option 2' },
{ id: '3', label: 'Option 3' },
];
describe('CheckboxFilter - component', () => {
it('should render CheckboxFilter properly', () => {
render(<CheckboxFilter options={options} />);
expect(screen.getByTestId('search')).toBeInTheDocument();
});
it('should filter options based on search term', async () => {
render(<CheckboxFilter options={options} />);
const searchInput = screen.getByLabelText('search-input');
fireEvent.change(searchInput, { target: { value: `Option 1` } });
expect(screen.getByLabelText('Option 1')).toBeInTheDocument();
expect(screen.queryByText('Option 2')).not.toBeInTheDocument();
expect(screen.queryByText('Option 3')).not.toBeInTheDocument();
});
it('should handle checkbox value change', async () => {
const onCheckedChange = jest.fn();
render(<CheckboxFilter options={options} onCheckedChange={onCheckedChange} />);
const checkbox = screen.getByLabelText('Option 1');
fireEvent.click(checkbox);
expect(onCheckedChange).toHaveBeenCalledWith([{ id: '1', label: 'Option 1' }]);
});
it('should call onFilterChange when searching new term', async () => {
const onFilterChange = jest.fn();
render(<CheckboxFilter options={options} onFilterChange={onFilterChange} />);
const searchInput = screen.getByLabelText('search-input');
fireEvent.change(searchInput, { target: { value: 'Option 1' } });
expect(onFilterChange).toHaveBeenCalledWith([{ id: '1', label: 'Option 1' }]);
});
it('should display message when no elements are found', async () => {
render(<CheckboxFilter options={options} />);
const searchInput = screen.getByLabelText('search-input');
fireEvent.change(searchInput, { target: { value: 'Nonexistent Option' } });
expect(screen.getByText('No matching elements found.')).toBeInTheDocument();
});
it('should display message when options are empty', () => {
const onFilterChange = jest.fn();
render(<CheckboxFilter 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} />);
const checkbox1 = screen.getByLabelText('Option 1');
const checkbox2 = screen.getByLabelText('Option 2');
fireEvent.click(checkbox1);
fireEvent.click(checkbox2);
expect(onCheckedChange).toHaveBeenCalledWith([
{ id: '1', label: 'Option 1' },
{ id: '2', label: 'Option 2' },
]);
});
it('should handle unchecking a checkbox', () => {
const onCheckedChange = jest.fn();
render(<CheckboxFilter options={options} onCheckedChange={onCheckedChange} />);
const checkbox = screen.getByLabelText('Option 1');
fireEvent.click(checkbox); // Check
fireEvent.click(checkbox); // Uncheck
expect(onCheckedChange).toHaveBeenCalledWith([]);
});
});
/* eslint-disable no-magic-numbers */
import Image from 'next/image';
import React, { useEffect, useState } from 'react';
import lensIcon from '@/assets/vectors/icons/lens.svg';
type CheckboxItem = { id: string; label: string };
type CheckboxFilterProps = {
options: CheckboxItem[];
onFilterChange?: (filteredItems: CheckboxItem[]) => void;
onCheckedChange?: (filteredItems: CheckboxItem[]) => void;
};
export const CheckboxFilter = ({
options,
onFilterChange,
onCheckedChange,
}: CheckboxFilterProps): React.ReactNode => {
const [searchTerm, setSearchTerm] = useState('');
const [filteredOptions, setFilteredOptions] = useState<CheckboxItem[]>(options);
const [checkedCheckboxes, setCheckedCheckboxes] = useState<CheckboxItem[]>([]);
const filterOptions = (term: string): void => {
const filteredItems = options.filter(item =>
item.label.toLowerCase().includes(term.toLowerCase()),
);
setFilteredOptions(filteredItems);
onFilterChange?.(filteredItems);
};
const handleSearchTermChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const newSearchTerm = e.target.value;
setSearchTerm(newSearchTerm);
filterOptions(newSearchTerm);
};
const handleCheckboxChange = (option: CheckboxItem): void => {
const newCheckedCheckboxes = checkedCheckboxes.includes(option)
? checkedCheckboxes.filter(item => item !== option)
: [...checkedCheckboxes, option];
setCheckedCheckboxes(newCheckedCheckboxes);
onCheckedChange?.(newCheckedCheckboxes);
};
useEffect(() => {
setFilteredOptions(options);
}, [options]);
return (
<div className="relative" data-testid="checkbox-filter">
<div className="relative" data-testid="search">
<input
name="search-input"
aria-label="search-input"
value={searchTerm}
onChange={handleSearchTermChange}
placeholder="Search..."
className="h-9 w-full rounded-[64px] border border-transparent bg-cultured px-4 py-2.5 text-xs font-medium text-font-400 outline-none hover:border-greyscale-600 focus:border-greyscale-600"
/>
<Image
src={lensIcon}
alt="lens icon"
height={16}
width={16}
className="absolute right-4 top-2.5"
/>
</div>
<div className="my-6 max-h-[250px] overflow-y-auto py-2.5 pr-2.5 ">
{filteredOptions.length === 0 ? (
<p className="w-full text-sm text-font-400">No matching elements found.</p>
) : (
<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)}
/>
<label htmlFor={option.id} className="break-all text-sm">
{option.label}
</label>
</li>
))}
</ul>
)}
</div>
</div>
);
};
export { CheckboxFilter } from './CheckboxFilter.component';
import { Annotations } from '../Annotations';
export const Elements = (): React.ReactNode => {
return (
<div data-testid="elements-tab">
<Annotations />
</div>
);
};
export { Elements } from './Elements.component';
import {
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import { StoreType } from '@/redux/store';
import { render, screen } from '@testing-library/react';
import { openedExportDrawerFixture } from '@/redux/drawer/drawerFixture';
import { act } from 'react-dom/test-utils';
import { ExportDrawer } from './ExportDrawer.component';
import { TAB_NAMES } from './TabNavigator/TabNavigator.constants';
const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState);
return (
render(
<Wrapper>
<ExportDrawer />
</Wrapper>,
),
{
store,
}
);
};
describe('ExportDrawer - component', () => {
it('should display drawer heading and tab names', () => {
renderComponent();
expect(screen.getByText('Export')).toBeInTheDocument();
Object.keys(TAB_NAMES).forEach(label => {
expect(screen.getByText(label)).toBeInTheDocument();
});
});
it('should close drawer after clicking close button', () => {
const { store } = renderComponent({
drawer: openedExportDrawerFixture,
});
const closeButton = screen.getByRole('close-drawer-button');
closeButton.click();
const {
drawer: { isOpen },
} = store.getState();
expect(isOpen).toBe(false);
});
it('should set elements as initial tab', () => {
renderComponent();
expect(screen.getByTestId('elements-tab')).toBeInTheDocument();
});
it('should set correct tab on tab change', () => {
renderComponent();
const currentTab = screen.getByRole('button', { current: true });
const networkTab = screen.getByText(/network/i);
const elementsTab = screen.getByTestId('elements-tab');
expect(currentTab).not.toBe(networkTab);
expect(screen.getByTestId('elements-tab')).toBeInTheDocument();
act(() => {
networkTab.click();
});
expect(screen.getByRole('button', { current: true })).toBe(networkTab);
expect(elementsTab).not.toBeInTheDocument();
});
});
import { DrawerHeading } from '@/shared/DrawerHeading';
import { useState } from 'react';
import { TabNavigator } from './TabNavigator';
import { Elements } from './Elements';
import { TAB_NAMES } from './TabNavigator/TabNavigator.constants';
import { TabNames } from './TabNavigator/TabNavigator.types';
export const ExportDrawer = (): React.ReactNode => {
const [activeTab, setActiveTab] = useState<TabNames>(TAB_NAMES.ELEMENTS);
const handleTabChange = (tabName: TabNames): void => {
setActiveTab(tabName);
};
return (
<div data-testid="export-drawer" className="h-full max-h-full">
<DrawerHeading title="Export" />
<div className="h-[calc(100%-93px)] max-h-[calc(100%-93px)] overflow-y-auto px-6">
<TabNavigator activeTab={activeTab} onTabChange={handleTabChange} />
{activeTab === TAB_NAMES.ELEMENTS && <Elements />}
</div>
</div>
);
};
import { RenderResult, fireEvent, render, screen } from '@testing-library/react';
import { TabButton } from './TabButton.component';
const mockHandleChangeTab = jest.fn();
const renderTabButton = (label: string, active = false): RenderResult =>
render(<TabButton label={label} handleChangeTab={mockHandleChangeTab} active={active} />);
describe('TabButton - component', () => {
it('should render TabButton with custom label', () => {
renderTabButton('Map');
expect(screen.getByText('Map')).toBeInTheDocument();
});
it('should handle click event', () => {
renderTabButton('Network');
fireEvent.click(screen.getByText('Network'));
expect(mockHandleChangeTab).toHaveBeenCalled();
});
it('should indicate active tab correctly', () => {
renderTabButton('Graphics', true);
const currentTab = screen.getByRole('button', { current: true });
expect(currentTab).toHaveTextContent('Graphics');
});
it('should indicate not active tab correctly', () => {
renderTabButton('Graphics');
const activeTab = screen.queryByRole('button', { current: true });
const graphicsTab = screen.getByRole('button', { current: false });
expect(activeTab).not.toBeInTheDocument();
expect(graphicsTab).toHaveTextContent('Graphics');
});
});
import { twMerge } from 'tailwind-merge';
type TabButtonProps = {
handleChangeTab: () => void;
active: boolean;
label: string;
};
export const TabButton = ({ handleChangeTab, active, label }: TabButtonProps): React.ReactNode => (
<button
type="button"
className={twMerge(
'text-sm font-normal text-[#979797]',
active &&
'relative py-2.5 font-semibold leading-6 text-cetacean-blue before:absolute before:inset-x-0 before:top-0 before:block before:h-1 before:rounded-b before:bg-primary-500 before:content-[""]',
)}
aria-current={active}
onClick={handleChangeTab}
>
{label}
</button>
);
export { TabButton } from './TabButton.component';
import { fireEvent, render, screen } from '@testing-library/react';
import { TAB_NAMES } from './TabNavigator.constants';
import { TabNavigator } from './TabNavigator.component';
const mockOnTabChange = jest.fn();
describe('TabNavigator - component', () => {
beforeEach(() => {
mockOnTabChange.mockReset();
});
it('should render TabNavigator with correct tabs', () => {
render(<TabNavigator activeTab="elements" onTabChange={mockOnTabChange} />);
Object.keys(TAB_NAMES).forEach(label => {
expect(screen.getByText(label)).toBeInTheDocument();
});
});
it('should change tabs correctly', () => {
render(<TabNavigator activeTab="elements" onTabChange={mockOnTabChange} />);
fireEvent.click(screen.getByText(/network/i));
expect(mockOnTabChange).toHaveBeenCalledWith('network');
fireEvent.click(screen.getByText(/graphics/i));
expect(mockOnTabChange).toHaveBeenCalledWith('graphics');
});
it('should set initial active tab', () => {
render(<TabNavigator activeTab="network" onTabChange={mockOnTabChange} />);
const currentTab = screen.getByRole('button', { current: true });
const networkTab = screen.getByText(/network/i);
expect(currentTab).toBe(networkTab);
});
});
import { TabButton } from '../TabButton';
import { TAB_NAMES } from './TabNavigator.constants';
import { TabNames } from './TabNavigator.types';
type TabNavigatorProps = {
activeTab: TabNames;
onTabChange: (tabName: TabNames) => void;
};
export const TabNavigator = ({ activeTab, onTabChange }: TabNavigatorProps): React.ReactNode => (
<div className="flex gap-5">
{Object.entries(TAB_NAMES).map(([label, tabName]) => (
<TabButton
key={tabName}
handleChangeTab={(): void => onTabChange(tabName)}
label={label}
active={activeTab === tabName}
/>
))}
</div>
);
export const TAB_NAMES = {
ELEMENTS: 'elements',
NETWORK: 'network',
GRAPHICS: 'graphics',
} as const;
import { TAB_NAMES } from './TabNavigator.constants';
export type TabNames = (typeof TAB_NAMES)[keyof typeof TAB_NAMES];
export { TabNavigator } from './TabNavigator.component';
export { ExportDrawer } from './ExportDrawer.component';
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