Testing slider component in react - reactjs

I want to test my slider component with react testing library. But I can't comprehend how to test it properly. I want to test changing slide when the user clicks the dot(StyledDotContainer). StyledDotContainer's background is gray but it is red when the active props is true. The component looks like this.
const Slider = ({
children,
autoPlayTime = 5000,
dots = true,
initialIndex= 0
}: SliderProps): React.ReactElement => {
const [activeIndex, setActiveIndex] = useState<number>(initialIndex)
const nextSlide = () => {
const newIndex = activeIndex >= length - 1 ? 0 : activeIndex + 1
setActiveIndex(newIndex)
}
useEffect(() => {
const timer = setTimeout(() => {
nextSlide()
}, autoPlayTime)
return () => clearTimeout(timer)
}, [activeIndex])
const length = useMemo(() => {
return React.Children.count(children)
}, [])
const setSlide = useCallback((index: number) => {
setActiveIndex(index)
}, [])
const value = useMemo(() => ({ activeIndex, setSlide }), [activeIndex])
return (
<SliderContext.Provider value={value}>
<StyledContainer>
{children}
{dots && (
<StyledDotsContainer data-testid="dots">
{[...Array(length)].map((_, index) => {
return (
<StyledDotContainer
data-testid={`dot-${index}`}
key={index}
onClick={() => setSlide(index)}
isActive={index === activeIndex}
/>
)
})}
</StyledDotsContainer>
)}
</StyledContainer>
</SliderContext.Provider>
)
}
Appreciate any suggestion.

It's a bad practice to test the styles of a Component. Instead you just want to test that the function you are trying to test is properly changing the props in your component. Styles should be inspected visually.
import screen from '#testing-library/dom'
import {render, screen} from '#testing-library/react'
import userEvent from '#testing-library/user-event'
describe("Slider", () => {
it('Should be red when slider is active', () => {
render(Slider)
const firstDot = screen.getByTestId('dots-0')
act(() => {
userEvent.click(firstDot)
})
waitFor(() => {
expect(screen.getByTestId('dots-0').props.isActive).toBeTruthy()
})
})
})

Related

React Scroll Animation

I have multiple pages in my react app and I want to set screen view to the page that is most visible when i scroll, just like what happen in tesla.com. This is my code if someone can help;
import React, { useState, useRef, useEffect } from "react";
const Page = ({ children, index }) => {
const ref = useRef(null);
return (
<div ref={ref} style={{ height: "100vh" }}>
{children}
</div>
);
};
const App = () => {
const [activePage, setActivePage] = useState(0);
const pages = [...Array(5)].map((_, index) => (
<Page key={index} index={index}>
Page {index + 1}
</Page>
));
const handleScroll = () => {
const pageRefs = pages.map((page) => page.ref.current);
let maxVisiblePageIndex = 0;
let maxVisiblePageAmount = 0;
pageRefs.forEach((pageRef, index) => {
const { top, bottom } = pageRef.getBoundingClientRect();
const visibleAmount =
top > 0 && bottom < window.innerHeight
? bottom - top
: Math.min(Math.abs(top), Math.abs(bottom));
if (visibleAmount > maxVisiblePageAmount) {
maxVisiblePageIndex = index;
maxVisiblePageAmount = visibleAmount;
}
});
setActivePage(maxVisiblePageIndex);
};
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
return <div>{pages}</div>;
};
export default App;
I want it to scroll like it does in Tesla.com

why useRef current value , isn't sharing trough custom hook?

I wanted to calculate the user scroll height , so I created a custom hook. and I wanted to share this value to another component. but it doesnt work.
code:
const useScroll = () => {
let scrollHeight = useRef(0);
const scroll = () => {
scrollHeight.current =
window.pageYOffset ||
(document.documentElement || document.body.parentNode || document.body)
.scrollTop;
};
useEffect(() => {
window.addEventListener("scroll", scroll);
return () => {
window.removeEventListener("scroll", () => {});
};
}, []);
return scrollHeight.current;
};
export default useScroll;
the value is not updating here.
but if I use useState here , it works. but that causes tremendous amount of component re-rendering. can you have any idea , how its happening?
Since the hook won't rerender you will only get the return value once. What you can do, is to create a useRef-const in the useScroll hook. The useScroll hook returns the reference of the useRef-const when the hook gets mounted. Because it's a reference you can write the changes in the useScroll hook to the useRef-const and read it's newest value in a component which implemented the hook. To reduce multiple event listeners you should implement the hook once in the parent component and pass the useRef-const reference to the child components. I made an example for you.
The hook:
import { useCallback, useEffect, useRef } from "react";
export const useScroll = () => {
const userScrollHeight = useRef(0);
const scroll = useCallback(() => {
userScrollHeight.current =
window.pageYOffset ||
(document.documentElement || document.body.parentNode || document.body)
.scrollTop;
}, []);
useEffect(() => {
window.addEventListener("scroll", scroll);
return () => {
window.removeEventListener("scroll", scroll);
};
}, []);
return userScrollHeight;
};
The parent component:
import { SomeChild, SomeOtherChild } from "./SomeChildren";
import { useScroll } from "./ScrollHook";
const App = () => {
const userScrollHeight = useScroll();
return (
<div>
<SomeChild userScrollHeight={userScrollHeight} />
<SomeOtherChild userScrollHeight={userScrollHeight} />
</div>
);
};
export default App;
The child components:
export const SomeChild = ({ userScrollHeight }) => {
const someButtonClickHandlerWhichPrintsUserScrollHeight = () => {
console.log("userScrollHeight from SomeChild", userScrollHeight.current);
};
return (
<div style={{
width: "100vw",
height: "100vh",
backgroundColor: "aqua"
}}>
<h1>SomeChild 1</h1>
<button onClick={() => someButtonClickHandlerWhichPrintsUserScrollHeight()}>Console.log userScrollHeight</button>
</div>
);
};
export const SomeOtherChild = ({ userScrollHeight }) => {
const someButtonClickHandlerWhichPrintsUserScrollHeight = () => {
console.log("userScrollHeight from SomeOtherChild", userScrollHeight.current);
};
return (
<div style={{
width: "100vw",
height: "100vh",
backgroundColor: "orange"
}}>
<h1>SomeOtherChild 1</h1>
<button onClick={() => someButtonClickHandlerWhichPrintsUserScrollHeight()}>Console.log userScrollHeight</button>
</div>
);
};
import { useRef } from 'react';
import throttle from 'lodash.throttle';
/**
* Hook to return the throttled function
* #param fn function to throttl
* #param delay throttl delay
*/
const useThrottle = (fn, delay = 500) => {
// https://stackoverflow.com/a/64856090/11667949
const throttledFn = useRef(throttle(fn, delay)).current;
return throttledFn;
};
export default useThrottle;
then, in your custom hook:
const scroll = () => {
scrollHeight.current =
window.pageYOffset ||
(document.documentElement || document.body.parentNode || document.body)
.scrollTop;
};
const throttledScroll = useThrottle(scroll)
Also, I like to point out that you are not clearing your effect. You should be:
useEffect(() => {
window.addEventListener("scroll", throttledScroll);
return () => {
window.removeEventListener("scroll", throttledScroll); // remove Listener
};
}, [throttledScroll]); // this will never change, but it is good to add it here. (We've also cleaned up effect)

How to test an if sentence inside useEffect?

I'm trying to test an if sentence inside a useEffect which main dependency, riskSelection, comes from a useSelector from react-redux library. Jest coverage indicates line 20-21 is missing.
App.tsx
function App(): JSX.Element {
const riskSelection = useAppSelector(selectRiskSelection);
const {save} = useActions({...riskSelectorActions});
const risks: Risk = risks_levels;
const [data, setData] = useState<Data[]>();
const width = 450,
height = 450,
margin = 40;
const radius = Math.min(width, height) / 2 - margin;
useEffect(() => {
if (typeof riskSelection !== 'undefined') { // line 20
setData(Object.values(risks[riskSelection])); // line 21
}
}, [riskSelection, risks]);
return (
<div>
<ul className={style.riskSelectorUl}>
{new Array(10).fill(0).map((v, i: number) => (
<li
className={Number(riskSelection) === i + 1 ? style.selected : ''}
onClick={() => save({riskSelection: `${i + 1}`})}
key={i}
>
{i + 1}
</li>
))}
</ul>
{data && (
<PieSVG
data={data}
width={width}
height={height}
innerRadius={100}
outerRadius={radius}
/>
)}
</div>
);
}
export default App;
My test should verify if PieSVG has been rendered or not. In order to do so I have to simulate a change in riskSelection. This is where I need help.
App.test.tsx
jest.mock('../utils/redux/hooks');
import * as reduxHooks from '../utils/redux/hooks';
describe('App', () => {
let wrapper: any;
beforeEach(() => {
jest
.spyOn(reduxHooks, 'useAppSelector')
.mockImplementation((f) => f({riskSelector: {riskSelection: undefined}}));
wrapper = shallow(<Dummy />);
});
afterEach(() => {
jest.clearAllMocks();
});
it('shows pie chart', () => {
reduxHooks.useAppSelector(() => '1');
expect(wrapper.find(PieSVG)).toHaveLength(1);
});
});
I use mockImplementation to try to change riskSelector but that just initializes my redux state.
The user trigger this change of redux state when it click on a <li /> which uses save action.
Maybe I should not make my UI changes so dependent on a useEffect?
hook.ts
import {TypedUseSelectorHook, useSelector} from 'react-redux';
import type {RootState} from './store';
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

How to render children within react functional components using react and typescript?

i want to render the component that is passed to the react functional component using react and typescript.
below is my code,
function MainComponent () {
return (
<Wrapper>
<SomeOtheComponent/>
<DragAndDrop>
<ListContent> //this is the children for the DragAndDrop component and want this to
//render within DragAndDrop component
{condition_true && (
<FirstList/>
)}
{condition_true && (
<SecondList/>
)}
</ListContent>
</Wrapper>
);
}
const DragAndDrop: React.FC = ({ children }) => { //here i am passing children
const [dragging, setDragging] = useState(false);
const [dragCounter, setDragCounter] = useState(0);
React.useEffect(() => {
// componentDidMount()
setDragCounter(0);
}, []);
const handleDrag = (e: any) => {
e.preventDefault();
e.stopPropagation();
};
const handleDragIn = (e: any) => {
e.preventDefault();
e.stopPropagation();
setDragCounter((prev: number) => prev + 1);
if (e.dataTransfer.files) {
setDragging(true);
}
};
const handleDragOut = (e: any) => {
e.preventDefault();
e.stopPropagation();
setDragCounter((prev: number) => prev - 1);
if (dragCounter === 0) {
setDragging(false);
}
};
const handleDrop = (e: any) => {
e.preventDefault();
e.stopPropagation();
setDragging(false);
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
e.dataTransfer.clearData();
setDragCounter(0);
}
};
return (
<DropZone
ref={dropRef}
onDragEnter={handleDragIn}
onDragLeave={handleDragOut}
onDragOver={handleDrag}
onDrop={handleDrop}
>
{dragging && <DropZoneOverlay>{children}</DropZoneOverlay>}
</DropZone>
);
};
Now the question is why doesnt ListContent within DragAndDrop Component render....I am not sure if there is something wrong with the way i pass children to DragAndDrop component in MainComponent. could someone help me with this. thanks.
The dragging value is set to false from the useState call in DragAndDrop, so on first render, the DropZoneOverlay won't get rendered.
{dragging && <DropZoneOverlay>{children}</DropZoneOverlay>}

Strange React hooks behavior, can't access new state from a function

I use the library react-use-modal, and
I'm trying to read the updated value of confirmLoading when inside the handleClick function.
handleClick does read the first value of confirmLoading defined when doing const [ confirmLoading, setConfirmLoading ] = useState(false), but never updates when I setConfirmLoading inside handleOk.
I don't understand what I'm doing wrong
import { Button, Modal as ModalAntd } from 'antd'
import { useModal } from 'react-use-modal'
export interface ModalFormProps {
form: React.ReactElement
}
export const ModalForm: React.FC = () => {
const [ confirmLoading, setConfirmLoading ] = useState(false)
const { showModal, closeModal } = useModal()
const handleOk = () => {
setConfirmLoading(true)
setTimeout(() => {
setConfirmLoading(false)
closeModal()
}, 1000)
}
const handleCancel = () => {
closeModal()
}
const handleClick = () => {
showModal(({ show }) => (
<ModalAntd
onCancel={handleCancel}
onOk={handleOk}
title='Title'
visible={show}
>
// the value of confirmLoading is always the one defined
// with useState at the beginning of the file.
<p>{confirmLoading ? 'yes' : 'no'}</p>
</ModalAntd>
))
}
return (
<div>
<Button onClick={handleClick}>
Open Modal
</Button>
</div>
)
}
This is happening because of closures. The component that you pass to showModal remembers confirmLoading and when you call function setConfirmLoading your component renders again and function handleClick is recreated. 'Old' handleClick and 'old' component in showModal know nothing about the new value in confirmLoading.
Try to do this:
export const ModalForm: React.FC = () => {
const { showModal, closeModal } = useModal();
const handleClick = () => {
showModal(({ show }) => {
const [ confirmLoading, setConfirmLoading ] = useState(false);
const handleOk = () => {
setConfirmLoading(true)
setTimeout(() => {
setConfirmLoading(false)
closeModal()
}, 1000)
};
const handleCancel = () => {
closeModal()
};
return (
<ModalAntd
onCancel={handleCancel}
onOk={handleOk}
title='Title'
visible={show}
>
// the value of confirmLoading is always the one defined
// with useState at the beginning of the file.
<p>{confirmLoading ? 'yes' : 'no'}</p>
</ModalAntd>
);
})
};
return (
<div>
<Button onClick={handleClick}>
Open Modal
</Button>
</div>
)
}

Resources