Here's a Toast component that is displayed when a button is clicked and disappears after x seconds.
For testing waitFor is used to change showToast state to true when API call is successful, then waitForElementToBeRemoved is used to check if the toast component has been removed from the screen.
Tests are passing, so the assumption is that showToast became false. But when I check the jest coverage, that line hide={() => setShowToast(false)} is still shown as uncovered.
What would be needed to cover that line using testing-library?
Toast component:
const Toast = props => {
const { message, color, iconName, show, hide, background, timeoutDuration, ...rest } = props;
useEffect(() => {
if (show) {
const timeout = setTimeout(() => {
hide();
}, timeoutDuration * 1000 + 1000);
return () => clearTimeout(timeout);
}
}, [show, timeoutDuration]);
return (
<Box>
<Container {...rest} show={show} timeoutDuration={timeoutDuration}>
<StyledToast py={1} px={2} background={background} bgColor={colors[color]} role="alert">
<StyledIcon name={iconName} color={color} />
<StyledP color={color} fontSize={[14, 16]}>
{message}
</StyledP>
</StyledToast>
</Container>
</Box>
);
};
Component that uses Toast:
const [showToast, setShowToast] = useState(false);
{showToast && (
<Toast
message="Store Settings successfully updated!"
color="green"
iconName="check-circle"
background={true}
show={showToast}
timeoutDuration={10}
zIndex={10}
hide={() => setShowToast(false)}
/>
)}
Test:
import '#testing-library/jest-dom';
import { render, screen, fireEvent, waitFor, waitForElementToBeRemoved } from '#testing-library/preact';
test('Clicking OK displays Toast and it disappears', async () => {
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ data: {}] } })
})
);
const CheckBox = screen.getByTestId('some-id');
fireEvent.click(CheckBox);
const SaveButton = screen.getByText('Save');
fireEvent.click(SaveButton);
expect(screen.getByText('OK')).toBeTruthy();
const OKButton = screen.getByText('OK');
fireEvent.click(OKButton);
await waitFor(() => {
expect(screen.getByText('Store Settings successfully updated!')).toBeInTheDocument();
}, { timeout: 11000 });
waitForElementToBeRemoved(screen.getByText('Store Settings successfully updated!')).then(() =>
expect(screen.queryByText('Store Settings successfully updated!')).not.toBeInTheDocument()
);
});
Try
await waitForElementToBeRemoved(...)
since waitForElementToBeRemoved is an async function call
Related
I am trying to test a React error boundary (with react-error-boundary library) by creating a button (to test that the code inside the library is displayed) and when clicked I want to verify that the error text is displayed, here are some screenshots
Here are the 2 files used:
error-fallback.tsx
import Box from "#mui/material/Box";
function ErrorFallback({ error }: { error: Error; resetErrorBoundary: () => void }) {
return (
<Box sx={{ textAlign: "center" }}>
<p>Error displaying this section</p>
<pre>{error.message}</pre>
</Box>
);
}
export default ErrorFallback;
error-fallback.test.tsx
import { render, fireEvent } from "#testing-library/react";
import { ErrorBoundary } from "react-error-boundary";
import Box from "#mui/material/Box";
import { useState } from "react";
import ErrorFallback from "./error-fallback";
const buttonText = "Throw Error";
const errorValue = "This is a test error";
const ThrowError = () => {
throw new Error(errorValue);
};
export const ErrorFallbackTest = () => {
const [error, setError] = useState(false);
const triggerError = () => setError(true);
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Box sx={{ textAlign: "center" }}>
{error && <ThrowError />}
<button onClick={triggerError}>{buttonText}</button>
</Box>
</ErrorBoundary>
);
};
it("react boundary works", async () => {
const { getByText } = render(<ErrorFallbackTest />);
const buttonTrigger = getByText(buttonText);
fireEvent(buttonTrigger, new MouseEvent("click", { bubbles: true }));
const boundaryText = getByText("Error displaying this section");
expect(boundaryText).toBeInTheDocument();
expect(boundaryText).toBeDefined();
expect(boundaryText).toBeVisible();
});
Attaching Jest error log. I am not sure which is the proper way to handle this, should I expect an error with Jest? How can I add it if the error is expected to be thrown on button click?
● Console
console.error
Error: Uncaught [Error: This is a test error]
Found the answer here
https://stackoverflow.com/a/54764497/9296982
https://github.com/facebook/react/issues/11098#issuecomment-523977830
it("react boundary works", async () => {
const { getByText } = render(<ErrorFallbackTest />);
const buttonTrigger = getByText(buttonText);
const spy = jest.spyOn(console, "error");
spy.mockImplementation(() => {});
fireEvent(buttonTrigger, new MouseEvent("click", { bubbles: true }));
const boundaryText = getByText("Error displaying this section");
expect(boundaryText).toBeInTheDocument();
expect(boundaryText).toBeDefined();
expect(boundaryText).toBeVisible();
spy.mockRestore();
});
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()
})
})
})
Im very new to React and im having an issue Im not sure how to troubleshoot. So im setting an array on the context when a http request fails in a custom hook
Here is my hook:
const useHttp = (requestObj: any, setData: Function) =>
{
const [isLoading, setIsLoading] = useState(false);
const ctx = useContext(GlobalContext);
const sendRequest = useCallback(() =>
{
setIsLoading(true);
fetch(requestObj.url, {
method: requestObj.method ? requestObj.method: 'GET',
headers: requestObj.headers ? requestObj.headers : {},
body: requestObj.body ? JSON.stringify(requestObj.body) : null
})
.then(res => res.json())
.then(data => {
setIsLoading(false);
setData(data);
})
.catch(err =>
{
setIsLoading(false);
ctx.setErrors([
(prevErrors: string[]) =>
{
//prevErrors.push(err.message)
let newArray = prevErrors.map((error) => {return error});
newArray.push(err.message);
return newArray;
}]
);
console.log('There was an error');
});
}, []);
return {
isLoading: isLoading,
sendRequest: sendRequest
}
}
Im using .map cos the spread operator for arrays isnt working. Im looking into it but its not important for this.
When there are errors I create a modal and then render it in my jsx. My problem is that for some reason my Modal is rendering twice. The second time it has no props and that blows up my program. I dont know why its rendering again and I dont know how to attack the problem. The stack has nothing with regards to what is causing it (that I can see). If a component is rendering again would the props not be the same as originally used? I have breakpoints in the spot where the modal is called and they arent getting hit again. So can anyone offer some advice for how I go about debugging this?
const App: FC = () => {
const [errors, setErrors] = useState([]);
let modal = null
if(errors.length > 0)
{
modal = (
<Modal
heading="Warning"
content={<div>{errors}</div>}
buttonList={
[
{label: "OK", clickHandler: ()=> {}, closesModal: true},
{label: "Cancel", clickHandler: ()=> {alert("cancelled")}, closesModal: false}
]
}
isOpen={true}/>
)
}
return (
<GlobalContext.Provider value={{errors: errors, setErrors: setErrors}}>
<ProviderV3 theme={defaultTheme}>
<Toolbar></Toolbar>
<Grid
margin='25px'
columns='50% 50%'
gap='10px'
maxWidth='100vw'>
<OwnerSearch />
<NewOwnerSearch />
</Grid>
</ProviderV3>
{modal}
</GlobalContext.Provider>
);
};
import { FC, useState } from 'react';
import {
ButtonGroup, Button, DialogContainer,
Dialog, Content, Heading, Divider
} from '#adobe/react-spectrum';
type Props = {
heading: string,
content : any,
buttonList: {label: string, clickHandler: Function, closesModal: boolean}[],
isOpen: boolean
}
const Modal: FC<Props> = ( props ) =>
{
const [open, setOpen] = useState(props.isOpen);
let buttons = props.buttonList.map((button, index) =>
{
return <Button key={index} variant="cta" onPress={() => close(button.clickHandler, button.closesModal)} autoFocus>
{button.label}
</Button>
});
const close = (clickHandler: Function | null, closesModal: boolean) =>
{
if(clickHandler != null)
{
clickHandler()
}
if(closesModal)
{
setOpen(false)
}
}
return (
<DialogContainer onDismiss={() => close(null, true)} >
{open &&
<Dialog>
<Heading>{props.heading}</Heading>
<Divider />
<Content>{props.content}</Content>
<ButtonGroup>
{buttons}
</ButtonGroup>
</Dialog>
}
</DialogContainer>
);
}
export default Modal;
Following Firefighters suggestion I get an error now:
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
at resolveDispatcher (react.development.js:1476)
at useContext (react.development.js:1484)
at useProvider (module.js:239)
at $bc3300334f45fd1ec62a173e70ad86$var$Provider (module.js:95)
at describeNativeComponentFrame (react-dom.development.js:946)
at describeFunctionComponentFrame (react-dom.development.js:1034)
at describeFiber (react-dom.development.js:1119)
at getStackByFiberInDevAndProd (react-dom.development.js:1138)
at createCapturedValue (react-dom.development.js:20023)
at throwException (react-dom.development.js:20351)
Try putting the open state inside the App component and remove it from the Modal component:
const [errors, setErrors] = useState([]);
const [isModalOpen, setIsModalOpen] = useState(false);
useEffect(() => {
if(errors.length > 0) setIsModalOpen(true);
}, [errors])
<Modal
...
isOpen={isModalOpen}
setIsOpen={setIsModalOpen}
/>
Basically the title.
Here is the overview of the App:
const App = () => {
const [isViewFavoriteImages, setIsViewFavoriteImages] = useState(false);
const toggleIsViewFavoriteImages = () => {
setIsViewFavoriteImages(
(prevToggleIsViewFavoriteImagesState) =>
!prevToggleIsViewFavoriteImagesState
);
};
return (
<div className="App">
<div className="container">
<ToggleImagesViewButton
toggleIsViewFavoriteImages={toggleIsViewFavoriteImages}
isViewFavoriteImages={isViewFavoriteImages}
/>
<ImageList isViewFavoriteImages={isViewFavoriteImages} />
</div>
</div>
);
};
export default App;
The button component:
export interface ToggleImageViewButtonProps {
toggleIsViewFavoriteImages: () => void;
isViewFavoriteImages: boolean;
}
const ToggleImageViewButton: React.FC<ToggleImageViewButtonProps> = ({
toggleIsViewFavoriteImages,
isViewFavoriteImages,
}) => {
return (
<button
onClick={toggleIsViewFavoriteImages}
className="btn btn_toggle-image-view"
data-testid="toggle-image-view"
>
{isViewFavoriteImages ? "view all" : "view favorites"}
</button>
);
};
export default ToggleImageViewButton;
And this is how I am testing it:
function renderToggleImagesViewButton(
props: Partial<ToggleImageViewButtonProps> = {}
) {
const defaultProps: ToggleImageViewButtonProps = {
toggleIsViewFavoriteImages: () => {
return;
},
isViewFavoriteImages: true,
};
return render(<ToggleImageViewButton {...defaultProps} {...props} />);
}
describe("<ToggleImagesViewButton />", () => {
test("button inner text should change to 'view all' when the user clicks the button", async () => {
const onToggle = jest.fn();
const { findByTestId } = renderToggleImagesViewButton({
toggleIsViewFavoriteImages: onToggle,
});
const toggleImagesViewButton = await findByTestId("toggle-image-view");
fireEvent.click(toggleImagesViewButton);
expect(toggleImagesViewButton).toHaveTextContent("view favorites");
});
});
This test fails and "view all" is still getting returned.
ToggleImageViewButton doesn't have internal state - the state was lifted to the parent, so testing state changes should happen in the parent's tests.
You could have the following integration test to verify the correct behaviour of the button when used in App.
test("App test", () => {
render(<App />);
const button = screen.getByTestId("toggle-image-view");
expect(button).toHaveTextContent("view favorites");
fireEvent.click(button);
expect(button).toHaveTextContent("view all");
});
As for the ToggleImageViewButton unit tests, you can simply test that it renders the right text based on isViewFavoriteImages value, and that the callback gets called when the button is clicked.
test("ToggleImageViewButton test", () => {
const onToggle = jest.fn();
render(<ToggleImageViewButton isViewFavoriteImages={false} toggleIsViewFavoriteImages={onToggle}/>);
expect(screen.getByTestId("toggle-image-view")).toHaveTextContent("view favorites");
fireEvent.click(screen.getByTestId("toggle-image-view"));
expect(onToggle).toHaveBeenCalled();
});
const fetchMusic= () => {
return new Promise((resolve) =>
setTimeout(() => {
const music = musicList.sort(() => 0.5 - Math.random()).slice(0, 4);
resolve({ data: music});
}, 300)
);
};
export default fetchMusic;
const getRandomMusic = () => {
return fetchMusic().then((result) => result.data);
};
const Button = (props) => {
return (
<div>
<Button {...props} onClick={getRandomMusic.bind(this)} />
<SomeComponent />
<p>Some text here</p>
</div>
);
};
I want add a spinner while waiting for the promise to resolve .
fetchMusic is in some other file.I m importing it in a component .
TLDR
How about use useState and useCallback for that action
Answer
At terms of react, use State for loading action is right use case.
So, When to start function, use can setLoading(true) and after action you can setLoading(false) for make loading effect
const fetchMusic= () => {
return new Promise((resolve) =>
setTimeout(() => {
const music = musicList.sort(() => 0.5 - Math.random()).slice(0, 4);
resolve({ data: music});
}, 300)
);
};
export default fetchMusic;
const Button = (props) => {
const [loaidng, setLoading] = useState(false);
const getRandomMusic = useCallback(() => {
setLoading(true)
return fetchMusic().then((result) => {
setLoading(false);
result.data
});
},[]);
return (
<div>
<Button {...props} onClick={getRandomMusic.bind(this)} />
{loading && <Sipinner/>}
<SomeComponent />
<p>Some text here</p>
</div>
);
};
Reference
Example of loading
ETC
If you have any other question. Just give me comment please.