I have the following component that I'm trying to test with react-testing-library:
const PasswordIconButton = ({
stateString
}) => {
const { state, dispatch } = useContext(Store);
const showPassword = getObjectValue(stateString, state);
const toggleShowPassword = event => {
event.preventDefault();
dispatch(toggleBoolean(stateString, !showPassword));
};
return (
<Layout
showPassword={showPassword}
toggleShowPassword={toggleShowPassword}
/>
);
};
export default PasswordIconButton;
const Layout = ({
showPassword,
toggleShowPassword
}) => {
return (
<IconButton onClick={toggleShowPassword} data-testid="iconButton">
{showPassword ? (
<HidePasswordIcon data-testid="hidePasswordIcon" />
) : (
<ShowPasswordIcon data-testid="showPasswordIcon" />
)}
</IconButton>
);
};
This works exactly as intended in production. If the user clicks the button then it calls toggleShowPassword() which toggles the value of boolean const showPassword.
If showPassword is equal to false and the user clicks the button, I can see that the <ShowPasswordIcon /> is removed and <HidePasswordIcon /> appears. Both have the correct data-testid attributes set.
I'm attempting to test the component will the following test:
import React from "react";
import {
render,
cleanup,
fireEvent,
waitForElement
} from "react-testing-library";
import PasswordIconButton from "./PasswordIconButton";
afterEach(cleanup);
const mockProps = {
stateString: "signUpForm.fields.password.showPassword"
};
describe("<PasswordIconButtonIcon />", () => {
it("renders as snapshot", () => {
const { asFragment } = render(<PasswordIconButton {...mockProps} />);
expect(asFragment()).toMatchSnapshot();
});
//
// ISSUE IS WITH THIS TEST:
// ::::::::::::::::::::::::::
it("shows 'hide password' icon on first click", async () => {
const { container, getByTestId } = render(
<PasswordIconButton {...mockProps} />
);
const icon = getByTestId("iconButton");
fireEvent.click(icon);
const hidePasswordIconTestId = await waitForElement(
() => getByTestId("hidePasswordIcon"),
{ container }
);
expect(hidePasswordIconTestId).not.toBeNull();
});
});
The shows 'hide password' icon on first click test always fails and I'm not sure why. The mockProps are definitely correct and work perfectly in production.
What am I missing here?
I figured it out... The issue is that I needed to wrap the component in the context provider as const { state, dispatch } = useContext(Store); won't work properly without it.
So I changed the render to:
const { container, getByTestId } = render(
<StateProvider>
<PasswordIconButton {...mockProps} />
</StateProvider>
);`
And now the test passes fine.
Related
So I'm very new to Prisma, and actually also to React. My Postgresql database works, but I'm trying to show the stored data in my application. My very simple table in the schema file looks like this:
model Hobby {
id Int #id #default(autoincrement())
title String
}
I'm using useContext to distribute my createHobby functionality, this is what the context file looks like.
export async function getServerSideProps() {
const hobbies: Prisma.HobbyUncheckedCreateInput[] = await prisma.hobby.findMany();
return {
props: {initialHobbies: hobbies},
};
}
export const HobbyContext = createContext({})
function Provider({ children, initialHobbies }){
const [hobbies, setHobbies] = useState<Prisma.HobbyUncheckedCreateInput[]>(initialHobbies);
const createHobby = async (title) => {
const body: Prisma.HobbyCreateInput = {
title,
};
await fetcher("/api/create-hobby", {hobby : body});
console.log(hobbies);
const updatedHobbies = [
...hobbies,
body
];
setHobbies(updatedHobbies);
const contextData = {
hobbies,
createHobby,
}
return (
<HobbyContext.Provider value={contextData}>
{children}
</HobbyContext.Provider>
);
};
export default HobbyContext;
export {Provider};
Here I get the following error Uncaught (in promise) TypeError: hobbies is not iterable at createHobby. Which refers to the const updatedHobbies = [...hobbies, body];
For more context, I have a HobbyCreate.tsx which creates a little hobby card that renders the title of the hobby, which is submitted with a form.
function HobbyCreate({updateModalState}) {
const [title, setTitle] = useState('');
const {createHobby} = useHobbiesContext();
const handleChange = (event) => {
setTitle(event.target.value)
};
const handleSubmit = (event) => {
event.preventDefault();
createHobby(title);
};
return (
...
<form onSubmit={handleSubmit}></form>
...
)
I can't really figure out what is going wrong, I assume somewhere when creating the const [hobbies, setHobbies] and using the initialHobbies.
I don't think you're using the Context API correctly. I've written working code to try and show you how to use it.
Fully typed hobby provider implementation
This is a fully typed implementation of your Provider:
import { createContext, useState } from 'react';
import type { Prisma } from '#prisma/client';
import fetcher from 'path/to/fetcher';
export type HobbyContextData = {
hobbies: Prisma.HobbyCreateInput[]
createHobby: (title: string) => void
};
// you could provide a meaningful default value here (instead of {})
const HobbyContext = createContext<HobbyContextData>({} as any);
export type HobbyProviderProps = React.PropsWithChildren<{
initialHobbies: Prisma.HobbyCreateInput[]
}>;
function HobbyProvider({ initialHobbies, children }: HobbyProviderProps) {
const [hobbies, setHobbies] = useState<Prisma.HobbyCreateInput[]>(initialHobbies);
const createHobby = async (title: string) => {
const newHobby: Prisma.HobbyCreateInput = {
title,
};
await fetcher("/api/create-hobby", { hobby: newHobby });
console.log(hobbies);
setHobbies((hobbies) => ([
...hobbies,
newHobby,
]));
};
const contextData: HobbyContextData = {
hobbies,
createHobby,
};
return (
<HobbyContext.Provider value={contextData}>
{children}
</HobbyContext.Provider>
);
}
export default HobbyContext;
export { HobbyProvider };
Using HobbyProvider
You can use HobbyProvider to provide access to HobbyContext for every component wrapped inside it.
For example, to use it in every component on /pages/hobbies your implementation would look like:
// /pages/hobbies.tsx
import { useContext, useState } from 'react';
import HobbyContext, { HobbyProvider } from 'path/to/hobbycontext';
export default function HobbiesPage() {
// wrapping the entire page in the `HobbyProvider`
return (
<HobbyProvider initialHobbies={[{ title: 'example hobby' }]}>
<ExampleComponent />
{/* page content */}
</HobbyProvider>
);
}
function ExampleComponent() {
const { hobbies, createHobby } = useContext(HobbyContext);
const [title, setTitle] = useState('');
return (
<div>
hobbies: {JSON.stringify(hobbies)}
<div>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<button onClick={() => createHobby(title)}>Create hobby</button>
</div>
</div>
);
}
Similarly, to make the context available throughout your entire website, you can use HobbyProvider in
/pages/_app.tsx.
Using getServerSideProps
To retrieve the initialHobbies from the database, your getServerSideProps would look something like this:
// /pages/hobbies.tsx
import type { Hobby } from '#prisma/client';
export async function getServerSideProps() {
// note: there is no need to use `Hobby[]` as prisma will automatically give you the correct return
// type depending on your query
const initialHobbies: Hobby[] = await prisma.hobby.findMany();
return {
props: {
initialHobbies,
},
};
}
You would have to update your page component to receive the props from getServerSideProps and set initialHobbies on HobbyProvider:
// /pages/hobbies.tsx
import type { InferGetServerSidePropsType } from 'next';
export default function HobbiesPage({ initialHobbies }: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<HobbyProvider initialHobbies={initialHobbies}>
<ExampleComponent />
</HobbyProvider>
);
}
Note your page component and getServerSideProps function have to be exported from the same file
I have a gatsby site which runs fine on develop mode but whenever the site is built and deployed and the page is refreshed the site renders erratically.
I suspect the header component which has a window.innerwidth query is the cause.
Here is my code:
export default function Header(props) {
const [state, setState] = useState({ mobileView: true })
const { mobileView } = state
const classes = useStyles()
useEffect(() => {
const setResponsiveness = () => {
return window.innerWidth < 900
? setState((prevState) => ({ ...prevState, mobileView: true }))
: setState((prevState) => ({ ...prevState, mobileView: false }));
};
setResponsiveness();
window.addEventListener("resize", () => setResponsiveness());
return () => {
window.removeEventListener("resize", () => setResponsiveness());
}
}, []);
const MobileMenu = () => {...some code}
const DesktopHeader = () => {...some code}
}
return (
<header>
<AppBar className={classes.root} elevation={0} position='static'>
{mobileView ? <MobileMenu /> : <DesktopHeader />}
</AppBar>
</header>
)
}
```
The link to the deployed site can be found here: [Working Site][1]
[1]: https://brit-tr.com
Got an odd error with FormContext when trying to run my tests. All I'm trying to do is render a component.
So this is the error that I am getting and this is the test that I have written.
import React from 'react';
import UserReportQuestion from './UserReportQuestion';
import { render } from '#testing-library/react';
import { useForm, FormContext } from 'react-hook-form';
describe('(Component) UserReport', () => {
let UserReportQuestionRender;
// jest.mock('react-hook-form', () => ({
// FormContext: jest.fn(),
// useForm: jest.fn(),
// }));
beforeEach(() => {
const form = useForm();
UserReportQuestionRender = render(
<FormContext {...form}>
<UserReportQuestion />
</FormContext>
)
});
it('Should render without crashing', () => {
expect(UserReportQuestionRender);
});
});
In the component I am testing I am using FormContext and passing it useForm as its methods. I've commented everything else out so it is just the FormContext in the component to make sure that it is 100% this that is causing the error.
Wondering if anyone has any ideas on how to work round this?
Update with component
import { useForm, FormContext } from "react-hook-form";
const UserReportQuestion = ({ text }) => {
const { t } = useTranslation();
const [isModalOpen, setModal] = useState(false);
const methods = useForm();
return (
<>
<Question>
{t('UserReport.criteria')}:
{' '}
{/* Placeholder text */}
{/* {text} */}
Find new, creative ways of completing tasks
<ModalIcon onClick={() => setModal(true)}>
<Icon
icon="info"
color="#a3a3a3"
modifiers={['size-large', 'solid']}
/>
</ModalIcon>
</Question>
<InfoModal
isModalOpen={isModalOpen}
closeModal={() => setModal(false)}
title={t('UserReport.modalTitle')}
>
<FormContext {...methods}>
{/* <form onSubmit={methods.handleSubmit()}>
<InfoModalParagraph>
{t('UserReport.modalDescription')}
</InfoModalParagraph>
<FeedbackQuestions
questions={mockData}
selfAssessment={false}
submitButtonDisabled
noSubmitButton
/>
</form> */}
</FormContext>
</InfoModal>
</>
);
}
I think you're importing a component which doesn't exist, in the react-hook-form documentation we have a FormProvider if we want to use useFormContext.
read More about it here: https://react-hook-form.com/api/#useFormContext
but in the mean time you need to change
import {FormContext} from 'react-hook-form'
to this:
import { FormProvider } from 'react-hook-form'
I've created a small keypad app in react and I'm trying to test the input event on the app and for some reason I am not getting the expected result. I'm trying to test it to failure and success. The test I'm running is this below, I want to input 1995 (the correct combination), click the unlock button and ultimately have a message return Unlocked! but it only returns Incorrect Code! which should only happen if the code is incorrect or the input field is empty. But it shouldn't be empty as I have filled it out in the test..
here is a codesandbox: https://codesandbox.io/s/quirky-cloud-gywu6?file=/src/App.test.js:0-26
Any ideas?
test:
const setup = () => {
const utils = render(<App />);
const input = utils.getByLabelText("input-code");
return {
input,
...utils
};
};
test("It should return a successful try", async () => {
const { input, getByTestId } = setup();
await act(async () => {
fireEvent.change(input, { target: { value: "1995" } });
});
expect(input.value).toBe("1995");
await act(async () => {
fireEvent.click(getByTestId("unlockbutton"));
});
expect(getByTestId("status")).toHaveTextContent("Unlocked!");
});
the component I'm trying to test
import React, { useState, useEffect } from "react";
import Keypad from "./components/Keypad";
import "./App.css";
import "./css/Result.css";
function App() {
//correctCombination: 1995
const [result, setResult] = useState("");
const [locked, setLocked] = useState("Locked");
const [tries, setTries] = useState(0);
const [hide, setHide] = useState(true);
//Along with the maxLength property on the input,
// this is also needed for the keypad
useEffect(() => {
(function() {
if (result >= 4) {
setResult(result.slice(0, 4));
}
})();
}, [result]);
const onClick = button => {
switch (button) {
case "unlock":
checkCode();
break;
case "clear":
clear();
break;
case "backspace":
backspace();
break;
default:
setResult(result + button);
break;
}
};
const checkCode = () => {
if (result === "1995") {
setLocked("Unlocked!");
setTries(0);
} else if (tries === 3) {
setHide(false);
setLocked("Too many incorrect attempts!");
setTimeout(() => {
setHide(true);
}, 3000);
} else {
setLocked("Incorrect code!");
setTries(tries + 1);
}
};
const clear = () => {
setResult("");
};
const backspace = () => {
setResult(result.slice(0, -1));
};
const handleChange = event => {
setResult(event.target.value);
};
return (
<div className="App">
<div className="pin-body">
<h1>Pin Pad</h1>
<div className="status">
<h2 data-testid="status">{locked}</h2>
</div>
<div className="result">
<input
maxLength={4}
type="phone"
aria-label="input-code"
data-testid="inputcode"
placeholder="Enter code"
onChange={handleChange}
value={result}
/>
</div>
{hide ? <Keypad onClick={onClick} /> : false}
</div>
</div>
);
}
export default App;
I'm using react-testing-library and I have a custom render:
const customRender = (node, ...options) => {
return render(
<ThemeProvider theme={Theme}>
<MemoryRouter>{node}</MemoryRouter>
</ThemeProvider>,
...options
);
};
I can use it successfully for a render in my test but not for a rerender:
test("shows content once data has loaded", () => {
const { queryByTestId, rerender, debug } = render(
<ConnectAccount
currentUser={{
loading: true
}}
/>
);
expect(queryByTestId("content")).toBeNull();
rerender(
<ConnectAccount
currentUser={{
user: {
name: "Test User"
}
}}
/>
);
debug();
});
I get an error TypeError: Cannot read property 'black' of undefined for the rerender. Is there any way for the rerender to use the custom render so I don't have to wrap every rerender in the ThemeProvider?
You need to redifine the rerender method. This should work:
const customRender = (node, options) => {
const rendered = render(
<ThemeProvider theme={Theme}>
<MemoryRouter>{node}</MemoryRouter>
</ThemeProvider>,
options);
return {
...rendered,
rerender: (ui, options) =>
customRender(ui, {container: rendered.container, ...options}),
}
};
Typescript version:
function AppProvider({ children }: { children: React.ReactNode }) {
return (
<OtherProvider>
<ThemeProvider theme={Theme}>
<MemoryRouter>{children}</MemoryRouter>
</ThemeProvider>,
</OtherProvider>
);
}
function renderInProvider(
ui: ReactElement,
options?: Omit<RenderOptions, "wrapper">
) {
return render(<AppProvider>{ui}</AppProvider>, options);
}