I'm new to Jest and the testing library. I have a NavbarContainer component that renders one button or another depending on a variable (menuOpen) that it's being changed with a function. I have already checked that the variable changes its value after the fireEvent, however, it seems like the component it's not updating. What am I doing wrong?
Here there is my component
export const NavbarContainer = ({functionality, menuOpen, activeLink}) => {
return (
<div className="navbar-container">
{ menuOpen && <Navbar functionality={functionality} activeLink={activeLink}/> }
{ menuOpen && <Button text="navbar.back" functionality={functionality}></Button> }
{ !menuOpen && <Button text="navbar.menu" functionality={functionality} modificator="back"></Button> }
</div>
);
};
Here is the button
export const Button = ({ text, type = "button", functionality, disabled = false}) => {
return (
<button onClick={functionality} type={type} disabled={disabled}>{i18n.t(text)}</button>
);
};
Here are the values and functions I am passing to the NavbarContainer component
const [menuOpen, setMenuOpen] = useState(false);
const [activeLink, setActiveLink] = useState("");
const openMenu = (link) => {
setMenuOpen(!menuOpen);
setActiveLink(link.toString());
};
Here are my tests
describe("NavbarContainer", () => {
i18n.changeLanguage('cimode');
let component;
let menuOpen = true;
const openMenu = () => {
menuOpen = !menuOpen;
};
beforeEach(() => {
component = render(
<BrowserRouter basename="/">
<NavbarContainer menuOpen={menuOpen} functionality={openMenu} activeLink=""/>
</BrowserRouter>
);
});
it("after click the menu button is shown", async () => {
const backButton = component.queryByText("navbar.back");
await fireEvent.click(backButton);
const menuButton = await component.queryByText("navbar.menu");
expect(menuButton).toBeInTheDocument();
});
});
Here is the error I'm getting
expect(received).toBeInTheDocument()
the received value must be an HTMLElement or an SVG element.
Received has value: null
Since it might take a few seconds for your navbar.menu to appear you after your click, you need to use findBy to try and select your item. This adds a timeout of 5 seconds to try and find the item. If it still hasn't appeared after 5 seconds, then there is probably something else going on with your code.
If you want your test to be async, then I would recommend that your first call also be a findBy rather than a queryBy since queryBy isn't an asynchronous call.
it("after click the menu button is shown", async () => {
const backButton = await component.findyByText("navbar.back");
await fireEvent.click(backButton);
const menuButton = await component.findByText("navbar.menu");
expect(menuButton).toBeInTheDocument();
});
You also probably don't need that last expect call because if it can't find the navbar.menu item via the previous call, then it would fail anyway. Same if it finds it.
Related
I would like to ask if I have the variable useState in the component which is used as a condition that determines the element inside it will appear or not. How to mock the variable? So that I can test the element inside the condition if the value is 'login'.
const [dataHistory,seDataHistory] = useState("")
const [data,setData] = useState("firstTimeLogin")
const func2 = () => {
getData({
item1: "0",
}).
then((res) => {
funct();
})
}
const funct = () =>
{setData("login")}
return
{data === "firstTimeLogin" && (
<div><button onClick="funct2()">next</button></div>
)}
{data === "login" && (
<div>flow login</div>
)}
Firstly, you need to add data-testid for your button
{data === "firstTimeLogin" && (
<div><button onClick="funct2" data-testid="next-button">next</button></div>
)}
You called onClick="funct2()" which is to trigger funct2 immediately after re-rendering, so I modified it to onClick="funct2".
Note that you also can use next content in the button for the event click but I'd prefer using data-testid is more fixed than next content.
In your test file, you should mock getData and call fireEvent.click to trigger funct2()
import { screen, fireEvent, render } from '#testing-library/react';
//'/getData' must be the actual path you're importing for `getData` usage
jest.mock('/getData', () => {
return {
getData: () => new Promise((resolve) => { resolve('') }) // it's mocked, so you can pass any data here
}
})
it('should call funct', async () => {
render(<YourComponent {...yourProps}/>)
const nextButton = await screen.findByTestId("next-button"); //matched with `data-testid` we defined earlier
fireEvent.click(nextButton); //trigger the event click on that button
const flowLoginElements = await screen.findByText('flow login'); //after state changes, you should have `flow login` in your content
expect(flowLoginElements).toBeTruthy();
})
You can create a button and onClick of the button call funct()
What i have is the simple page where I have the "Reset Password" button and "Confirm" button by defaut. If you will press "Reset password" it will be fetched from the server and displayed where this button was in "TextInput". Aslo "Confirm" button become active after that.
Here is my component.
export default function App({ profileId, onClose }) {
const [ resetPasswordIsPressed, setResetPasswordIsPressed ] = useState(false);
const [ temporaryPassword, setTemporaryPassword ] = useState(null);
const [ isButtonDisabled, setIsButtonDisabled ] = true;
const handleUserUpdate = () => {
handleChangePassword(false);
onClose()
}
const handleChangePassword = async isPressed => {
const tempPassword = await fetchPassword(isPressed, profileId);
setTemporaryPassword(tempPassword);
setResetPasswordIsPressed(isPressed);
setIsButtonDisabled(false);
}
return (
<div className="App">
{ resetPasswordIsPressed ? (
<TextInput
isDisabled
testId='passwordInput'
value={temporaryPassword}
/>
) : (
<PasswrodResetButton
onClick={() => handleChangePassword(true)}
text='PasswordReset'
/>
)
}
<ConfirmButton isDisabled={isButtonDisabled} onClick={handleUserUpdate}/>
</div>
);
}
and here is fetch function which is imported from separate file
const fetchPassword = async (isPressed, profileId) => {
const getPassword = httpPost(userService, userendPoint);
try {
const {data} = await getPassword({
data: {
profileId
}
});
const { tempPassword } = data;
return tempPassword;
} catch (e) {
console.log(e.message)
}
}
What i need is to test that after clicking "Reset Password" handler was called, after that the "Reset Password" button is not on the page anymore, the "TextInput" with fetched password IS on the page, and the confirm button become active. Here is what I'm trying to do.
describe('User handlers', () => {
it('Should trigger reset password handler', async () => {
const mockCallBack = jest.fn();
const action = shallow(<PasswordResetButton onClick={mockCallBack}/>);
action.simulate('click');
await screen.findByTestId('passwordInput')
})
})
But got 'Unable to find and element by: [data-testid="passwordInput"]'
You're rendering your 'action' separately from your App component, which makes it completely independent.
I can't see in your code what 'screen' is, but most likely it's your shallowly rendered App component, and this is exactly where you need to find your <PasswordResetButton /> and click it.
If you were to add to your <PasswordResetButton /> a testid like you did to <TextInput />, you'd be able to do something like this:
it('Should trigger reset password handler', async () => {
const action = await screen.findByTestId('#passwordResetButton');
action.simulate('click');
const passwordInput = await screen.findByTestId('passwordInput');
expect(passwordInput).toHaveLength(1);
})
But again, I'm not sure what your screen variable is and what exactly screen.findByTestId does and returns.
I am using ResizeObserver to call a function when the screen is resized, but I need to get the updated value of a state within the observer in order to determine some conditions before the function gets invoked.
It's something like this:
let [test, setTest] = React.useState(true)
const callFunction = () => {
console.log('function invoked')
setTest(false) // => set 'test' to 'false', so 'callFunction' can't be invoked again by the observer
}
const observer = React.useRef(
new ResizeObserver(entries => {
console.log(test) // => It always has the initial value (true), so the function is always invoked
if (test === true) {
callFunction()
}
})
)
React.useEffect(() => {
const body = document.getElementsByTagName('BODY')[0]
observer.current.observe(body)
return () => observer.unobserve(body)
}, [])
Don't worry about the details or why I'm doing this, since my application is way more complex than this example.
I only need to know if is there a way to get the updated value within the observer. I've already spent a considerable time trying to figure this out, but I couldn't yet.
Any thoughts?
The problem is, you are defining new observer in each re render of the component, Move it inside useEffect will solve the problem. also you must change this observer.unobserve(body) to this observer..current.unobserve(body).
I have created this codesandbox to show you how to do it properly. this way you don't need external variable and you can use states safely.
import { useEffect, useState, useRef } from "react";
const MyComponent = () => {
const [state, setState] = useState(false);
const observer = useRef(null);
useEffect(() => {
observer.current = new ResizeObserver((entries) => {
console.log(state);
});
const body = document.getElementsByTagName("BODY")[0];
observer.current.observe(body);
return () => observer.current.unobserve(body);
}, []);
return (
<div>
<button onClick={() => setState(true)}>Click Me</button>
<div>{state.toString()}</div>
</div>
);
};
export default MyComponent;
I have a DatePicker component like this:
const MonthPanel = ({ setMode = () => {} }) => {
return (
<div>
<button
onClick={(e) => setMode("year")}
data-testid="datepicker-year-button"
>
2021
</button>
</div>
);
};
const YearPanel = () => {
return <div data-testid="datepicker-year-panel">YearPanel</div>;
};
const DatePicker = (props) => {
const [open, setOpen] = useState(false);
const [mode, setMode] = useState("month");
const containerRef = useRef();
const handleOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const handleModeChange = (newMode) => {
setMode(newMode);
};
const generatePanel = () => {
switch (mode) {
case "month":
return <MonthPanel setMode={handleModeChange} />;
case "year":
return <YearPanel />;
default:
return null;
}
};
useEffect(() => {
const handleOutsideClick = (e) => {
if (containerRef.current && !containerRef.current.contains(e.target)) {
handleClose();
}
};
window.addEventListener("click", handleOutsideClick);
return () => {
window.removeEventListener("click", handleOutsideClick);
};
}, []);
return (
<div
ref={containerRef}
data-testid="datepicker-container"
>
<div onClick={handleOpen} data-testid="datepicker-input">
Select a date
</div>
{open && <div data-testid="datepicker-dialog">{generatePanel()}</div>}
</div>
);
};
And I have a test file like this:
it.only("should close DatePicker dialog when clicked only outside DatePicker", () => {
const { getByTestId, queryByTestId } = render(
<DatePicker />
);
userEvent.click(getByTestId("datepicker-input"));
userEvent.click(getByTestId("datepicker-year-button"));
expect(queryByTestId("datepicker-dialog")).toBeInTheDocument();
userEvent.click(document.body);
expect(queryByTestId("datepicker-dialog")).toBeNull();
});
Desired state:
When you click outside DatePicker, it should close. And when you click [data-testid="datepicker-year-button"] it should change DatePicker mode to "year", so the year panel will be shown.
Current state:
When you click [data-testid="datepicker-year-button"], it changes Datepicker mode to "year" and MonthPanel (and with it button itself) are removed. Because the button is event target and has already removed, containerRef.current.contains(e.target) condition is false and Dialog will be removed too. But the test is showing that dialog is in the document.
The question is how I should test this functionality correctly.
You could call e.stopPropagation() on your button click handler in <MonthPanel>, to prevent the event bubbling up and being caught by the window's eventListener.
<button
onClick={(e) => {
e.stopPropagation();
setMode("year");
}}
data-testid="datepicker-year-button"
>
Try with async methods like waitFor or waitForElementToBeRemoved:
await waitFor(() => {
expect(queryByTestId("datepicker-dialog")).toBeNull();
});
React-dom introduced act API to wrap code that renders or updates components, this makes your test run closer to how React works in the browser. React testing library wraps some of its APIs in the act function, but in some cases, you would still need to use waitFor, or waitForElementToBeRemoved
I believe your userEvent.click(document.body); is not working properly.
Most likely document.body isn't resolved the way it should.
you're probably using testing-library user-event, and their basic example is:
import { screen } from '#testing-library/dom'
import userEvent from '#testing-library/user-event'
test('types inside textarea', () => {
document.body.innerHTML = `<textarea />`
userEvent.type(screen.getByRole('textbox'), 'Hello, World!')
expect(screen.getByRole('textbox')).toHaveValue('Hello, World!')
})
check that your document.body.innerHTML is set to something
I've been trying to build an React app with multiple alerts that disappear after a set amount of time. Sample: https://codesandbox.io/s/multiple-alert-countdown-294lc
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function TimeoutAlert({ id, message, deleteAlert }) {
const onClick = () => deleteAlert(id);
useEffect(() => {
const timer = setTimeout(onClick, 2000);
return () => clearTimeout(timer);
});
return (
<p>
<button onClick={onClick}>
{message} {id}
</button>
</p>
);
}
let _ID = 0;
function App() {
const [alerts, setAlerts] = useState([]);
const addAlert = message => setAlerts([...alerts, { id: _ID++, message }]);
const deleteAlert = id => setAlerts(alerts.filter(m => m.id !== id));
console.log({ alerts });
return (
<div className="App">
<button onClick={() => addAlert("test ")}>Add Alertz</button>
<br />
{alerts.map(m => (
<TimeoutAlert key={m.id} {...m} deleteAlert={deleteAlert} />
))}
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
The problem is if I create multiple alerts, it disappears in the incorrect order. For example, test 0, test 1, test 2 should disappear starting with test 0, test 1, etc but instead test 1 disappears first and test 0 disappears last.
I keep seeing references to useRefs but my implementations don't resolve this bug.
With #ehab's input, I believe I was able to head down the right direction. I received further warnings in my code about adding dependencies but the additional dependencies would cause my code to act buggy. Eventually I figured out how to use refs. I converted it into a custom hook.
function useTimeout(callback, ms) {
const savedCallBack = useRef();
// Remember the latest callback
useEffect(() => {
savedCallBack.current = callback;
}, [callback]);
// Set up timeout
useEffect(() => {
if (ms !== 0) {
const timer = setTimeout(savedCallBack.current, ms);
return () => clearTimeout(timer);
}
}, [ms]);
}
You have two things wrong with your code,
1) the way you use effect means that this function will get called each time the component is rendered, however obviously depending on your use case, you want this function to be called once, so change it to
useEffect(() => {
const timer = setTimeout(onClick, 2000);
return () => clearTimeout(timer);
}, []);
adding the empty array as a second parameter, means that your effect does not depend on any parameter, and so it should only be called once.
Your delete alert depends on the value that was captured when the function was created, this is problematic since at that time, you don't have all the alerts in the array, change it to
const deleteAlert = id => setAlerts(alerts => alerts.filter(m => m.id !== id));
here is your sample working after i forked it
https://codesandbox.io/s/multiple-alert-countdown-02c2h
well your problem is you remount on every re-render, so basically u reset your timers for all components at time of rendering.
just to make it clear try adding {Date.now()} inside your Alert components
<button onClick={onClick}>
{message} {id} {Date.now()}
</button>
you will notice the reset everytime
so to achieve this in functional components you need to use React.memo
example to make your code work i would do:
const TimeoutAlert = React.memo( ({ id, message, deleteAlert }) => {
const onClick = () => deleteAlert(id);
useEffect(() => {
const timer = setTimeout(onClick, 2000);
return () => clearTimeout(timer);
});
return (
<p>
<button onClick={onClick}>
{message} {id}
</button>
</p>
);
},(oldProps, newProps)=>oldProps.id === newProps.id) // memoization condition
2nd fix your useEffect to not run cleanup function on every render
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
finally something that is about taste, but really do you need to destruct the {...m} object ? i would pass it as a proper prop to avoid creating new object every time !
Both answers kind of miss a few points with the question, so after a little while of frustration figuring this out, this is the approach I came to:
Have a hook that manages an array of "alerts"
Each "Alert" component manages its own destruction
However, because the functions change with every render, timers will get reset each prop change, which is undesirable to say the least.
It also adds another lay of complexity if you're trying to respect eslint exhaustive deps rule, which you should because otherwise you'll have issues with state responsiveness. Other piece of advice, if you are going down the route of using "useCallback", you are looking in the wrong place.
In my case I'm using "Overlays" that time out, but you can imagine them as alerts etc.
Typescript:
// useOverlayManager.tsx
export default () => {
const [overlays, setOverlays] = useState<IOverlay[]>([]);
const addOverlay = (overlay: IOverlay) => setOverlays([...overlays, overlay]);
const deleteOverlay = (id: number) =>
setOverlays(overlays.filter((m) => m.id !== id));
return { overlays, addOverlay, deleteOverlay };
};
// OverlayIItem.tsx
interface IOverlayItem {
overlay: IOverlay;
deleteOverlay(id: number): void;
}
export default (props: IOverlayItem) => {
const { deleteOverlay, overlay } = props;
const { id } = overlay;
const [alive, setAlive] = useState(true);
useEffect(() => {
const timer = setTimeout(() => setAlive(false), 2000);
return () => {
clearTimeout(timer);
};
}, []);
useEffect(() => {
if (!alive) {
deleteOverlay(id);
}
}, [alive, deleteOverlay, id]);
return <Text>{id}</Text>;
};
Then where the components are rendered:
const { addOverlay, deleteOverlay, overlays } = useOverlayManger();
const [overlayInd, setOverlayInd] = useState(0);
const addOverlayTest = () => {
addOverlay({ id: overlayInd});
setOverlayInd(overlayInd + 1);
};
return {overlays.map((overlay) => (
<OverlayItem
deleteOverlay={deleteOverlay}
overlay={overlay}
key={overlay.id}
/>
))};
Basically: Each "overlay" has a unique ID. Each "overlay" component manages its own destruction, the overlay communicates back to the overlayManger via prop function, and then eslint exhaustive-deps is kept happy by setting an "alive" state property in the overlay component that, when changed to false, will call for its own destruction.