Smart filters improvements (#1814)

* Add Filters add button and remove the Add Filter Icon

* Generalize DeleteFilterModal and write tests suites + add custom addModal hook

* add react-testing-hook-lib + add tests to useModal hook

* Add parameter to ConfirmationModal + remove delete Modal add generic modal in add filter page

* implementing the modal hook on add Filter

* Finalize the Smart Filters functionality

* Styling code modifications

* Styling code modifications

* Filters styling code modifcations

* minor modifications in the tests suites

* minor tests suites description modifications

* minor tests suites code modifications

* Adding unNamed Filter selection option + tests suites

* Fix typo

Signed-off-by: Roman Zabaluev <rzabaluev@provectus.com>

* Adding tests Wuites to AddEditFilterContainer and to addFilter

* Add Filters add button and remove the Add Filter Icon

* Generalize DeleteFilterModal and write tests suites + add custom addModal hook

* add react-testing-hook-lib + add tests to useModal hook

* Add parameter to ConfirmationModal + remove delete Modal add generic modal in add filter page

* implementing the modal hook on add Filter

* Finalize the Smart Filters functionality

* Styling code modifications

* Styling code modifications

* Filters styling code modifcations

* minor modifications in the tests suites

* minor tests suites description modifications

* minor tests suites code modifications

* Adding unNamed Filter selection option + tests suites

* Fix typo

Signed-off-by: Roman Zabaluev <rzabaluev@provectus.com>

* Adding tests Wuites to AddEditFilterContainer and to addFilter

* q parameter modifications in the SmartFilters

* Add popup close functionality after applying the filters

Co-authored-by: Roman Zabaluev <rzabaluev@provectus.com>
This commit is contained in:
Mgrdich 2022-04-18 14:14:53 +04:00 committed by GitHub
parent 76c5fae4dd
commit 6eb6bb1d7e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 830 additions and 475 deletions

View file

@ -4869,6 +4869,19 @@
"@testing-library/dom": "^8.0.0"
}
},
"@testing-library/react-hooks": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-7.0.2.tgz",
"integrity": "sha512-dYxpz8u9m4q1TuzfcUApqi8iFfR6R0FaMbr2hjZJy1uC8z+bO/K4v8Gs9eogGKYQop7QsrBTFkv/BCF7MzD2Cg==",
"dev": true,
"requires": {
"@babel/runtime": "^7.12.5",
"@types/react": ">=16.9.0",
"@types/react-dom": ">=16.9.0",
"@types/react-test-renderer": ">=16.9.0",
"react-error-boundary": "^3.1.0"
}
},
"@testing-library/user-event": {
"version": "13.5.0",
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz",
@ -17194,6 +17207,15 @@
"scheduler": "^0.20.2"
}
},
"react-error-boundary": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz",
"integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==",
"dev": true,
"requires": {
"@babel/runtime": "^7.12.5"
}
},
"react-error-overlay": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz",

View file

@ -82,6 +82,7 @@
"@openapitools/openapi-generator-cli": "^2.4.15",
"@testing-library/dom": "^8.11.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react-hooks": "^7.0.2",
"@testing-library/user-event": "^13.5.0",
"@types/classnames": "^2.2.11",
"@types/enzyme": "^3.10.9",

View file

@ -7,41 +7,38 @@ import { FormProvider, Controller, useForm } from 'react-hook-form';
import { ErrorMessage } from '@hookform/error-message';
import { Button } from 'components/common/Button/Button';
import { FormError } from 'components/common/Input/Input.styled';
import { MessageFilters } from 'components/Topics/Topic/Details/Messages/Filters/Filters';
import { AddMessageFilters } from 'components/Topics/Topic/Details/Messages/Filters/AddFilter';
import { yupResolver } from '@hookform/resolvers/yup';
import yup from 'lib/yupExtended';
const validationSchema = yup.object().shape({
name: yup.string().required(),
saveFilter: yup.boolean(),
code: yup.string().required(),
name: yup.string().when('saveFilter', {
is: (value: boolean | undefined) => typeof value === 'undefined' || value,
then: (schema) => schema.required(),
otherwise: (schema) => schema.notRequired(),
}),
});
export interface AddEditFilterContainerProps {
title: string;
cancelBtnHandler: () => void;
submitBtnText: string;
inputDisplayNameDefaultValue?: string;
inputCodeDefaultValue?: string;
toggleSaveFilterValue?: boolean;
toggleSaveFilterSetter?: () => void;
createNewFilterText?: string;
submitCallback?: (values: MessageFilters) => void;
submitCallbackWithReset?: boolean;
isAdd?: boolean;
submitCallback?: (values: AddMessageFilters) => void;
}
const AddEditFilterContainer: React.FC<AddEditFilterContainerProps> = ({
title,
cancelBtnHandler,
submitBtnText,
inputDisplayNameDefaultValue = '',
inputCodeDefaultValue = '',
toggleSaveFilterValue,
toggleSaveFilterSetter,
createNewFilterText,
submitCallback,
submitCallbackWithReset,
isAdd,
}) => {
const methods = useForm<MessageFilters>({
const methods = useForm<AddMessageFilters>({
mode: 'onChange',
resolver: yupResolver(validationSchema),
});
@ -53,88 +50,77 @@ const AddEditFilterContainer: React.FC<AddEditFilterContainerProps> = ({
} = methods;
const onSubmit = React.useCallback(
(values: MessageFilters) => {
(values: AddMessageFilters) => {
submitCallback?.(values);
if (submitCallbackWithReset) {
reset({ name: '', code: '' });
}
reset({ name: '', code: '', saveFilter: false });
},
[reset, submitCallback, submitCallbackWithReset]
[isAdd, reset, submitCallback]
);
return (
<>
<S.FilterTitle>{title}</S.FilterTitle>
<FormProvider {...methods}>
{createNewFilterText && (
<S.CreatedFilter>{createNewFilterText}</S.CreatedFilter>
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)} aria-label="Filters submit Form">
<div>
<InputLabel>Filter code</InputLabel>
<Controller
control={control}
name="code"
defaultValue={inputCodeDefaultValue}
render={({ field: { onChange, ref } }) => (
<Textarea ref={ref} onChange={onChange} />
)}
/>
</div>
<div>
<FormError>
<ErrorMessage errors={errors} name="code" />
</FormError>
</div>
{isAdd && (
<S.CheckboxWrapper>
<input
{...methods.register('saveFilter')}
name="saveFilter"
type="checkbox"
/>
<InputLabel>Save this filter</InputLabel>
</S.CheckboxWrapper>
)}
<form
onSubmit={handleSubmit(onSubmit)}
aria-label="Filters submit Form"
>
<div>
<InputLabel>Display name</InputLabel>
<Input
inputSize="M"
placeholder="Enter Name"
autoComplete="off"
name="name"
defaultValue={inputDisplayNameDefaultValue}
/>
</div>
<div>
<FormError>
<ErrorMessage errors={errors} name="name" />
</FormError>
</div>
<div>
<InputLabel>Filter code</InputLabel>
<Controller
control={control}
name="code"
defaultValue={inputCodeDefaultValue}
render={({ field: { onChange, ref } }) => (
<Textarea ref={ref} onChange={onChange} />
)}
/>
</div>
<div>
<FormError>
<ErrorMessage errors={errors} name="code" />
</FormError>
</div>
{!!toggleSaveFilterSetter && (
<S.CheckboxWrapper>
<input
type="checkbox"
checked={toggleSaveFilterValue}
onChange={toggleSaveFilterSetter}
/>
<InputLabel>Save this filter</InputLabel>
</S.CheckboxWrapper>
)}
<S.FilterButtonWrapper>
<Button
buttonSize="M"
buttonType="secondary"
type="button"
onClick={cancelBtnHandler}
>
Cancel
</Button>
<Button
buttonSize="M"
buttonType="primary"
type="submit"
disabled={!isValid || isSubmitting || !isDirty}
>
{submitBtnText}
</Button>
</S.FilterButtonWrapper>
</form>
</FormProvider>
</>
<div>
<InputLabel>Display name</InputLabel>
<Input
inputSize="M"
placeholder="Enter Name"
autoComplete="off"
name="name"
defaultValue={inputDisplayNameDefaultValue}
/>
</div>
<div>
<FormError>
<ErrorMessage errors={errors} name="name" />
</FormError>
</div>
<S.FilterButtonWrapper>
<Button
buttonSize="M"
buttonType="secondary"
type="button"
onClick={cancelBtnHandler}
>
Cancel
</Button>
<Button
buttonSize="M"
buttonType="primary"
type="submit"
disabled={!isValid || isSubmitting || !isDirty}
>
{submitBtnText}
</Button>
</S.FilterButtonWrapper>
</form>
</FormProvider>
);
};

View file

@ -1,8 +1,9 @@
import React from 'react';
import * as S from 'components/Topics/Topic/Details/Messages/Filters/Filters.styled';
import { Button } from 'components/common/Button/Button';
import { MessageFilters } from 'components/Topics/Topic/Details/Messages/Filters/Filters';
import { FilterEdit } from 'components/Topics/Topic/Details/Messages/Filters/FilterModal';
import SavedFilters from 'components/Topics/Topic/Details/Messages/Filters/SavedFilters';
import SavedIcon from 'components/common/Icons/SavedIcon';
import AddEditFilterContainer from './AddEditFilterContainer';
@ -16,6 +17,10 @@ export interface FilterModalProps {
editFilter(value: FilterEdit): void;
}
export interface AddMessageFilters extends MessageFilters {
saveFilter: boolean;
}
const AddFilter: React.FC<FilterModalProps> = ({
toggleIsOpen,
filters,
@ -25,139 +30,54 @@ const AddFilter: React.FC<FilterModalProps> = ({
toggleEditModal,
editFilter,
}) => {
const [addNewFilter, setAddNewFilter] = React.useState(false);
const [toggleSaveFilter, setToggleSaveFilter] = React.useState(false);
const [selectedFilter, setSelectedFilter] = React.useState(-1);
const [toggleDeletionModal, setToggleDeletionModal] =
const [savedFilterState, setSavedFilterState] =
React.useState<boolean>(false);
const [deleteIndex, setDeleteIndex] = React.useState<number>(-1);
const deleteFilterHandler = (index: number) => {
setToggleDeletionModal(!toggleDeletionModal);
setDeleteIndex(index);
};
const activeFilter = () => {
if (selectedFilter > -1) {
activeFilterHandler(filters[selectedFilter], selectedFilter);
toggleIsOpen();
}
};
const onSubmit = React.useCallback(
async (values: MessageFilters) => {
if (!toggleSaveFilter) {
activeFilterHandler(values, -1);
async (values: AddMessageFilters) => {
const data = { ...values };
if (data.saveFilter) {
addFilter(data);
} else {
addFilter(values);
// other case is not applying the filter
data.name = data.name ? data.name : 'Unsaved filter';
activeFilterHandler(data, -1);
toggleIsOpen();
}
setAddNewFilter(!addNewFilter);
},
[addNewFilter, toggleSaveFilter, activeFilterHandler, addFilter]
[activeFilterHandler, addFilter, toggleIsOpen]
);
return !addNewFilter ? (
return (
<>
<S.FilterTitle>Add filter</S.FilterTitle>
<S.NewFilterIcon onClick={() => setAddNewFilter(!addNewFilter)}>
<i className="fas fa-plus fa-sm" /> New filter
</S.NewFilterIcon>
<S.CreatedFilter>Created filters</S.CreatedFilter>
{toggleDeletionModal && (
<S.ConfirmDeletionModal>
<S.ConfirmDeletionModalHeader>
<S.ConfirmDeletionTitle>Confirm deletion</S.ConfirmDeletionTitle>
<S.CloseDeletionModalIcon
data-testid="closeDeletionModalIcon"
onClick={() => setToggleDeletionModal(!toggleDeletionModal)}
>
<i className="fas fa-times-circle" />
</S.CloseDeletionModalIcon>
</S.ConfirmDeletionModalHeader>
<S.ConfirmDeletionText>
Are you sure want to remove {filters[deleteIndex].name}?
</S.ConfirmDeletionText>
<S.FilterButtonWrapper>
<Button
buttonSize="M"
buttonType="secondary"
type="button"
onClick={() => setToggleDeletionModal(!toggleDeletionModal)}
>
Cancel
</Button>
<Button
buttonSize="M"
buttonType="primary"
type="button"
onClick={() => {
deleteFilter(deleteIndex);
setToggleDeletionModal(!toggleDeletionModal);
}}
>
Delete
</Button>
</S.FilterButtonWrapper>
</S.ConfirmDeletionModal>
)}
<S.SavedFiltersContainer>
{filters.length === 0 && <p>no saved filter(s)</p>}
{filters.map((filter, index) => (
<S.SavedFilter
key={Symbol(filter.name).toString()}
selected={selectedFilter === index}
onClick={() => setSelectedFilter(index)}
{savedFilterState ? (
<SavedFilters
deleteFilter={deleteFilter}
activeFilterHandler={activeFilterHandler}
closeModal={toggleIsOpen}
onGoBack={() => setSavedFilterState(false)}
filters={filters}
onEdit={(index: number, filter: MessageFilters) => {
toggleEditModal();
editFilter({ index, filter });
}}
/>
) : (
<>
<S.SavedFiltersTextContainer
onClick={() => setSavedFilterState(true)}
>
<S.SavedFilterName>{filter.name}</S.SavedFilterName>
<S.FilterOptions>
<S.FilterEdit
onClick={() => {
toggleEditModal();
editFilter({ index, filter });
}}
>
Edit
</S.FilterEdit>
<S.DeleteSavedFilter
data-testid="deleteIcon"
onClick={() => deleteFilterHandler(index)}
>
<i className="fas fa-times" />
</S.DeleteSavedFilter>
</S.FilterOptions>
</S.SavedFilter>
))}
</S.SavedFiltersContainer>
<S.FilterButtonWrapper>
<Button
buttonSize="M"
buttonType="secondary"
type="button"
onClick={toggleIsOpen}
disabled={toggleDeletionModal}
>
Cancel
</Button>
<Button
buttonSize="M"
buttonType="primary"
type="button"
onClick={activeFilter}
disabled={toggleDeletionModal}
>
Select filter
</Button>
</S.FilterButtonWrapper>
<SavedIcon /> <S.SavedFiltersText>Saved Filters</S.SavedFiltersText>
</S.SavedFiltersTextContainer>
<AddEditFilterContainer
cancelBtnHandler={toggleIsOpen}
submitBtnText="Add filter"
submitCallback={onSubmit}
isAdd
/>
</>
)}
</>
) : (
<AddEditFilterContainer
title="Add filter"
cancelBtnHandler={() => setAddNewFilter(!addNewFilter)}
submitBtnText="Add filter"
submitCallback={onSubmit}
submitCallbackWithReset
createNewFilterText="Create a new filter"
toggleSaveFilterValue={toggleSaveFilter}
toggleSaveFilterSetter={() => setToggleSaveFilter(!toggleSaveFilter)}
/>
);
};

View file

@ -3,6 +3,7 @@ import { MessageFilters } from 'components/Topics/Topic/Details/Messages/Filters
import { FilterEdit } from 'components/Topics/Topic/Details/Messages/Filters/FilterModal';
import AddEditFilterContainer from './AddEditFilterContainer';
import * as S from './Filters.styled';
export interface EditFilterProps {
editFilter: FilterEdit;
@ -23,14 +24,16 @@ const EditFilter: React.FC<EditFilterProps> = ({
[editSavedFilter, editFilter.index, toggleEditModal]
);
return (
<AddEditFilterContainer
title="Edit saved filter"
cancelBtnHandler={() => toggleEditModal()}
submitBtnText="Save"
inputDisplayNameDefaultValue={editFilter.filter.name}
inputCodeDefaultValue={editFilter.filter.code}
submitCallback={onSubmit}
/>
<>
<S.FilterTitle>Edit saved filter</S.FilterTitle>
<AddEditFilterContainer
cancelBtnHandler={() => toggleEditModal()}
submitBtnText="Save"
inputDisplayNameDefaultValue={editFilter.filter.name}
inputCodeDefaultValue={editFilter.filter.code}
submitCallback={onSubmit}
/>
</>
);
};

View file

@ -1,4 +1,4 @@
import styled from 'styled-components';
import styled, { css } from 'styled-components';
interface SavedFilterProps {
selected: boolean;
@ -99,7 +99,6 @@ export const ClearAll = styled.span`
color: ${({ theme }) => theme.metrics.filters.color.normal};
font-size: 12px;
cursor: pointer;
font-family: Inter;
`;
export const MessageFilterModal = styled.div`
@ -117,16 +116,19 @@ export const MessageFilterModal = styled.div`
export const FilterTitle = styled.h3`
line-height: 32px;
font-family: Inter;
font-size: 20px;
margin-bottom: 40px;
`;
export const NewFilterIcon = styled.div`
color: ${({ theme }) => theme.icons.newFilterIcon};
padding-right: 6px;
height: 12px;
cursor: pointer;
position: relative;
&:after {
content: '';
width: calc(100% + 32px);
height: 1px;
position: absolute;
top: 40px;
left: -16px;
display: inline-block;
background-color: #f1f2f3;
}
`;
export const CreatedFilter = styled.p`
@ -154,15 +156,20 @@ export const SavedFilterName = styled.div`
export const FilterButtonWrapper = styled.div`
display: flex;
justify-content: flex-end;
margin-top: 25px;
margin-top: 10px;
gap: 10px;
`;
export const AddFiltersIcon = styled.div`
color: ${({ theme }) => theme.metrics.filters.color.icon};
padding-right: 6px;
height: 20px;
cursor: pointer;
padding-top: 16px;
position: relative;
&:before {
content: '';
width: calc(100% + 32px);
height: 1px;
position: absolute;
top: 0;
left: -16px;
display: inline-block;
background-color: #f1f2f3;
}
`;
export const ActiveSmartFilterWrapper = styled.div`
@ -200,6 +207,7 @@ export const SavedFilter = styled.div.attrs({
height: 32px;
align-items: center;
cursor: pointer;
border-top: 1px solid #f1f2f3;
&:hover ${FilterOptions} {
display: flex;
}
@ -240,37 +248,6 @@ export const DeleteSavedFilterIcon = styled.div`
cursor: pointer;
`;
export const ConfirmDeletionModal = styled.div.attrs({ role: 'deletionModal' })`
height: auto;
width: 348px;
border-radius: 8px;
background: ${({ theme }) => theme.modal.backgroundColor};
position: absolute;
left: 20%;
border: 1px solid ${({ theme }) => theme.breadcrumb};
box-shadow: ${({ theme }) => theme.modal.shadow};
padding: 16px;
z-index: 2;
`;
export const ConfirmDeletionModalHeader = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
height: 48px;
`;
export const ConfirmDeletionTitle = styled.h3`
font-size: 20px;
line-height: 32px;
`;
export const CloseDeletionModalIcon = styled.div`
color: ${({ theme }) => theme.icons.closeModalIcon};
height: 20px;
cursor: pointer;
`;
export const ConfirmDeletionText = styled.h3`
color: ${({ theme }) => theme.modal.deletionTextColor};
font-size: 14px;
@ -310,3 +287,28 @@ export const MessageLoadingSpinner = styled.div<MessageLoadingSpinnerProps>`
}
}
`;
export const SavedFiltersTextContainer = styled.div.attrs({
role: 'savedFilterText',
})`
display: flex;
align-items: center;
cursor: pointer;
margin-bottom: 15px;
`;
const textStyle = css`
font-size: 14px;
color: ${({ theme }) => theme.editFilterText.color};
font-weight: 500;
`;
export const SavedFiltersText = styled.div`
${textStyle};
margin-left: 7px;
`;
export const BackToCustomText = styled.div`
${textStyle};
cursor: pointer;
`;

View file

@ -27,6 +27,7 @@ import FilterModal, {
} from 'components/Topics/Topic/Details/Messages/Filters/FilterModal';
import { SeekDirectionOptions } from 'components/Topics/Topic/Details/Messages/Messages';
import TopicMessagesContext from 'components/contexts/TopicMessagesContext';
import useModal from 'lib/hooks/useModal';
import * as S from './Filters.styled';
import {
@ -89,8 +90,7 @@ const Filters: React.FC<FiltersProps> = ({
const { searchParams, seekDirection, isLive, changeSeekDirection } =
useContext(TopicMessagesContext);
const [isOpen, setIsOpen] = React.useState(false);
const toggleIsOpen = () => setIsOpen(!isOpen);
const { isOpen, toggle } = useModal();
const source = React.useRef<EventSource | null>(null);
@ -157,7 +157,7 @@ const Filters: React.FC<FiltersProps> = ({
return {
q:
queryType === MessageFilterType.GROOVY_SCRIPT
? `valueAsText.contains('${activeFilter.code}')`
? activeFilter.code
: query,
filterQueryType: queryType,
attempt,
@ -445,9 +445,10 @@ const Filters: React.FC<FiltersProps> = ({
/>
</div>
<S.ActiveSmartFilterWrapper>
<S.AddFiltersIcon data-testid="addFilterIcon" onClick={toggleIsOpen}>
<Button buttonType="primary" buttonSize="M" onClick={toggle}>
<i className="fas fa-plus fa-sm" />
</S.AddFiltersIcon>
Add Filters
</Button>
{activeFilter.name && (
<S.ActiveSmartFilter data-testid="activeSmartFilter">
{activeFilter.name}
@ -462,7 +463,7 @@ const Filters: React.FC<FiltersProps> = ({
</S.ActiveSmartFilterWrapper>
{isOpen && (
<FilterModal
toggleIsOpen={toggleIsOpen}
toggleIsOpen={toggle}
filters={savedFilters}
addFilter={addFilter}
deleteFilter={deleteFilter}

View file

@ -0,0 +1,109 @@
import React, { FC } from 'react';
import { Button } from 'components/common/Button/Button';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
import useModal from 'lib/hooks/useModal';
import * as S from './Filters.styled';
import { MessageFilters } from './Filters';
export interface Props {
filters: MessageFilters[];
onEdit(index: number, filter: MessageFilters): void;
deleteFilter(index: number): void;
activeFilterHandler(activeFilter: MessageFilters, index: number): void;
closeModal(): void;
onGoBack(): void;
}
const SavedFilters: FC<Props> = ({
filters,
onEdit,
deleteFilter,
activeFilterHandler,
closeModal,
onGoBack,
}) => {
const { isOpen, setOpen, setClose } = useModal();
const [deleteIndex, setDeleteIndex] = React.useState<number>(-1);
const [selectedFilter, setSelectedFilter] = React.useState(-1);
const activeFilter = () => {
if (selectedFilter > -1) {
activeFilterHandler(filters[selectedFilter], selectedFilter);
}
closeModal();
};
const deleteFilterHandler = (index: number) => {
setOpen();
setDeleteIndex(index);
};
return (
<>
<ConfirmationModal
isOpen={isOpen}
title="Confirm deletion"
onConfirm={() => {
deleteFilter(deleteIndex);
setClose();
}}
onCancel={setClose}
submitBtnText="Delete"
>
<S.ConfirmDeletionText>
Are you sure want to remove {filters[deleteIndex]?.name}?
</S.ConfirmDeletionText>
</ConfirmationModal>
<S.BackToCustomText onClick={onGoBack}>
Back To custom filters
</S.BackToCustomText>
<S.SavedFiltersContainer>
<S.CreatedFilter>Saved filters</S.CreatedFilter>
{filters.length === 0 && <p>No saved filter(s)</p>}
{filters.map((filter, index) => (
<S.SavedFilter
key={Symbol(filter.name).toString()}
selected={selectedFilter === index}
onClick={() => setSelectedFilter(index)}
>
<S.SavedFilterName>{filter.name}</S.SavedFilterName>
<S.FilterOptions>
<S.FilterEdit onClick={() => onEdit(index, filter)}>
Edit
</S.FilterEdit>
<S.DeleteSavedFilter
data-testid="deleteIcon"
onClick={() => deleteFilterHandler(index)}
>
<i className="fas fa-times" />
</S.DeleteSavedFilter>
</S.FilterOptions>
</S.SavedFilter>
))}
</S.SavedFiltersContainer>
<S.FilterButtonWrapper>
<Button
buttonSize="M"
buttonType="secondary"
type="button"
onClick={closeModal}
disabled={isOpen}
>
Cancel
</Button>
<Button
buttonSize="M"
buttonType="primary"
type="button"
onClick={activeFilter}
disabled={isOpen}
>
Select filter
</Button>
</S.FilterButtonWrapper>
</>
);
};
export default SavedFilters;

View file

@ -8,9 +8,7 @@ import userEvent from '@testing-library/user-event';
import { MessageFilters } from 'components/Topics/Topic/Details/Messages/Filters/Filters';
describe('AddEditFilterContainer component', () => {
const defaultTitle = 'Test Title';
const defaultSubmitBtn = 'Submit Button';
const defaultNewFilter = 'Create New Filters';
const mockData: MessageFilters = {
name: 'mockName',
@ -18,14 +16,11 @@ describe('AddEditFilterContainer component', () => {
};
const setupComponent = (props: Partial<AddEditFilterContainerProps> = {}) => {
const { title, submitBtnText, createNewFilterText } = props;
const { submitBtnText } = props;
return render(
<AddEditFilterContainer
title={title || defaultTitle}
cancelBtnHandler={jest.fn()}
submitBtnText={submitBtnText || defaultSubmitBtn}
createNewFilterText={createNewFilterText || defaultNewFilter}
toggleSaveFilterSetter={jest.fn()}
{...props}
/>
);
@ -35,14 +30,9 @@ describe('AddEditFilterContainer component', () => {
beforeEach(() => {
setupComponent();
});
it('should render the components', () => {
expect(screen.getByRole('heading', { level: 3 })).toBeInTheDocument();
});
it('should check the default parameters values', () => {
expect(screen.getByText(defaultTitle)).toBeInTheDocument();
it('should check the default Button text', () => {
expect(screen.getByText(defaultSubmitBtn)).toBeInTheDocument();
expect(screen.getByText(defaultNewFilter)).toBeInTheDocument();
});
it('should check whether the submit Button is disabled when the form is pristine and disabled if dirty', async () => {
@ -51,12 +41,12 @@ describe('AddEditFilterContainer component', () => {
const inputs = screen.getAllByRole('textbox');
const inputNameElement = inputs[0];
userEvent.type(inputNameElement, 'Hello World!');
const textAreaElement = inputs[1];
const textAreaElement = inputs[0];
userEvent.type(textAreaElement, 'Hello World With TextArea');
const inputNameElement = inputs[1];
userEvent.type(inputNameElement, 'Hello World!');
await waitFor(() => {
expect(submitButtonElem).toBeEnabled();
});
@ -71,12 +61,12 @@ describe('AddEditFilterContainer component', () => {
it('should view the error message after typing and clearing the input', async () => {
const inputs = screen.getAllByRole('textbox');
const inputNameElement = inputs[0];
userEvent.type(inputNameElement, 'Hello World!');
const textAreaElement = inputs[1];
const textAreaElement = inputs[0];
userEvent.type(textAreaElement, 'Hello World With TextArea');
const inputNameElement = inputs[1];
userEvent.type(inputNameElement, 'Hello World!');
userEvent.clear(inputNameElement);
userEvent.clear(textAreaElement);
@ -96,8 +86,8 @@ describe('AddEditFilterContainer component', () => {
});
const inputs = screen.getAllByRole('textbox');
const inputNameElement = inputs[0];
const textAreaElement = inputs[1];
const textAreaElement = inputs[0];
const inputNameElement = inputs[1];
expect(inputNameElement).toHaveValue(mockData.name);
expect(textAreaElement).toHaveValue(mockData.code);
@ -114,19 +104,19 @@ describe('AddEditFilterContainer component', () => {
});
it('should test whether the submit Callback is being called', async () => {
const submitCallback = jest.fn() as (v: MessageFilters) => void;
const submitCallback = jest.fn();
setupComponent({
submitCallback,
});
const inputs = screen.getAllByRole('textbox');
const inputNameElement = inputs[0];
userEvent.type(inputNameElement, 'Hello World!');
const textAreaElement = inputs[1];
const textAreaElement = inputs[0];
userEvent.type(textAreaElement, 'Hello World With TextArea');
const inputNameElement = inputs[1];
userEvent.type(inputNameElement, 'Hello World!');
const submitBtnElement = screen.getByText(defaultSubmitBtn);
await waitFor(() => {
@ -140,48 +130,20 @@ describe('AddEditFilterContainer component', () => {
});
});
it('should display the checkbox if the props is passed and click stay checking', async () => {
const setCheckboxMock = jest.fn();
setupComponent({
toggleSaveFilterSetter: setCheckboxMock,
});
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toBeInTheDocument();
userEvent.click(checkbox);
await waitFor(() => {
expect(checkbox).toBeChecked();
});
await waitFor(() => {
expect(setCheckboxMock).toBeCalled();
});
});
it('should display the checkbox if the props is passed and initially check state', () => {
setupComponent({
toggleSaveFilterSetter: jest.fn(),
toggleSaveFilterValue: true,
});
setupComponent({ isAdd: true });
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toBeInTheDocument();
expect(checkbox).not.toBeChecked();
userEvent.click(checkbox);
expect(checkbox).toBeChecked();
});
it('should pass and render the view props', () => {
const title = 'titleTest';
const createNewFilterText = 'createNewFilterTextTest';
it('should pass and render the correct button text', () => {
const submitBtnText = 'submitBtnTextTest';
setupComponent({
title,
createNewFilterText,
submitBtnText,
});
expect(screen.getByText(title)).toBeInTheDocument();
expect(screen.getByText(createNewFilterText)).toBeInTheDocument();
expect(screen.getByText(submitBtnText)).toBeInTheDocument();
});
});

View file

@ -7,8 +7,12 @@ import { MessageFilters } from 'components/Topics/Topic/Details/Messages/Filters
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
const filters: MessageFilters[] = [{ name: 'name', code: 'code' }];
const setupComponent = (props?: Partial<FilterModalProps>) =>
const filters: MessageFilters[] = [
{ name: 'name', code: 'code' },
{ name: 'name2', code: 'code2' },
];
const setupComponent = (props: Partial<FilterModalProps> = {}) =>
render(
<AddFilter
toggleIsOpen={jest.fn()}
@ -17,151 +21,103 @@ const setupComponent = (props?: Partial<FilterModalProps>) =>
activeFilterHandler={jest.fn()}
toggleEditModal={jest.fn()}
editFilter={jest.fn()}
filters={filters}
filters={props.filters || filters}
{...props}
/>
);
describe('AddFilter component', () => {
it('renders component with filters', () => {
setupComponent({ filters });
expect(screen.getByRole('savedFilter')).toBeInTheDocument();
});
it('renders component without filters', () => {
setupComponent({ filters: [] });
expect(screen.getByText('no saved filter(s)')).toBeInTheDocument();
});
it('renders add filter modal with saved filters', () => {
it('should test click on Saved Filters redirects to Saved components', () => {
setupComponent();
expect(screen.getByText('Created filters')).toBeInTheDocument();
userEvent.click(screen.getByRole('savedFilterText'));
expect(screen.getByText('Saved filters')).toBeInTheDocument();
expect(screen.getAllByRole('savedFilter')).toHaveLength(2);
});
describe('Filter deletion', () => {
it('open deletion modal', () => {
setupComponent();
userEvent.hover(screen.getByRole('savedFilter'));
userEvent.click(screen.getByTestId('deleteIcon'));
expect(screen.getByRole('deletionModal')).toBeInTheDocument();
});
it('close deletion modal with button', () => {
setupComponent();
userEvent.hover(screen.getByRole('savedFilter'));
userEvent.click(screen.getByTestId('deleteIcon'));
expect(screen.getByRole('deletionModal')).toBeInTheDocument();
const cancelButton = screen.getAllByRole('button', { name: /Cancel/i });
userEvent.click(cancelButton[0]);
expect(screen.getByText('Created filters')).toBeInTheDocument();
});
it('close deletion modal with close icon', () => {
setupComponent();
userEvent.hover(screen.getByRole('savedFilter'));
userEvent.click(screen.getByTestId('deleteIcon'));
expect(screen.getByRole('deletionModal')).toBeInTheDocument();
userEvent.click(screen.getByTestId('closeDeletionModalIcon'));
expect(screen.getByText('Created filters')).toBeInTheDocument();
});
it('delete filter', () => {
const deleteFilter = jest.fn();
setupComponent({ filters, deleteFilter });
userEvent.hover(screen.getByRole('savedFilter'));
userEvent.click(screen.getByTestId('deleteIcon'));
userEvent.click(screen.getByRole('button', { name: /Delete/i }));
expect(deleteFilter).toHaveBeenCalledTimes(1);
expect(screen.getByText('Created filters')).toBeInTheDocument();
});
it('should test click on return to custom filter redirects to Add filters', () => {
setupComponent();
userEvent.click(screen.getByRole('savedFilterText'));
expect(screen.getByText('Saved filters')).toBeInTheDocument();
expect(screen.queryByRole('savedFilterText')).not.toBeInTheDocument();
expect(screen.getAllByRole('savedFilter')).toHaveLength(2);
userEvent.click(screen.getByText(/back to custom filters/i));
expect(screen.queryByText('Saved filters')).not.toBeInTheDocument();
expect(screen.getByRole('savedFilterText')).toBeInTheDocument();
});
describe('Add new filter', () => {
beforeEach(() => {
setupComponent();
});
it('renders add new filter modal', async () => {
await waitFor(() => {
userEvent.click(screen.getByText('New filter'));
});
expect(screen.getByText('Create a new filter')).toBeInTheDocument();
});
it('adding new filter', async () => {
await waitFor(() => {
userEvent.click(screen.getByText('New filter'));
});
expect(
screen.getByRole('button', { name: /Add filter/i })
).toBeDisabled();
const codeValue = 'filter code';
const nameValue = 'filter name';
const textBoxes = screen.getAllByRole('textbox');
const codeTextBox = textBoxes[0];
const nameTextBox = textBoxes[1];
const addFilterBtn = screen.getByRole('button', { name: /Add filter/i });
expect(addFilterBtn).toBeDisabled();
expect(screen.getByPlaceholderText('Enter Name')).toBeInTheDocument();
await waitFor(() => {
userEvent.type(screen.getAllByRole('textbox')[0], 'filter name');
userEvent.type(screen.getAllByRole('textbox')[1], 'filter code');
userEvent.type(codeTextBox, codeValue);
userEvent.type(nameTextBox, nameValue);
});
expect(screen.getAllByRole('textbox')[0]).toHaveValue('filter name');
expect(screen.getAllByRole('textbox')[1]).toHaveValue('filter code');
expect(addFilterBtn).toBeEnabled();
expect(codeTextBox).toHaveValue(codeValue);
expect(nameTextBox).toHaveValue(nameValue);
});
it('close add new filter modal', () => {
userEvent.click(screen.getByText('New filter'));
expect(screen.getByText('Save this filter')).toBeInTheDocument();
userEvent.click(screen.getByText('Cancel'));
expect(screen.getByText('Created filters')).toBeInTheDocument();
});
});
describe('Edit filter', () => {
it('opens editFilter modal', () => {
const editFilter = jest.fn();
const toggleEditModal = jest.fn();
setupComponent({ editFilter, toggleEditModal });
userEvent.click(screen.getByText('Edit'));
expect(editFilter).toHaveBeenCalledTimes(1);
expect(toggleEditModal).toHaveBeenCalledTimes(1);
});
});
describe('Selecting a filter', () => {
it('should mock the select function if the filter is check no otherwise', () => {
const toggleOpenMock = jest.fn();
const activeFilterMock = jest.fn() as (
activeFilter: MessageFilters,
index: number
) => void;
setupComponent({
filters,
toggleIsOpen: toggleOpenMock,
activeFilterHandler: activeFilterMock,
it('should check unSaved filter without name', async () => {
const codeTextBox = screen.getAllByRole('textbox')[0];
const code = 'filter code';
const addFilterBtn = screen.getByRole('button', { name: /Add filter/i });
expect(addFilterBtn).toBeDisabled();
expect(screen.getByPlaceholderText('Enter Name')).toBeInTheDocument();
await waitFor(() => {
userEvent.type(codeTextBox, code);
});
const selectFilterButton = screen.getByText(/Select filter/i);
userEvent.click(selectFilterButton);
expect(activeFilterMock).not.toHaveBeenCalled();
expect(toggleOpenMock).not.toHaveBeenCalled();
const savedFilterElement = screen.getByRole('savedFilter');
userEvent.click(savedFilterElement);
userEvent.click(selectFilterButton);
expect(activeFilterMock).toHaveBeenCalled();
expect(toggleOpenMock).toHaveBeenCalled();
expect(addFilterBtn).toBeEnabled();
expect(codeTextBox).toHaveValue(code);
});
});
describe('onSubmit with Filter being saved', () => {
let addFilterMock: (values: MessageFilters) => void;
let activeFilterHandlerMock: (
activeFilter: MessageFilters,
index: number
) => void;
const addFilterMock = jest.fn();
const activeFilterHandlerMock = jest.fn();
const toggleModelMock = jest.fn();
const codeValue = 'filter code';
const nameValue = 'filter name';
beforeEach(async () => {
addFilterMock = jest.fn() as (values: MessageFilters) => void;
activeFilterHandlerMock = jest.fn() as (
activeFilter: MessageFilters,
index: number
) => void;
setupComponent({
addFilter: addFilterMock,
activeFilterHandler: activeFilterHandlerMock,
toggleIsOpen: toggleModelMock,
});
userEvent.click(screen.getByText(/New filter/i));
await waitFor(() => {
userEvent.type(screen.getAllByRole('textbox')[0], 'filter name');
userEvent.type(screen.getAllByRole('textbox')[1], 'filter code');
userEvent.type(screen.getAllByRole('textbox')[0], codeValue);
userEvent.type(screen.getAllByRole('textbox')[1], nameValue);
});
});
afterEach(() => {
addFilterMock.mockClear();
activeFilterHandlerMock.mockClear();
toggleModelMock.mockClear();
});
it('OnSubmit condition with checkbox off functionality', async () => {
userEvent.click(screen.getAllByRole('button')[1]);
// since both values are in it
const addFilterBtn = screen.getByRole('button', { name: /Add filter/i });
expect(addFilterBtn).toBeEnabled();
userEvent.click(addFilterBtn);
await waitFor(() => {
expect(activeFilterHandlerMock).toHaveBeenCalled();
expect(addFilterMock).not.toHaveBeenCalled();
@ -175,6 +131,57 @@ describe('AddFilter component', () => {
await waitFor(() => {
expect(activeFilterHandlerMock).not.toHaveBeenCalled();
expect(addFilterMock).toHaveBeenCalled();
expect(toggleModelMock).not.toHaveBeenCalled();
});
});
it('should check the state submit button when checkbox state changes so is name input value', async () => {
const checkbox = screen.getByRole('checkbox');
const codeTextBox = screen.getAllByRole('textbox')[0];
const nameTextBox = screen.getAllByRole('textbox')[1];
const addFilterBtn = screen.getByRole('button', { name: /Add filter/i });
userEvent.clear(nameTextBox);
expect(nameTextBox).toHaveValue('');
userEvent.click(addFilterBtn);
await waitFor(() => {
expect(activeFilterHandlerMock).toHaveBeenCalledTimes(1);
expect(activeFilterHandlerMock).toHaveBeenCalledWith(
{
name: 'Unsaved filter',
code: codeValue,
saveFilter: false,
},
-1
);
// get reset-ed
expect(codeTextBox).toHaveValue('');
expect(toggleModelMock).toHaveBeenCalled();
});
userEvent.type(codeTextBox, codeValue);
expect(codeTextBox).toHaveValue(codeValue);
userEvent.click(checkbox);
expect(addFilterBtn).toBeDisabled();
userEvent.type(nameTextBox, nameValue);
expect(nameTextBox).toHaveValue(nameValue);
await waitFor(() => {
expect(addFilterBtn).toBeEnabled();
});
userEvent.click(addFilterBtn);
await waitFor(() => {
expect(activeFilterHandlerMock).toHaveBeenCalledTimes(1);
expect(addFilterMock).toHaveBeenCalledWith({
name: nameValue,
code: codeValue,
saveFilter: true,
});
});
});
});

View file

@ -25,13 +25,16 @@ const setupComponent = (props?: Partial<EditFilterProps>) =>
describe('EditFilter component', () => {
it('renders component', () => {
setupComponent();
expect(screen.getByText(/edit saved filter/i)).toBeInTheDocument();
});
it('closes editFilter modal', () => {
const toggleEditModal = jest.fn();
setupComponent({ toggleEditModal });
userEvent.click(screen.getByRole('button', { name: /Cancel/i }));
expect(toggleEditModal).toHaveBeenCalledTimes(1);
});
it('save edited fields and close modal', async () => {
const toggleEditModal = jest.fn();
const editSavedFilter = jest.fn();
@ -40,13 +43,14 @@ describe('EditFilter component', () => {
expect(toggleEditModal).toHaveBeenCalledTimes(1);
expect(editSavedFilter).toHaveBeenCalledTimes(1);
});
it('checks input values to match', () => {
setupComponent();
expect(screen.getAllByRole('textbox')[0]).toHaveValue(
editFilter.filter.name
editFilter.filter.code
);
expect(screen.getAllByRole('textbox')[1]).toHaveValue(
editFilter.filter.code
editFilter.filter.name
);
});
});

View file

@ -26,10 +26,15 @@ describe('FilterModal component', () => {
setupWrapper();
});
it('renders component with add filter modal', () => {
expect(screen.getByText('Add filter')).toBeInTheDocument();
expect(
screen.getByRole('heading', { name: /add filter/i, level: 3 })
).toBeInTheDocument();
});
it('renders component with edit filter modal', async () => {
await waitFor(() => userEvent.click(screen.getByRole('savedFilterText')));
await waitFor(() => userEvent.click(screen.getByText('Edit')));
expect(screen.getByText('Edit saved filter')).toBeInTheDocument();
expect(
screen.getByRole('heading', { name: /edit saved filter/i, level: 3 })
).toBeInTheDocument();
});
});

View file

@ -45,35 +45,42 @@ describe('Filters component', () => {
it('renders component', () => {
setupWrapper();
});
describe('when fetching', () => {
it('shows cancel button while fetching', () => {
setupWrapper({ isFetching: true });
expect(screen.getByText('Cancel')).toBeInTheDocument();
});
});
describe('when fetching is over', () => {
it('shows submit button while fetching is over', () => {
setupWrapper();
expect(screen.getByText('Submit')).toBeInTheDocument();
});
});
describe('Input elements', () => {
const inputValue = 'Hello World!';
it('search input', () => {
setupWrapper();
const SearchInput = screen.getByPlaceholderText('Search');
expect(SearchInput).toBeInTheDocument();
expect(SearchInput).toHaveValue('');
userEvent.type(SearchInput, 'Hello World!');
expect(SearchInput).toHaveValue('Hello World!');
userEvent.type(SearchInput, inputValue);
expect(SearchInput).toHaveValue(inputValue);
});
it('offset input', () => {
setupWrapper();
const OffsetInput = screen.getByPlaceholderText('Offset');
expect(OffsetInput).toBeInTheDocument();
expect(OffsetInput).toHaveValue('');
userEvent.type(OffsetInput, 'Hello World!');
expect(OffsetInput).toHaveValue('Hello World!');
userEvent.type(OffsetInput, inputValue);
expect(OffsetInput).toHaveValue(inputValue);
});
it('timestamp input', () => {
setupWrapper();
const seekTypeSelect = screen.getAllByRole('listbox');
@ -84,11 +91,12 @@ describe('Filters component', () => {
const TimestampInput = screen.getByPlaceholderText('Select timestamp');
expect(TimestampInput).toBeInTheDocument();
expect(TimestampInput).toHaveValue('');
userEvent.type(TimestampInput, 'Hello World!');
expect(TimestampInput).toHaveValue('Hello World!');
userEvent.type(TimestampInput, inputValue);
expect(TimestampInput).toHaveValue(inputValue);
expect(screen.getByText('Submit')).toBeInTheDocument();
});
});
describe('Select elements', () => {
let seekTypeSelects: HTMLElement[];
let options: HTMLElement[];
@ -137,7 +145,11 @@ describe('Filters component', () => {
describe('add new filter modal', () => {
it('renders addFilter modal', () => {
setupWrapper();
userEvent.click(screen.getByTestId('addFilterIcon'));
userEvent.click(
screen.getByRole('button', {
name: /add filters/i,
})
);
expect(screen.getByTestId('messageFilterModal')).toBeInTheDocument();
});
});
@ -146,21 +158,43 @@ describe('Filters component', () => {
beforeEach(async () => {
setupWrapper();
await waitFor(() => userEvent.click(screen.getByTestId('addFilterIcon')));
userEvent.click(screen.getByText('New filter'));
await waitFor(() => {
userEvent.type(screen.getAllByRole('textbox')[2], 'filter name');
userEvent.type(screen.getAllByRole('textbox')[3], 'filter code');
});
expect(screen.getAllByRole('textbox')[2]).toHaveValue('filter name');
expect(screen.getAllByRole('textbox')[3]).toHaveValue('filter code');
await waitFor(() =>
userEvent.click(screen.getByRole('button', { name: /Add Filter/i }))
userEvent.click(
screen.getByRole('button', {
name: /add filters/i,
})
)
);
const filterName = 'filter name';
const filterCode = 'filter code';
const messageFilterModal = screen.getByTestId('messageFilterModal');
await waitFor(() => {
const textBoxElements =
within(messageFilterModal).getAllByRole('textbox');
userEvent.type(textBoxElements[0], filterName);
userEvent.type(textBoxElements[1], filterCode);
});
const textBoxElements =
within(messageFilterModal).getAllByRole('textbox');
expect(textBoxElements[0]).toHaveValue(filterName);
expect(textBoxElements[1]).toHaveValue(filterCode);
await waitFor(() => {
return userEvent.click(
within(messageFilterModal).getByRole('button', {
name: /add filter/i,
})
);
});
});
it('shows saved smart filter', () => {
expect(screen.getByTestId('activeSmartFilter')).toBeInTheDocument();
});
it('delete the active smart Filter', async () => {
const smartFilterElement = screen.getByTestId('activeSmartFilter');
const deleteIcon = within(smartFilterElement).getByTestId(

View file

@ -0,0 +1,157 @@
import React from 'react';
import SavedFilters, {
Props,
} from 'components/Topics/Topic/Details/Messages/Filters/SavedFilters';
import { MessageFilters } from 'components/Topics/Topic/Details/Messages/Filters/Filters';
import { screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from 'lib/testHelpers';
describe('SavedFilter Component', () => {
const mockFilters: MessageFilters[] = [
{ name: 'name', code: 'code' },
{ name: 'name1', code: 'code1' },
];
const setUpComponent = (props: Partial<Props> = {}) => {
return render(
<SavedFilters
filters={props.filters || mockFilters}
onEdit={props.onEdit || jest.fn()}
closeModal={props.closeModal || jest.fn()}
onGoBack={props.onGoBack || jest.fn()}
activeFilterHandler={props.activeFilterHandler || jest.fn()}
deleteFilter={props.deleteFilter || jest.fn()}
/>
);
};
it('should check the Cancel button click', () => {
const cancelMock = jest.fn();
setUpComponent({ closeModal: cancelMock });
userEvent.click(screen.getByText(/cancel/i));
expect(cancelMock).toHaveBeenCalled();
});
it('should check on go back button click', () => {
const onGoBackMock = jest.fn();
setUpComponent({ onGoBack: onGoBackMock });
userEvent.click(screen.getByText(/back to custom filters/i));
expect(onGoBackMock).toHaveBeenCalled();
});
describe('Empty Filters Rendering', () => {
beforeEach(() => {
setUpComponent({ filters: [] });
});
it('should check the rendering of the empty filter', () => {
expect(screen.getByText(/no saved filter/i)).toBeInTheDocument();
expect(screen.queryByRole('savedFilter')).not.toBeInTheDocument();
});
});
describe('Saved Filters Deleting Editing', () => {
const onEditMock = jest.fn();
const activeFilterMock = jest.fn();
const cancelMock = jest.fn();
beforeEach(() => {
setUpComponent({
onEdit: onEditMock,
activeFilterHandler: activeFilterMock,
closeModal: cancelMock,
});
});
afterEach(() => {
onEditMock.mockClear();
activeFilterMock.mockClear();
cancelMock.mockClear();
});
it('should check the normal data rendering', () => {
expect(screen.getAllByRole('savedFilter')).toHaveLength(
mockFilters.length
);
expect(screen.getByText(mockFilters[0].name)).toBeInTheDocument();
expect(screen.getByText(mockFilters[1].name)).toBeInTheDocument();
});
it('should check the Filter edit Button works', () => {
const savedFilters = screen.getAllByRole('savedFilter');
userEvent.hover(savedFilters[0]);
userEvent.click(within(savedFilters[0]).getByText(/edit/i));
expect(onEditMock).toHaveBeenCalled();
userEvent.hover(savedFilters[1]);
userEvent.click(within(savedFilters[1]).getByText(/edit/i));
expect(onEditMock).toHaveBeenCalledTimes(2);
});
it('should check the select filter', () => {
const selectFilterButton = screen.getByText(/Select filter/i);
userEvent.click(selectFilterButton);
expect(activeFilterMock).not.toHaveBeenCalled();
const savedFilterElement = screen.getAllByRole('savedFilter');
userEvent.click(savedFilterElement[0]);
userEvent.click(selectFilterButton);
expect(activeFilterMock).toHaveBeenCalled();
expect(cancelMock).toHaveBeenCalled();
});
});
describe('Saved Filters Deletion', () => {
const deleteMock = jest.fn();
beforeEach(() => {
setUpComponent({ deleteFilter: deleteMock });
});
afterEach(() => {
deleteMock.mockClear();
});
it('Open Confirmation for the deletion modal', () => {
const savedFilters = screen.getAllByRole('savedFilter');
const deleteIcons = screen.getAllByTestId('deleteIcon');
userEvent.hover(savedFilters[0]);
userEvent.click(deleteIcons[0]);
const modelDialog = screen.getByRole('dialog');
expect(modelDialog).toBeInTheDocument();
expect(
within(modelDialog).getByText(/Confirm deletion/i)
).toBeInTheDocument();
});
it('Close Confirmations deletion modal with button', () => {
const savedFilters = screen.getAllByRole('savedFilter');
const deleteIcons = screen.getAllByTestId('deleteIcon');
userEvent.hover(savedFilters[0]);
userEvent.click(deleteIcons[0]);
const modelDialog = screen.getByRole('dialog');
expect(modelDialog).toBeInTheDocument();
const cancelButton = within(modelDialog).getByRole('button', {
name: /Cancel/i,
});
userEvent.click(cancelButton);
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
it('Delete the saved filter', () => {
const savedFilters = screen.getAllByRole('savedFilter');
const deleteIcons = screen.getAllByTestId('deleteIcon');
userEvent.hover(savedFilters[0]);
userEvent.click(deleteIcons[0]);
userEvent.click(screen.getByRole('button', { name: /Delete/i }));
expect(deleteMock).toHaveBeenCalledTimes(1);
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
});
});

View file

@ -9,15 +9,17 @@ export interface ConfirmationModalProps {
onConfirm(): void;
onCancel(): void;
isConfirming?: boolean;
submitBtnText?: string;
}
const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
isOpen,
children,
title,
title = 'Confirm the action',
onCancel,
onConfirm,
isConfirming = false,
submitBtnText = 'Submit',
}) => {
const cancelHandler = React.useCallback(() => {
if (!isConfirming) {
@ -30,7 +32,7 @@ const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
<div onClick={cancelHandler} aria-hidden="true" />
<div>
<header>
<p>{title || 'Confirm the action'}</p>
<p>{title}</p>
</header>
<section>{children}</section>
<footer>
@ -51,7 +53,7 @@ const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
type="button"
disabled={isConfirming}
>
Submit
{submitBtnText}
</Button>
</footer>
</div>

View file

@ -9,7 +9,7 @@ import theme from 'theme/theme';
const confirmMock = jest.fn();
const cancelMock = jest.fn();
const body = 'Please Confirm the action!';
describe('ConfiramationModal', () => {
describe('ConfirmationModal', () => {
const setupWrapper = (props: Partial<ConfirmationModalProps> = {}) => (
<ThemeProvider theme={theme}>
<ConfirmationModal
@ -49,6 +49,12 @@ describe('ConfiramationModal', () => {
title
);
});
it('Check the text on the submit button default behavior', () => {
const wrapper = mount(setupWrapper({ isOpen: true }));
expect(wrapper.exists({ children: 'Submit' })).toBeTruthy();
});
it('handles onConfirm when user clicks confirm button', () => {
const wrapper = mount(setupWrapper({ isOpen: true }));
const confirmBtn = wrapper.find({ children: 'Submit' });
@ -57,6 +63,12 @@ describe('ConfiramationModal', () => {
expect(confirmMock).toHaveBeenCalledTimes(1);
});
it('Check the text on the submit button', () => {
const submitBtnText = 'Submit btn Text';
const wrapper = mount(setupWrapper({ isOpen: true, submitBtnText }));
expect(wrapper.exists({ children: submitBtnText })).toBeTruthy();
});
describe('cancellation', () => {
let wrapper: ReactWrapper;

View file

@ -0,0 +1,29 @@
import React, { FC } from 'react';
import { useTheme } from 'styled-components';
const SavedIcon: FC = () => {
const theme = useTheme();
return (
<svg
width="18"
height="20"
viewBox="0 0 18 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M16 2H2L2 17.9873L7.29945 15.4982C8.3767 14.9922 9.6233 14.9922 10.7005 15.4982L16 17.9873V2ZM2 0C0.895431 0 0 0.895431 0 2V17.9873C0 19.4527 1.5239 20.4206 2.85027 19.7976L8.14973 17.3085C8.68835 17.0555 9.31165 17.0555 9.85027 17.3085L15.1497 19.7976C16.4761 20.4206 18 19.4527 18 17.9873V2C18 0.895431 17.1046 0 16 0H2Z"
fill={theme.icons.savedIcon}
/>
<path
d="M9 4L10.4401 7.01791L13.7553 7.45492L11.3301 9.75709L11.9389 13.0451L9 11.45L6.06107 13.0451L6.66991 9.75709L4.24472 7.45492L7.55993 7.01791L9 4Z"
fill={theme.icons.savedIcon}
/>
</svg>
);
};
export default SavedIcon;

View file

@ -0,0 +1,66 @@
import { renderHook, act } from '@testing-library/react-hooks';
import useModal from 'lib/hooks/useModal';
describe('useModal CustomHook', () => {
it('should check true initial values', () => {
let initialValue = true;
const { result, rerender } = renderHook(() => useModal(initialValue));
expect(result.current.isOpen).toBe(initialValue);
initialValue = false;
rerender();
// because state is in useState
expect(result.current.isOpen).not.toBe(initialValue);
});
it('should check false initial values', () => {
let initialValue = false;
const { result, rerender } = renderHook(() => useModal(initialValue));
expect(result.current.isOpen).toBe(initialValue);
initialValue = true;
rerender();
// because state is in useState
expect(result.current.isOpen).not.toBe(initialValue);
});
it('should check setOpen function', () => {
const { result } = renderHook(() => useModal());
expect(result.current.isOpen).toBeFalsy();
act(() => {
result.current.setOpen();
});
expect(result.current.isOpen).toBeTruthy();
});
it('should check setClose function', () => {
const { result } = renderHook(() => useModal());
expect(result.current.isOpen).toBeFalsy();
act(() => {
result.current.setOpen();
});
expect(result.current.isOpen).toBeTruthy();
act(() => {
result.current.setClose();
});
expect(result.current.isOpen).toBeFalsy();
});
it('should check setToggle function', () => {
const { result } = renderHook(() => useModal());
expect(result.current.isOpen).toBeFalsy();
act(() => {
result.current.toggle();
});
expect(result.current.isOpen).toBeTruthy();
act(() => {
result.current.toggle();
});
expect(result.current.isOpen).toBeFalsy();
});
});

View file

@ -0,0 +1,32 @@
import { useCallback, useState } from 'react';
interface UseModalReturn {
isOpen: boolean;
setOpen(): void;
setClose(): void;
toggle(): void;
}
const useModal = (initialModalState?: boolean): UseModalReturn => {
const [modalOpen, setModalOpen] = useState<boolean>(!!initialModalState);
const setOpen = useCallback(() => {
setModalOpen(true);
}, []);
const setClose = useCallback(() => {
setModalOpen(false);
}, []);
const toggle = useCallback(() => {
setModalOpen((prev) => !prev);
}, []);
return {
isOpen: modalOpen,
setOpen,
setClose,
toggle,
};
};
export default useModal;

View file

@ -489,6 +489,7 @@ const theme = {
},
newFilterIcon: Colors.brand[50],
closeModalIcon: Colors.neutral[25],
savedIcon: Colors.brand[50],
},
viewer: {
wrapper: Colors.neutral[3],