How do you unit test a component that has a ref prop ? i'm getting this error
● Should render › should render
TypeError: Cannot add property current, object is not extensible
I looked at another question Unit testing React component ref, but there is no solution for that question.
this is the test
CommentList.test.tsx
import "#testing-library/jest-dom";
import React from "react";
import { CommentListComponent as CommentList } from "./CommentList";
import { render, getByText, queryByText, getAllByTestId } from "#testing-library/react";
const props = {
user: {},
postId: null,
userId: null,
currentUser: {},
ref: {},
comments: [
{
author: { username: "barnowl", gravatar: "https://api.adorable.io/avatars/400/bf1eed82fbe37add91cb4192e4d14de6.png", bio: null },
comment_body: "fsfsfsfsfs",
createdAt: "2020-05-27T14:32:01.682Z",
gifUrl: "",
id: 520,
postId: 28,
updatedAt: "2020-05-27T14:32:01.682Z",
userId: 9,
},
{
author: { username: "barnowl", gravatar: "https://api.adorable.io/avatars/400/bf1eed82fbe37add91cb4192e4d14de6.png", bio: null },
comment_body: "fsfsfsfsfs",
createdAt: "2020-05-27T14:32:01.682Z",
gifUrl: "",
id: 519,
postId: 27,
updatedAt: "2020-05-27T14:32:01.682Z",
userId: 10,
},
],
deleteComment: jest.fn(),
};
describe("Should render <CommentList/>", () => {
it("should render <CommentList/>", () => {
const commentList = render(<CommentList {...props} />);
expect(commentList).toBeTruthy();
});
});
This is the component.
CommentList.tsx
import React, { Fragment, useState, Ref } from "react";
import Grid from "#material-ui/core/Grid";
import OurSecondaryButton from "../../../common/OurSecondaryButton";
import CommentListContainer from "../commentListContainer/commentListContainer";
function CommentList(props: any, ref: Ref<HTMLDivElement>) {
const [showMore, setShowMore] = useState<Number>(2);
const [openModal, setOpenModal] = useState(false);
const [showLessFlag, setShowLessFlag] = useState<Boolean>(false);
const the_comments = props.comments.length;
const inc = showMore as any;
const min = Math.min(2, the_comments - inc);
const showComments = (e) => {
e.preventDefault();
if (inc + 2 && inc <= the_comments) {
setShowMore(inc + 2);
setShowLessFlag(true);
} else {
setShowMore(the_comments);
}
};
const handleClickOpen = () => {
setOpenModal(true);
};
const handleCloseModal = () => {
setOpenModal(false);
};
const showLessComments = (e) => {
e.preventDefault();
setShowMore(2);
setShowLessFlag(false);
};
const isBold = (comment) => {
return comment.userId === props.userId ? 800 : 400;
};
// show comments by recent, and have the latest comment at the bottom, with the previous one just before it.
const filterComments = props.comments
.slice(0)
.sort((a, b) => {
const date1 = new Date(a.createdAt) as any;
const date2 = new Date(b.createdAt) as any;
return date2 - date1;
})
.slice(0, inc)
.reverse();
const showMoreComments = () => {
return filterComments.map((comment, i) => (
<div key={i}>
<CommentListContainer ref={ref} comment={comment} openModal={openModal} handleCloseModal={handleCloseModal} isBold={isBold} handleClickOpen={handleClickOpen} {...props} />
</div>
));
};
console.log(ref);
return (
<Grid>
<Fragment>
<div data-testid="comment-list-div" style={{ margin: "30px 0px" }}>
{props.comments.length > 2 ? (
<Fragment>
{min !== -1 && min !== -2 ? (
<Fragment>
{min !== 0 ? (
<OurSecondaryButton onClick={(e) => showComments(e)} component="span" color="secondary">
View {min !== -1 && min !== -2 ? min : 0} More Comments
</OurSecondaryButton>
) : (
<OurSecondaryButton onClick={(e) => showLessComments(e)} component="span" color="secondary">
Show Less Comments
</OurSecondaryButton>
)}
</Fragment>
) : (
<OurSecondaryButton onClick={(e) => showLessComments(e)} component="span" color="secondary">
Show Less Comments
</OurSecondaryButton>
)}
</Fragment>
) : null}
</div>
</Fragment>
{showLessFlag === true ? (
// will show most recent comments below
showMoreComments()
) : (
<Fragment>
{/* filter based on first comment */}
{filterComments.map((comment, i) => (
<div key={i}>
<CommentListContainer ref={ref} comment={comment} openModal={openModal} handleCloseModal={handleCloseModal} isBold={isBold} handleClickOpen={handleClickOpen} {...props} />
</div>
))}
</Fragment>
)}
</Grid>
);
}
export default React.forwardRef(CommentList) as React.RefForwardingComponent<HTMLDivElement, any>;
I fixed it, i had to import
import CommentList from "./CommentList";
instead of
import { CommentListComponent as CommentList } from "./CommentList";
and do this to the props
ref: {
current: undefined,
},
and comment/remove this line of code from commentList
// // prevents un-necesary re renders
// // export default React.memo(CommentList);
// // will be useful for unit testing.
// export { CommentList as CommentListComponent };
Related
import { FC } from 'react';
import { useDropzone } from 'react-dropzone';
import { FileIcon } from 'assets/icons';
import Typography from '../Typography';
import { TMultipleDropzoneProps } from './types';
import styles from './MultipleDropzone.module.scss';
const MultipleDropzone: FC<TMultipleDropzoneProps> = ({ title, onDrop }) => {
const { getRootProps, getInputProps, open, isDragAccept, isFocused, isDragReject } = useDropzone({
accept: { 'image/*': ['.jpeg', '.png'], 'video/mp4': ['.mp4', '.MP4'] },
onDrop,
noClick: true,
noKeyboard: true,
maxFiles: 3,
});
const accept = isDragAccept ? 1 : 0;
const focused = isFocused ? 1 : 0;
const rejected = isDragReject ? 1 : 0;
// This is used for warning in console for camel-case attributes to the DOM element and to make it boolean
return (
<div className={styles.wrapper}>
<div
onClick={open}
className={styles.container}
{...getRootProps({ accept, focused, rejected })}
>
<input {...getInputProps({})} />
{rejected === 1 && (
<div className={styles.error}>
Some files were rejected because they did not meet the requirements.
</div>
)}
<div className={styles.container__content}>
<Typography>{title}</Typography>
</div>
<button onClick={open} className={styles.icon}>
<FileIcon />
</button>
</div>
</div>
);
};
export default MultipleDropzone;
the type file:
export type TMultipleDropzoneProps = {
title: string;
onDrop: (e: any, a: any) => void;
isFileUploaded: boolean;
maxFiles?: number;
};
the modal I am using it in:
import { useContext, useState } from 'react';
import { ModalContext } from 'context/Modal';
import { FileExtended } from 'types/global/file';
import { useAppDispatch, useAppSelector } from 'hooks';
import { NewPostTextArea, MultipleDropzone, Typography, Button } from 'components';
import { createActivityPost } from 'store/slices/activitiesSlice/activitiesThunks';
import { CloseCircleIcon } from 'assets/icons';
import { TImages } from './types';
import styles from './NewPost.module.scss';
const NewPost = () => {
const { closeModal } = useContext(ModalContext);
const [images, setImages] = useState<TImages[]>([]);
const [description, setDescription] = useState<string>('');
const dispatch = useAppDispatch();
const { userData } = useAppSelector((state) => state.auth);
const createPost = () => {
const post = {
user_id: userData?.id as number,
description: description,
content_url: 'https://cdn.pixabay.com/photo/2015/04/23/22/00/tree-736885__340.jpg',
content_type: 'string',
};
closeModal();
if (description.trim()) {
dispatch(createActivityPost(post));
}
};
const isFileUploaded = images.length > 0;
const onDrop = (acceptedFiles: FileExtended[], maxFiles: number) => {
acceptedFiles.forEach((file) => {
const reader = new FileReader();
reader.onload = (e) => {
setImages((prev) => [
...prev,
{
id: Date.now(),
type: file.type,
src: e.target && e.target.result,
file: file,
description: file.name,
},
]);
};
reader.readAsDataURL(file);
});
let modifiedAcceptedFiles = acceptedFiles;
if (acceptedFiles.length > maxFiles) {
// If the number of files exceeds the maxFiles limit,
// slice the extra files off the array and show an error message
modifiedAcceptedFiles = acceptedFiles.slice(0, maxFiles);
}
};
const removeImage = (id: number) => {
setImages(images.filter((image) => image.id !== id));
};
const imageFiles = images.map((image) => {
return (
<div key={image.id} className={styles.container__main__case__box}>
<CloseCircleIcon
onClick={() => removeImage(image.id)}
className={styles.container__main__case__box_close}
/>
{image.type.includes('video') ? (
<video src={image.src as string} autoPlay loop />
) : (
<img src={image.src as string} alt={image.description} />
)}
</div>
);
});
return (
<div className={styles.container}>
<Typography className={styles.container__head}>New Post</Typography>
<div className={styles.container__description}>
<NewPostTextArea value={description} setValue={setDescription} />
</div>
<div className={styles.container__main}>
<Typography className={styles.container__main__head}>{images.length} items</Typography>
<div className={styles.container__main__case}>{imageFiles}</div>
</div>
<MultipleDropzone
onDrop={onDrop}
title='Please attach your files here (Max 3)'
isFileUploaded={isFileUploaded}
/>
<div className={styles.container__footer}>
<Button className={styles.container__footer_close} onClick={closeModal}>
Close
</Button>
<Button type='submit' className={styles.container__footer_submit} onClick={createPost}>
Create
</Button>
</div>
</div>
);
};
export default NewPost;
I tried adding maxFiles to every single component, I also tried adding it to the onDrop component. New to React ( 1 week) and I am slowly losing my sanity. I will never forgive Zuckerberg for this apparition he has brought upon coderkin. Even chatGPT could not help my case.
I have a project, and this project contains several interfaces, and among these interfaces there is an interface for uploading an image, and the problem is in the deletion icon. When you click on it, a modal appears, but the element is deleted before the modal appears.
How can i solve the problem?
this file display a list of instructions that contains upload Image
import '../../../styles/input/index.scss';
import '../../../styles/dropzone/index.scss';
import { Button, Col, message, Modal, Row, Spin, Upload, UploadFile } from 'antd';
import { FunctionComponent, useCallback, useRef, useState } from 'react';
import { motion, useAnimation } from 'framer-motion';
import { defaultTranstion } from '../../../constants/framer';
import { Controller } from 'react-hook-form';
import FromElemnetWrapper from '../form-element-wrapper';
import { Delete, UploadCloud } from 'react-feather';
import { getBase64 } from '../../../utils/get-base64';
import _ from 'lodash';
import config from '../../../api/nuclearMedicineApi/config';
import { FormattedMessage } from 'react-intl';
import BasicModal from '../modal';
import { UploadOutlined } from '#ant-design/icons';
import axios from 'axios';
import { IFormError } from '../general-form-containner';
interface DropzoneProps {
name: string;
control: any;
rules?: any;
label: string;
disabled?: boolean;
multiple?: boolean;
accept?: string;
refType?: number;
defaultFileList?: any;
onRemove?: any;
customRequest?: (option: any) => void;
onProgress?: any;
}
const Dropzone: FunctionComponent<DropzoneProps> = ({
name,
control,
rules,
label,
disabled,
multiple,
accept,
refType,
defaultFileList,
onRemove,
customRequest,
onProgress
}) => {
const focusController = useAnimation();
const errorController = useAnimation();
const [previewVisible, setpreviewVisible] = useState(false);
const [previewImage, setpreviewImage] = useState('');
const handleCancel = () => setpreviewVisible(false);
const handlePreview = async (file: any) => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj);
}
setpreviewImage(file?.preview ?? file.url);
setpreviewVisible(true);
};
const [isModalOpen, setIsModalOpen] = useState(false);
const [errors, setErrors] = useState<IFormError[]>([]);
const [visibleModal, setVisibleModal] = useState(false);
const [removePromise, setRemovePromise] = useState();
const [deleteVisible, setdeleteVisible] = useState<boolean>(false);
const onDeleteHandle = () => {
setdeleteVisible(false);
};
const deletehandleCancel = () => {
setdeleteVisible(false);
};
let resolvePromiseRef = useRef<((value: boolean) => void) | undefined>();
// let resolvePromiseRef = useRef<HTMLInputElement | null>(null)
const handleRemove = useCallback(() =>{
const promise = new Promise(resolve => {
resolvePromiseRef.current = resolve
});
setVisibleModal(true);
return promise;
}, [])
const handleOkModalRemove = useCallback(() => {
if (resolvePromiseRef.current) {
resolvePromiseRef.current(true)
}
}, [removePromise]);
const handleCancelModalRemove = useCallback(() => {
if (resolvePromiseRef.current) {
resolvePromiseRef.current(false);
setVisibleModal(false)
}
}, [removePromise]);
return (
<>
<FromElemnetWrapper
focusController={focusController}
errorController={errorController}
label={label}
required={rules.required?.value}
>
<Controller
control={control}
name={name}
rules={rules}
render={({
field: { onChange, onBlur, value, name, ref },
fieldState: { invalid, error },
}) => {
if (invalid) {
errorController.start({ scale: 80 });
} else {
errorController.start(
{ scale: 0 },
{ ease: defaultTranstion.ease.reverse() },
);
}
return (
<div
onBlur={() => {
onBlur();
focusController.start({ scale: 0 });
}}
onFocus={() => {
focusController.start({ scale: 80 });
}}
className='relative'
>
<div className='upload-container'>
<form
className='dropzone needsclick'
id='demo-upload'
action='/upload'
>
{/* <> */}
<Upload
action={`${config.baseUrl}api/services/app/Attachment/Upload`}
headers={config.headers}
ref={ref}
multiple={multiple}
disabled={disabled}
data={{ RefType: refType }}
listType='picture'
fileList={value}
id={name}
accept={accept}
onPreview={handlePreview}
onRemove={handleRemove}
iconRender={
() => {
return <Spin style={{ marginBottom: '12px', paddingBottom: '12px' }}></Spin>
}
}
progress={{
strokeWidth: 3,
strokeColor: {
"0%": "#f0f",
"100%": "#ff0"
},
style: { top: 12 }
}}
beforeUpload={
(file) => {
console.log({ file });
return true
}
}
// onProgress= {(event: any) => (event.loaded / event.total) * 100}
// onChange={(e) =>
// onChange(e.fileList)
// }
// onChange={(response) => {
// console.log('response: ', response);
// if (response.file.status !== 'uploading') {
// console.log(response.file, response.fileList);
// }
// if (response.file.status === 'done') {
// message.success(`${response.file.name}
// file uploaded successfully`);
// } else if (response.file.status === 'error') {
// message.error(`${response.file.name}
// file upload failed.`);
// }
// else if (response.file.status === 'removed') {
// message.error(`${response.file.name}
// file upload removed.`);
// }
// }}
>
<div className='upload-button'>
<div className='wrapper'>
<motion.div
className='fas fa-angle-double-up'
whileHover={{
y: [
0, -2, 2,
0,
],
transition: {
duration: 1.5,
ease: 'easeInOut',
yoyo: Infinity,
},
}}
>
<UploadCloud
style={{
margin: '.2rem',
display:
'inline-block',
}}
color='white'
size={20}
/>
Upload
</motion.div>
</div>
</div>
</Upload>
{/*
<Modal
title="Are you sure?"
visible={visibleModal}
onOk={handleOkModalRemove}
onCancel={handleCancelModalRemove}
/> */}
<BasicModal
header={
<>
<FormattedMessage id={'confirmdeletion'} />
</>
}
headerType='error'
content={
<>
<Row>
<Col span={8} offset={4}>
<Button
type='primary'
className='savebtn'
onClick={onDeleteHandle}
style={{
cursor:
Object.keys(errors).length !==
0
? 'not-allowed'
: 'pointer',
}}
>
<FormattedMessage id={'affirmation'} />
</Button>
</Col>
<Col span={8} offset={4}>
<Button
type='default'
className='savebtn'
onClick={deletehandleCancel}
style={{
cursor:
Object.keys(errors).length !==
0
? 'not-allowed'
: 'pointer',
}}
>
<FormattedMessage id={'cancel'} />
</Button>
</Col>
</Row>
</>
}
isOpen={visibleModal}
footer={false}
width='35vw'
handleCancel={handleCancelModalRemove}
handleOk={handleOkModalRemove}
/>
{/* {_.isEmpty(value) && (
<div className='dz-message needsclick'>
<FormattedMessage id='dropfileshere' />
</div>
)} */}
<BasicModal
isOpen={previewVisible}
header={<FormattedMessage id="Preview image" />}
footer={false}
handleCancel={handleCancel}
content={<img
alt='example'
style={{ width: '100%' }}
src={previewImage}
/>}
/>
{/* </> */}
</form>
</div>
{invalid && (
<p className='form-element-error'>
{error?.message}
</p>
)}
</div>
);
}}
/>
</FromElemnetWrapper>
</>
);
};
export default Dropzone;
Why it doesn't work?
https://ant.design/components/upload
onRemove - A callback function, will be executed when removing file button is clicked, remove event will be prevented when return value is false or a Promise which resolve(false) or reject
You have to return a promise which resolves to false or return false
How to solve it?
You have to return a promise so first of all, you need a ref or a state to store resolve function of that promise so you can call it in modal
const resolvePromiseRef = useRef<((value: boolean) => void) | undefined>();
Then you will need to assign the resolve function to the ref and return the promise
Now onRemove will wait for your promise to resolve
const handleRemove = () =>{
const promise = new Promise(resolve => {
resolvePromiseRef.current = resolve
});
setVisibleModal(true);
return promise;
}
Now your functions handlers
const handleOkModalRemove = useCallback(() => {
if (resolvePromiseRef.current) {
resolvePromiseRef.current(true)
}
}, [removePromise]);
const handleCancelModalRemove = useCallback(() => {
if (resolvePromiseRef.current) {
resolvePromiseRef.current(false)
}
}, [removePromise]);
You can also use the state, but I recommend ref because it doesn't rerender the component when changed.
My tests are passing fine - but I still get the (in)famous *Warning: An update to Movie inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */*
here:
13 |
14 | const handleSelect = () => {
15 | setSelected(!selected);
| ^
16 | };
17 |
18 | return (
I can't figure how to solve it.
My code below
Movies.tsx
import { useState } from "react";
import { MovieSearchResults } from "../../model/movie";
import Movie from "../Movie/Movie";
import Search from "../Search/Search";
import Pager from "../Pager/Pager";
import "./Movies.css";
const Movies = () => {
const [search, setSearch] = useState<string>("");
const [movies, setMovies] = useState<MovieSearchResults>({
results: [],
total_pages: 1,
page: 1,
});
const handleSearch = () => {
fetch(
`https://api.themoviedb.org/3/search/movie?api_key=${process.env.REACT_APP_API_KEY}&query=${search}`
)
.then((response) => response.json())
.then((json) => setMovies(json));
};
const handlePager = (page: number) => {
fetch(
`https://api.themoviedb.org/3/search/movie?api_key=${process.env.REACT_APP_API_KEY}&query=${search}&page=${page}`
)
.then((response) => response.json())
.then((json) => setMovies(json));
};
return (
<div className="Movies">
<Search search={search} setSearch={setSearch} onSearch={handleSearch} />
{movies && movies.total_pages > 1 ? (
<Pager
{...movies}
total_pages={movies.total_pages}
onPageChange={handlePager}
/>
) : (
<></>
)}
{movies &&
movies.results.map((movie) => (
<Movie
{...movie} /*here I am destructuring movie properties because I don't need to specify all*/
id={movie.id}
poster_path={movie.poster_path}
title={movie.title}
key={movie.id}
overview={movie.overview.substring(0, 250) + "..."}
/>
))}
{movies && movies.total_pages > 1 ? (
<Pager
{...movies}
total_pages={movies.total_pages}
onPageChange={handlePager}
/>
) : (
<div className="message--noResulst">
Sorry. There's no results available for your search...
</div>
)}
</div>
);
};
export default Movies;
Movie.tsx
import { useState } from "react";
import { MovieSearchResult } from "../../model/movie";
import classnames from "classnames";
import "./Movie.css";
const Movie = (movie: MovieSearchResult, key: number) => {
//select Movie on click:
const [selected, setSelected] = useState<boolean>(false);
// use classnames to modify className on select:
const classNames = classnames("Movie", { Movie__selected: selected });
const handleSelect = () => {
setSelected(!selected);
};
return (
<div className={classNames} onClick={handleSelect}>
<div className="Movie__poster-wrap">
<img
className="Movie__poster"
src={`https://image.tmdb.org/t/p/w92${movie.poster_path}`}
alt="Movie poster"
/>
</div>
<div className="Movie__details">
<h2>{movie.title}</h2>
<p>{movie.overview}</p>
</div>
</div>
);
};
export default Movie;
Movie.test.js
import React from "react";
import renderer from "react-test-renderer";
import { act, render, fireEvent } from "#testing-library/react";
import Movie from "./Movie";
describe("Movie", () => {
it(" changes the class of Movie when clicked i.e. selected", async () => {
const movie = {
adult: false,
backdrop_path: "/eM9MZ1dhDCbRzwQg0FWpWWtLPMn.jpg",
genre_ids: [10752, 28, 18],
id: 390054,
original_language: "en",
original_title: "Sand Castle",
overview:
"Set during the occupation of Iraq, a squad of U.S. soldiers try to protect a small village....",
popularity: 38.305,
poster_path: "/c9buG2jVRgAu68E4D4jpwlgqhO1.jpg",
release_date: "2017-04-21",
title: "Sand Castle",
video: false,
vote_average: 6.5,
vote_count: 613,
};
const component = renderer.create(
<Movie
{...movie}
id={movie.id}
poster_path={movie.poster_path}
title={movie.title}
key={movie.id}
overview={movie.overview.substring(0, 250) + "..."}
/>
);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
//trigger the click
tree.props.onClick();
// re-rendering
tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});
I've looked into the act(...) functionality on Jets but couldn't figure it out.
Thanks in advance for the help and explanations
I have two pages and two components LibraryPageFilters.tsx (url: /courses) and UserVideoCreatePage.tsx (url: /ugc/courses/${course.id}).
In component LibraryPageFilters.tsx
useEffect(() => {
console.log(course.id)
if (course.id) {
console.log(544)
dispatch(push(`/ugc/courses/${course.id}`));
}
}, [course]);
i have a check that if course.id present in the store, then we make a redirect.
In component UserVideoCreatePage.tsx
useEffect(() => {
return () => {
console.log(333344444)
dispatch(courseDelete());
};
}, []);
i am deleting a course from the store when componentUnmount.
why does unmount happen after a redirect? as a result, I am redirected back. Because the course is not removed from the store at the moment of unmount, and the check (if (course.id)) shows that the course is in the store and a redirect occurs back (dispatch(push(/ugc/courses/${course.id})))
UserVideoCreatePage.tsx
import React, { useEffect, useRef, useState } from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router';
import { Link } from 'react-router-dom';
import { Container } from 'Core/components/Container/Container';
import { Svg } from 'Core/components/Svg';
import { Button } from 'Core/Molecules/Button';
import { Select } from 'Core/Molecules/Select';
import {
agreementCourse, categoriesSelector,
courseDelete,
courseEditorCourseSelector,
courseUpdateApi, getCategories,
getCourse,
updateCourseApi,
} from 'Learnings/store/courseEdit';
import { CourseComments } from 'Learnings/screens/CoursePlayPage/CourseBottom/CourseComments/CourseComments';
import { AddItem } from './AddItem';
import s from 'Admin/Pages/Course/Description/index.scss';
import './UserVideoCreatePage.scss';
export const UserVideoCreatePage: React.FC = () => {
const dispatch = useDispatch();
const { id: idCourse } = useParams();
const course = useSelector(courseEditorCourseSelector, shallowEqual);
const categories = useSelector(categoriesSelector, shallowEqual);
const [value, valueSet] = useState({ name: '', description: '', categories: [], lectures: [], materials: [] });
const [tab, tabSet] = useState('program');
const inputFileRef = useRef<HTMLInputElement>(null);
const img = course.gallery_items && course.gallery_items[0];
console.log(categories);
const handleBtnClick = () => {
if (inputFileRef && inputFileRef.current) {
inputFileRef.current.click();
}
};
useEffect(() => {
dispatch(getCourse(idCourse));
dispatch(getCategories());
}, [idCourse]);
useEffect(() => {
valueSet({
name: course.name,
description: course.description,
categories: course.categories && course.categories[0] && course.categories[0].id,
lectures: course.lectures,
materials: course.materials,
});
}, [course]);
useEffect(() => {
return () => {
console.log(333344444)
dispatch(courseDelete());
};
}, []);
return (
<Container className="createCourse">
<Link to="/" className="gallery__back">
<Svg name="arrow_back" width={26} height={20} className="gallery__svg"/>
<span>Назад</span>
</Link>
<div className="createCourse__twoColumn">
<div className="createCourse__twoColumn-left">
<div className="inputBlock">
<label className="inputBlock__label" htmlFor="video">
Название видео-курса
</label>
<input
id="video"
type="text"
placeholder="Введите название вашего видео"
className="inputBlock__input"
value={value.name || ''}
onChange={e =>
valueSet({
...value,
name: e.target.value,
})
}
onBlur={e => {
if (e.target.value && course.name !== e.target.value) {
dispatch(updateCourseApi(idCourse, { name: e.target.value }));
}
}}
/>
</div>
<div className="inputBlock">
<label className="inputBlock__label" htmlFor="opisanie">
Описание видео-курса
</label>
<textarea
id="opisanie"
placeholder="Введите краткое описание вашего видео"
className="inputBlock__input"
value={value.description || ''}
onChange={e =>
valueSet({
...value,
description: e.target.value,
})
}
onBlur={e => {
if (e.target.value && course.description !== e.target.value) {
dispatch(updateCourseApi(idCourse, { description: e.target.value }));
}
}}
/>
</div>
<Select
title="Категории видео-курса"
placeholder="Категории видео-курса"
value={value.categories}
options={categories.map(category => ({ value: category.id, label: category.name }))}
onChange={val => {
valueSet({
...value,
categories: val,
});
dispatch(
updateCourseApi(idCourse, {
category_ids: val,
courses_curators: {
'': {
user_id: val,
},
},
}),
);
}}
search
/>
</div>
<div className="createCourse__twoColumn-right">
<div className="loadVideo">
<div className="loadVideo__field">
<div className="loadVideo__field--block">
{!img && (
<>
<Svg className="loadVideo__field--block-icon" name="icn-load" width={104} height={69}/>
<p className="loadVideo__field--block-text">Загрузите обложку к видео</p>
</>
)}
{img && <img src={img && img.image_url} alt=""/>}
</div>
</div>
<div className="loadVideo__under">
<div className="loadVideo__under--left">
<div className="loadVideo__under--text">
<span className="loadVideo__under--text-grey">*Рекомендуемый формат</span>
<span className="loadVideo__under--text-bold"> 356х100</span>
</div>
<div className="loadVideo__under--text">
<span className="loadVideo__under--text-grey">*Вес не должен превышать</span>
<span className="loadVideo__under--text-bold"> 10 Мб</span>
</div>
</div>
<div className="loadVideo__under--right">
<input
onChange={val => {
if (val.target.files[0]) {
if (img) {
dispatch(
updateCourseApi(idCourse, {
gallery_items: {
'': {
image: val.target.files[0],
id: img.id,
},
},
}),
);
} else {
dispatch(
updateCourseApi(idCourse, {
gallery_items: {
'': {
image: val.target.files[0],
},
},
}),
);
}
}
}}
type="file"
ref={inputFileRef}
className="Library__btn"
/>
<Button
onClick={() => {
handleBtnClick();
}}
>
Библиотека обложек
</Button>
</div>
</div>
</div>
</div>
</div>
<div className={`block-switcher block-switcher--courseCreate`}>
<div
className={`block-switcher__item ${tab === 'program' && 'block-switcher__item_active'}`}
onClick={() => tabSet('program')}
>
Программы
</div>
<div
className={`block-switcher__item ${tab === 'comments' && 'block-switcher__item_active'}`}
onClick={() => tabSet('comments')}
>
Комментарии эксперта
</div>
</div>
{tab === 'program' && (
<>
<AddItem
accept="video/mp4,video/x-m4v,video/*"
fieldName="name"
addType="lecture_type"
title="Видео-курсы"
addBtn="Добавить видео"
type="lectures"
file="video"
lecturesArg={course.lectures}
value={value}
onChangeInput={lecturesNew => {
valueSet({
...value,
lectures: lecturesNew,
});
}}
onVideoUpdate={(params: any) => {
dispatch(updateCourseApi(idCourse, params));
}}
posMove={(lectures: any) => {
dispatch(courseUpdateApi({ id: idCourse, lectures: lectures }, true));
}}
/>
<AddItem
accept=""
fieldName="title"
addType="material_type"
title="Материалы к видео-курсам"
addBtn="Добавить файл"
type="materials"
file="document"
lecturesArg={course.materials}
value={value}
onChangeInput={lecturesNew => {
valueSet({
...value,
materials: lecturesNew,
});
}}
onVideoUpdate={(params: any) => {
dispatch(updateCourseApi(idCourse, params));
}}
posMove={(lectures: any) => {
dispatch(courseUpdateApi({ id: idCourse, materials: lectures }, true));
}}
/>
</>
)}
{tab === 'comments' && <CourseComments title="Обсуждение"/>}
<Button
className={`${s.button} agreement__btn`}
size="big"
onClick={() =>
dispatch(
agreementCourse(idCourse, {
visibility_all_users: true,
}),
)
}
>
Отправить на согласование
</Button>
</Container>
);
};
LibraryPageFilters.tsx
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
import { push } from 'connected-react-router';
import { getSettingsGlobalSelector } from 'Core/store/settings';
import { Svg } from 'Core/components/Svg';
import { NavBar } from 'Core/Organisms/NavBar';
import { Button } from 'Core/Molecules/Button';
import { CategoriesFilter } from 'Core/Organisms/Filters/components/CategoriesFilter';
import { courseDelete, courseEditorCourseSelector, createNewCourse } from 'Learnings/store/courseEdit';
import { FILTERS, LINKS } from '../../libraryPageConstants';
import { setLibraryPageQuery } from '../../actions/libraryPageActions';
import { getLibraryPageQuerySelector } from '../../libraryPageSelectors';
import s from './index.scss';
import { languageTranslateSelector } from 'Core/store/language';
import { LanguageType } from 'Core/models/LanguageSchema';
import { Status, Tabs } from 'Core/components/Tabs/Tabs';
const statuses: Array<Status> = [
{
key: 'Filter/all',
link: '/courses' || '/courses',
type: 'all' || '',
},
{
key: 'Filter/online',
link: '/courses/online',
type: 'online',
},
{
key: 'Filter/offline',
link: '/courses/offline',
type: 'offline',
},
{
key: 'Filter/complete',
link: '/courses/complete',
type: 'complete',
},
];
export const LibraryPageFilters = () => {
const dispatch = useDispatch();
const [searchTerm, setSearchTerm] = useState('');
const [isBtnDisabled, setIsBtnDisabled] = useState(false);
const course = useSelector(courseEditorCourseSelector, shallowEqual);
const global = useSelector(getSettingsGlobalSelector);
const query = useSelector(getLibraryPageQuerySelector, shallowEqual);
const courseCreateButtonText = useSelector(
languageTranslateSelector('CoursePage/courseCreateButton'),
) as LanguageType;
const { category_id: categoryID } = query;
console.log(course)
useEffect(() => {
console.log(course.id)
if (course.id) {
console.log(544)
dispatch(push(`/ugc/courses/${course.id}`));
}
}, [course]);
useEffect(() => {
return () => {
setIsBtnDisabled(false);
dispatch(courseDelete());
};
}, []);
const onFilter = (values: any) => {
return false;
};
const handleActiveCategory = (id: number) => {
const categoryParam = {
...query,
offset: 0,
category_id: id,
};
if (id === categoryID) {
delete categoryParam.category_id;
}
dispatch(setLibraryPageQuery(categoryParam));
};
const handleSearch = () => {
dispatch(setLibraryPageQuery({ query: searchTerm }));
};
return (
<React.Fragment>
<div className={s.filters}>
{global.coursesPage?.filters.length ? (
<NavBar
className={s.navBar}
links={global.coursesPage.filtersLinks.map(linkType => LINKS[linkType])}
filters={global.coursesPage.filters.map(filterType => FILTERS[filterType])}
onFilter={onFilter}
postfix={
global.coursesPage.courseCreateButton && global.coursesPage.courseCreateButton.enable ? (
<Button
className="coursePageCreateButton"
onClick={() => {
dispatch(createNewCourse());
setIsBtnDisabled(true);
}}
disabled={isBtnDisabled}
>
{courseCreateButtonText['CoursePage/courseCreateButton']}
</Button>
) : null
}
/>
) : (
<div className="track-page__header" data-tut="track-header">
<Tabs statuses={statuses} />
<div className={s.filtersSearch}>
<Svg className={s.filtersSearchIcon} name="search_alternative" width={18} height={18} />
<input
type="text"
placeholder="Поиск"
className={s.filtersSearchInput}
value={searchTerm}
onChange={event => setSearchTerm(event.target.value)}
/>
<button type="button" className={s.filtersButton} onClick={handleSearch}>
Найти
</button>
</div>
</div>
)}
</div>
<CategoriesFilter onChange={handleActiveCategory} selectedID={categoryID} />
</React.Fragment>
);
};
Although react suggests to use Functional Component, try Class Component, I faced similar issues, this was resolved easily in Class Component :
componentDidMount();
componentDidUpdate(prevProps, prevState, snapshot);
These two will solve your problem. Ask me if anything you need.
What would be the way to test a component that relies on the initial state for conditional rendering ?
For example showLessFlag is dependent on state, and testing state in react-testing-library is counter productive.
so how would i test this condition in the CommentList component
{showLessFlag === true ? (
// will show most recent comments below
showMoreComments()
) : (
<Fragment>
{/* filter based on first comment, this shows by default */}
{filterComments.map((comment, i) => (
<div key={i} className="comment">
<CommentListContainer ref={ref} comment={comment} openModal={openModal} handleCloseModal={handleCloseModal} isBold={isBold} handleClickOpen={handleClickOpen} {...props} />
</div>
))}
</Fragment>
)}
Should it be test like the following
it("should check more comments", () => {
const { getByTestId } = render(<CommentList {...props} />);
const commentList = getByTestId("comment-show-more");
expect(commentList).toBeNull();
});
But im getting this error because of the conditional rendering
TestingLibraryElementError: Unable to find an element by:
[data-testid="comment-show-more"]
CommentList.tsx
import React, { Fragment, useState, Ref } from "react";
import Grid from "#material-ui/core/Grid";
import OurSecondaryButton from "../../../common/OurSecondaryButton";
import CommentListContainer from "../commentListContainer/commentListContainer";
function CommentList(props: any, ref: Ref<HTMLDivElement>) {
const [showMore, setShowMore] = useState<Number>(2);
const [openModal, setOpenModal] = useState(false);
const [showLessFlag, setShowLessFlag] = useState<Boolean>(false);
const the_comments = props.comments.length;
const inc = showMore as any;
const min = Math.min(2, the_comments - inc);
const showComments = (e) => {
e.preventDefault();
if (inc + 2 && inc <= the_comments) {
setShowMore(inc + 2);
setShowLessFlag(true);
} else {
setShowMore(the_comments);
}
};
const handleClickOpen = () => {
setOpenModal(true);
};
const handleCloseModal = () => {
setOpenModal(false);
};
const showLessComments = (e) => {
e.preventDefault();
setShowMore(2);
setShowLessFlag(false);
};
const isBold = (comment) => {
return comment.userId === props.userId ? 800 : 400;
};
// show comments by recent, and have the latest comment at the bottom, with the previous one just before it.
const filterComments = props.comments
.slice(0)
.sort((a, b) => {
const date1 = new Date(a.createdAt) as any;
const date2 = new Date(b.createdAt) as any;
return date2 - date1;
})
.slice(0, inc)
.reverse();
const showMoreComments = () => {
return filterComments.map((comment, i) => (
<div data-testid="comment-show-more" key={i} className="comment">
<CommentListContainer ref={ref} comment={comment} openModal={openModal} handleCloseModal={handleCloseModal} isBold={isBold} handleClickOpen={handleClickOpen} {...props} />
</div>
));
};
return (
<Grid data-testid="comment-list-div">
<Fragment>
<div style={{ margin: "30px 0px" }}>
{props.comments.length > 2 ? (
<Fragment>
{min !== -1 && min !== -2 ? (
<Fragment>
{min !== 0 ? (
<OurSecondaryButton onClick={(e) => showComments(e)} component="span" color="secondary">
View {min !== -1 && min !== -2 ? min : 0} More Comments
</OurSecondaryButton>
) : (
<OurSecondaryButton onClick={(e) => showLessComments(e)} component="span" color="secondary">
Show Less Comments
</OurSecondaryButton>
)}
</Fragment>
) : (
<OurSecondaryButton onClick={(e) => showLessComments(e)} component="span" color="secondary">
Show Less Comments
</OurSecondaryButton>
)}
</Fragment>
) : null}
</div>
</Fragment>
{showLessFlag === true ? (
// will show most recent comments below
showMoreComments()
) : (
<Fragment>
{/* filter based on first comment */}
{filterComments.map((comment, i) => (
<div key={i} className="comment">
<CommentListContainer ref={ref} comment={comment} openModal={openModal} handleCloseModal={handleCloseModal} isBold={isBold} handleClickOpen={handleClickOpen} {...props} />
</div>
))}
</Fragment>
)}
</Grid>
);
}
export default React.forwardRef(CommentList) as React.RefForwardingComponent<HTMLDivElement, any>;
CommentList.test.tsx
import "#testing-library/jest-dom";
import React, { Ref } from "react";
import CommentList from "./CommentList";
import { render, getByText, queryByText, getAllByTestId } from "#testing-library/react";
const props = {
user: {},
postId: null,
userId: null,
currentUser: {},
ref: {
current: undefined,
},
comments: [
{
author: { username: "barnowl", gravatar: "https://api.adorable.io/avatars/400/bf1eed82fbe37add91cb4192e4d14de6.png", bio: null },
comment_body: "fsfsfsfsfs",
createdAt: "2020-05-27T14:32:01.682Z",
gifUrl: "",
id: 520,
postId: 28,
updatedAt: "2020-05-27T14:32:01.682Z",
userId: 9,
},
{
author: { username: "barnowl", gravatar: "https://api.adorable.io/avatars/400/bf1eed82fbe37add91cb4192e4d14de6.png", bio: null },
comment_body: "fsfsfsfsfs",
createdAt: "2020-05-27T14:32:01.682Z",
gifUrl: "",
id: 519,
postId: 27,
updatedAt: "2020-05-27T14:32:01.682Z",
userId: 10,
},
],
deleteComment: jest.fn(),
};
describe("Should render <CommentList/>", () => {
it("should render <CommentList/>", () => {
const commentList = render(<CommentList {...props} />);
expect(commentList).toBeTruthy();
});
it("should render first comment", () => {
const { getByTestId } = render(<CommentList {...props} />);
const commentList = getByTestId("comment-list-div");
expect(commentList.firstChild).toBeTruthy();
});
it("should render second child", () => {
const { getByTestId } = render(<CommentList {...props} />);
const commentList = getByTestId("comment-list-div");
expect(commentList.lastChild).toBeTruthy();
});
it("should check comments", () => {
const rtl = render(<CommentList {...props} />);
expect(rtl.getByTestId("comment-list-div")).toBeTruthy();
expect(rtl.getByTestId("comment-list-div")).toBeTruthy();
expect(rtl.getByTestId("comment-list-div").querySelectorAll(".comment").length).toEqual(2);
});
it("should match snapshot", () => {
const rtl = render(<CommentList {...props} />);
expect(rtl).toMatchSnapshot();
});
it("should check more comments", () => {
const { getByTestId } = render(<CommentList {...props} />);
const commentList = getByTestId("comment-show-more");
expect(commentList).toBeNull();
});
});
Any getBy* query in react-testing-library will throw an error if no match is found. If you want to test/assert the absence of an element then you want to use any of the queryBy* queries, they return null if no match is found.
Queries
it("should check more comments", () => {
const { queryByTestId } = render(<CommentList {...props} />);
const commentList = queryByTestId("comment-show-more");
expect(commentList).toBeNull();
});
To better answer this question, being that i have more experience with using react testing library now.
When we go about testing for conditions, we need to trigger the action that makes the change to the state.
For example in this situation
We have a condition like showLessFlag
{showLessFlag === true ? (
// will show most recent comments below
showMoreComments()
) : (
<Fragment>
{/* filter based on first comment, this shows by default */}
{filterComments.map((comment, i) => (
<div key={i} className="comment">
<CommentListContainer ref={ref} comment={comment} openModal={openModal} handleCloseModal={handleCloseModal} isBold={isBold} handleClickOpen={handleClickOpen} {...props} />
</div>
))}
</Fragment>
)}
In order to properly test this, we need to trigger the event that will change showLessFlag to false.
So we can do something like
<OurSecondaryButton
onClick={(e) => showLessComments(e)}
data-testid="_test-show-less"
component="span"
color="secondary"
>
Show Less Comments
</OurSecondaryButton>
test
it("should trigger showLessComments ", () => {
const { getByTestId } = render(<CommentList {...props} />);
const showLessButton = getByTestId("__test-show-less");
fireEvent.click(showLessButton);
expect(...) // whatever to be called, check for the existence of a div tag, or whatever you want
});
Testing for conditions improves code coverage :)