I am pretty new to using jest and Im trying to test a component that makes a state change which acts upon my global state (using Zustand). Basically im clicking a button and its adding an item to my state.traits. Here is my component code:
import { Flex, useToast } from '#chakra-ui/react'
import { FC } from 'react'
import { useProfileStore } from 'stores/profileStore'
interface DataTrait {
name: string,
id: string
}
type Props = {
trait: DataTrait
}
export const ChipItem: FC<Props> = ({ trait }) => {
const { traits, setTraits } = useProfileStore()
const toast = useToast()
const traitNames = traits.map((trait) => trait.name)
const emptyTraits = traits.filter((trait) => trait.name === "")
const handleClick = (trait: DataTrait) => {
if (!traitNames.includes(trait.name) && emptyTraits.length !== 0) {
let currentItem = traits.filter(trait => trait.name === "")[0]
let items = [...traits]
let item = {position: currentItem.position, id: trait.id, name: trait.name}
items[items.indexOf(currentItem)] = item
setTraits(items)
} else if (emptyTraits.length === 0){
toast({
title: 'Error',
status: 'error',
description: 'Only 5 traits can be selected',
isClosable: true,
duration: 5000
})
} else {
toast({
title: 'Error',
status: 'error',
description: 'Please select unique traits',
isClosable: true,
duration: 5000
})
}
}
return (
traitNames.includes(trait.name) ? (
<Flex mx={4} p={2} cursor="pointer" borderRadius="20px" backgroundColor="green" borderWidth="1px" borderColor="white" textColor="white" onClick={() => handleClick(trait)}>{trait.name}</Flex>
) : (
<Flex mx={4} p={2} cursor="pointer" borderRadius="20px" borderWidth="1px" borderColor="grey" onClick={() => handleClick(trait)}>{trait.name}</Flex>
)
)
}
here is my store code:
import create from 'zustand'
export interface Trait {
position: string,
name: string,
id: string,
}
export type Traits = Trait[]
const initialTraits = [
{position: "0", name: "", id: ""},
{position: "1", name: "", id: ""},
{position: "2", name: "", id: ""},
{position: "3", name: "", id: ""},
{position: "4", name: "", id: ""},
]
export type ProfileStore = {
traits: Traits;
setTraits: (traits: Traits) => void;
clearTraits: () => void;
}
export const useProfileStore = create<ProfileStore>((set) => ({
traits: initialTraits,
setTraits: (traits) => set({ traits }),
clearTraits: () => set({ traits: initialTraits })
}))
and here is my test code:
import React from 'react';
import { ChipItem } from "../../ChipList/ChipItem";
import { act, render, renderHook } from "#testing-library/react";
import { useProfileStore } from "../../../stores/profileStore";
const stubbedTrait = {
name: "Doing Work",
id: "efepofkwpeok"
}
it("displays the trait chip", () => {
const { queryByText } = render(<ChipItem trait={stubbedTrait} />);
expect(queryByText("Doing Work")).toBeTruthy();
})
it("sets the chip information in the store", () => {
act(() => {
const { traits } = renderHook(() => useProfileStore())
const { getByText } = render(<ChipItem trait={stubbedTrait}/>);
getByText(stubbedTrait.name).click()
expect(traits.includes(stubbedTrait)).toBeTruthy()
})
})
whats happening, is that it keeps telling me that renderHook is not a function and traits always comes back undefined. any help would be greatly appreciated!
Currently you must install and import React Testing Hooks separately
The best way to unit test Zustand state changes inside and specific component is not by using Zustand but by mocking the store hook with Jest.
You should create a test case for the Zustand Store using React Hook Testing library and once you verify the hook behaves as expected, then you mock the store with manual traits and setTraits changes.
Once you have the unit tests then you should test the behaviour of the real hook and components together with integration tests.
Related
I have a component like this:
export const MyComponent = props => {
return (
{
props.options.map(option =>
<>
<div>
<input type="radio" id={option.id} name="group" value={option.id} />
<label htmlFor={option.id}>{option.label}</label>
</div>
<span>Some other text</span>
</>
)
}
)
}
And in my test, I want to make sure that both that all the radio buttons are rendered with the right label text and the extra text in the span are present.
import { render, screen, within } from '#testing-library/react'
describe('MyComponent', () => {
const props = {
options: [
{ id: 1, label: 'Apple' },
{ id: 2, label: 'Banana' },
{ id: 3, label: 'Cherry' },
]
}
it('Renders the component', () => {
render(<MyComponent {...props} />)
const options = screen.queryAllByRole('radio')
expect(options).toBeArrayOfSize(3)
options.forEach((option, index) => {
const { getByText } = within(option)
expect(getByText(props.options[index])).toBeInTheDocument() // Assertion fails
expect(getByText("Some other text")).toBeInTheDocument() // Also fails
})
})
})
However, I'm getting errors on the two expect assertions.
You can try the following:
import { render, screen } from "#testing-library/react"
import { MyComponent } from "./MyComponent"
describe("MyComponent", () => {
const props = {
options: [
{ id: 1, label: "Apple" },
{ id: 2, label: "Banana" },
{ id: 3, label: "Cherry" },
],
}
it("Renders the component", () => {
render(<MyComponent {...props} />)
const options = screen.queryAllByRole("radio")
expect(options).toHaveLength(3)
props.options.forEach((option) => {
const label = screen.getByLabelText(option.label)
const radioBtn = screen.getByRole("radio", { name: option.label })
// Need to use getAllByText query since the string "Some other text" is repeated... getByText will throw because of multiple matches
const [someOtherText] = screen.getAllByText("Some other text")
expect(label).toBeInTheDocument()
expect(radioBtn).toBeInTheDocument()
expect(someOtherText).toHaveTextContent(someOtherText.textContent)
})
})
})
As the image shows, it only renders one array of objects.
How to reproduce:
Create a Blank Template and paste this code on ExploreContainer.tsx:
import {
IonCard,
IonCardHeader,
IonCardSubtitle,
IonCardTitle,
IonCardContent,
} from '#ionic/react'
import { useEffect, useState } from 'react'
import './ExploreContainer.css'
interface ContainerProps {}
interface TestArrayObject {
key: string
id: string
name: string
age: number
}
const ExploreContainer: React.FC<ContainerProps> = () => {
const [testArray, setTestArray] = useState<TestArrayObject[]>([])
const arraySample: TestArrayObject[] = [
{
key: '1',
id: '12345',
name: 'Jack',
age: 40,
},
{
key: '2',
id: '67890',
name: 'Black',
age: 30,
},
]
useEffect(() => {
arraySample.map((arr: TestArrayObject) => {
setTestArray([...testArray, arr])
})
}, [])
const listArray = testArray.map((arr) => {
return (
<IonCard>
<IonCardHeader>
<IonCardSubtitle>{arr.id}</IonCardSubtitle>
<IonCardTitle>{arr.name}</IonCardTitle>
</IonCardHeader>
<IonCardContent>
Keep close to Nature's heart... {arr.age}
</IonCardContent>
</IonCard>
)
})
return <>{ listArray }</>
}
export default ExploreContainer
I'm trying to figure out the solution, but happens that i`m more than 24hours trying to figure out and nothing. could someone help?
I am writing a unit test including multiple tests. for each of the test I need to use mockReturnValue to mock the returned value (I want the status property to be different in different test) of an imported api function.
My code:
import userEvent from '#testing-library/user-event'
import { renderWithWrappers, screen, waitFor } from '#/tests/testUtils'
import { getUserDetails, updateUser } from '#/controllers/Users'
import { LANG } from '#/constants'
import EditUserModal from '../EditUserModal'
const { EN } = LANG
jest.mock('#/controllers/Users')
describe('EditUserModal', () => {
afterEach(() => {
jest.clearAllMocks()
})
const props = {
id: 123,
show: true,
onHide: jest.fn(),
mutate: jest.fn(),
roles: [
{ value: 111, label: 'Low', allowPortalAccess: 0 },
{ value: 123, label: 'High', allowPortalAccess: 1 }
]
}
const data = {
first_name: 'Jean Gold',
last_name: 'SD',
lang: 'FR',
role_id: 111
}
const mockState = {
user: {
credentials: { id: 12345 }
}
}
it('renders modal header and close icon correctly', async () => {
getUserDetails.mockReturnValue({ ...data, status: 1 })
const { baseElement } = renderWithWrappers(<EditUserModal {...props} />)
expect(baseElement).toMatchSnapshot()
})
it('renders modal body and footer correctly when status is inactive', async () => {
getUserDetails.mockReturnValue({ ...data, status: 2 })
renderWithWrappers(<EditUserModal {...props} />)
})
As you can see I mocked the return value in two different it tests(the status value is different), but when I run the tests it seems for the second it test the returned status value is still 1 instead of 2 even though I set the status is 2 for the mockReturnValue function.
I am using react native normal workflow (not expo) and also my project is based on functional component type (no class component) so how to implement it. I have used react native gifted chat npm package .. my dilogflow api is working but the messages index are not updating so i am missing some messages
here is my code
import React, { useState, useCallback, useEffect } from 'react'
import { View } from 'react-native'
import { GiftedChat } from 'react-native-gifted-chat'
import { Dialogflow_V2 } from 'react-native-dialogflow';
import { dialogFlowConfig } from '../../config'
import styles from './styles'
export default function chatScreen(props) {
const dabbaBot = {
_id: 2,
name: 'React Native',
avatar: 'https://placeimg.com/140/140/any',
}
let welcomeMessage = {
_id: 0,
text: 'Hellooooo....How are you',
createdAt: new Date(),
user: dabbaBot
}
const [messages, setMessages] = useState([welcomeMessage]);
const [messagelength, setMessageLength] = useState(0);
useEffect(() => {
Dialogflow_V2.setConfiguration(
dialogFlowConfig.client_email,
dialogFlowConfig.private_key,
Dialogflow_V2.LANG_ENGLISH_US,
dialogFlowConfig.project_id
);
}, [])
function sendBotResponse(text) {
let messageLength = messages.length + 2
let msg = {
_id: messageLength + 1,
text,
createdAt: new Date(),
user: dabbaBot
};
console.log(msg._id)
setMessages(messages);
setMessages(previousMessages => GiftedChat.append(previousMessages, [msg]));
}
function handleGoogleResponse(result) {
let text = result.queryResult.fulfillmentMessages[0].text.text[0];
sendBotResponse(text);
}
const onSend = useCallback((messages = []) => {
let messageLength = messages.length + 1
let modifiedMessage = {
text: messages[0].text,
user: messages[0].user,
_id: messageLength + 1,
createdAt: new Date()
}
console.log(modifiedMessage._id)
// // messages = messages.push(modifiedMessage)
// // setMessages(messages)
// setMessages(previousMessages => GiftedChat.append(previousMessages, [modifiedMessage]));
setMessages(messages);
setMessages(previousMessages => GiftedChat.append(previousMessages, [modifiedMessage]));
let message = messages[0].text;
console.log(message)
Dialogflow_V2.requestQuery(
message,
result => handleGoogleResponse(result),
error => console.log(error)
);
}, [])
return (
<View style={styles.maincontainer}>
<GiftedChat
messages={messages}
onSend={messages => onSend(messages)}
user={{
_id: 1
}}
/>
</View>
)
}
I've got a fairly simple example of a component (Hello.js) that renders three components, each with a different id (Speaker.js). I have a clickFunction that I pass back from the Speaker.js. I would think that using React.memo and React.useCallback would stop all three from re-rendering when only one changes, but sadly, you can see from the console.log in Speaker.js, clicking any of the three buttons causes all three to render.
Here is the problem example on stackblitz:
https://stackblitz.com/edit/react-dmclqm
Hello.js
import React, { useCallback, useState } from "react";
import Speaker from "./Speaker";
export default () => {
const speakersArray = [
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true },
];
const [speakers, setSpeakers] = useState(speakersArray);
const clickFunction = useCallback((speakerIdClicked) => {
var speakersArrayUpdated = speakers.map((rec) => {
if (rec.id === speakerIdClicked) {
rec.favorite = !rec.favorite;
}
return rec;
});
setSpeakers(speakersArrayUpdated);
},[speakers]);
return (
<div>
{speakers.map((rec) => {
return (
<Speaker
speaker={rec}
key={rec.id}
clickFunction={clickFunction}
></Speaker>
);
})}
</div>
);
};
Speaker.js
import React from "react";
export default React.memo(({ speaker, clickFunction }) => {
console.log(`speaker ${speaker.id} ${speaker.name} ${speaker.favorite}`);
return (
<button
onClick={() => {
clickFunction(speaker.id);
}}
>
{speaker.name} {speaker.id} {speaker.favorite === true ? "true" : "false"}
</button>
);
});
because when you fire clickFunction it update speakers wich cause the recreating of this functions, to solve this you need to remove speakers from clickFunction dependencies and accessing it from setState callback.
here the solution :
import React, { useCallback, useState,useEffect } from "react";
import Speaker from "./Speaker";
export default () => {
const [speakers, setSpeakers] = useState([
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true },
]);
const clickFunction = useCallback((speakerIdClicked) => {
setSpeakers(currentState=>currentState.map((rec) => {
if (rec.id === speakerIdClicked) {
rec.favorite = !rec.favorite;
return {...rec};
}
return rec
}));
},[]);
useEffect(()=>{
console.log("render")
})
return (
<div>
{speakers.map((rec) => {
return (
<Speaker
speaker={rec}
key={rec.id}
clickFunction={clickFunction}
></Speaker>
);
})}
</div>
);
};
and for speaker component:
import React from "react";
export default React.memo(({ speaker, clickFunction }) => {
return (
<button
onClick={() => {
clickFunction(speaker.id);
}}
>
{speaker.name} {speaker.id} {speaker.favorite === true ? "true" : "false"}
</button>
);
});
Upon further reflection, I think my answer may not be entirely correct: without the [speakers] dependency this won't work as intended.
Two things:
The [speakers] dependency passed to useCallback causes the function to get recreated every time speakers changes, and because the callback itself calls setSpeakers, it will get recreated on every render.
If you fix #1, the Speaker components won't re-render at all, because they're receiving the same speaker prop. The fact that speaker.favorite has changed doesn't trigger a re-render because speaker is still the same object. To fix this, have your click function return a copy of rec with favorite flipped instead of just toggling it in the existing object:
import React, { useCallback, useState } from "react";
import Speaker from "./Speaker";
export default () => {
const speakersArray = [
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true },
];
const [speakers, setSpeakers] = useState(speakersArray);
const clickFunction = useCallback((speakerIdClicked) => {
var speakersArrayUpdated = speakers.map((rec) => {
if (rec.id === speakerIdClicked) {
return { ...rec, favorite: !rec.favorite }; // <= return a copy of rec
}
return rec;
});
setSpeakers(speakersArrayUpdated);
}, []); // <= remove speakers dependency
return (
<div>
{speakers.map((rec) => {
return (
<Speaker
speaker={rec}
key={rec.id}
clickFunction={clickFunction}
></Speaker>
);
})}
</div>
);
};