I'm having trouble testing a page that has Context and useEffect using Jest and Testing-library, can you help me?
REPO: https://github.com/jefferson1104/padawan
My Context: src/context/personContext.tsx
import { createContext, ReactNode, useState } from 'react'
import { useRouter } from 'next/router'
import { api } from '../services/api'
type PersonData = {
name?: string
avatar?: string
}
type PersonProviderProps = {
children: ReactNode
}
type PersonContextData = {
person: PersonData
loading: boolean
handlePerson: () => void
}
export const PersonContext = createContext({} as PersonContextData)
export function PersonProvider({ children }: PersonProviderProps) {
const [person, setPerson] = useState<PersonData>({})
const [loading, setLoading] = useState(false)
const router = useRouter()
function checkAvatar(name: string): string {
return name === 'Darth Vader'
? '/img/darth-vader.png'
: '/img/luke-skywalker.png'
}
async function handlePerson() {
setLoading(true)
const promise1 = api.get('/1')
const promise2 = api.get('/4')
Promise.race([promise1, promise2]).then(function (values) {
const data = {
name: values.data.name,
avatar: checkAvatar(values.data.name)
}
setPerson(data)
setLoading(false)
router.push('/battlefield')
})
}
return (
<PersonContext.Provider value={{ person, handlePerson, loading }}>
{children}
</PersonContext.Provider>
)
}
My Page: src/pages/battlefield.tsx
import { useContext, useEffect } from 'react'
import { useRouter } from 'next/router'
import { PersonContext } from '../context/personContext'
import Person from '../components/Person'
const Battlefield = () => {
const { person } = useContext(PersonContext)
const router = useRouter()
useEffect(() => {
if (!person.name) {
router.push('/')
}
})
return <Person />
}
export default Battlefield
My Test: src/tests/pages/Battlefield.spec.tsx
import { render, screen } from '#testing-library/react'
import { PersonContext } from '../../context/personContext'
import Battlefield from '../../pages'
jest.mock('../../components/Person', () => {
return {
__esModule: true,
default: function mock() {
return <div data-test-id="person" />
}
}
})
describe('Battlefield page', () => {
it('renders correctly', () => {
const mockPerson = { name: 'Darth Vader', avatar: 'darth-vader.png' }
const mockHandlePerson = jest.fn()
const mockLoading = false
render(
<PersonContext.Provider
value={{
person: mockPerson,
handlePerson: mockHandlePerson,
loading: mockLoading
}}
>
<Battlefield />
</PersonContext.Provider>
)
expect(screen.getByTestId('person')).toBeInTheDocument()
})
})
PRINSCREEN ERROR
enter image description here
I found a solution:
The error was happening because the path where I call the Battlefield page didn't have the absolute path.
Related
I m getting error in TwitterProvider how to fix that
import '../styles/globals.css'
import type { AppProps } from 'next/app'
import { TwitterProvider } from '../context/TwitterContext'
import '../lib/hexStyles.css'
import React from 'react'
function MyApp({ Component, pageProps }: AppProps) {
return (
<TwitterProvider>
<Component {...pageProps} />
</TwitterProvider>
)
}
export default MyApp
error image
TwitterContext Code
import { createContext, useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { client } from '../lib/client'
export const TwitterContext = createContext()
export const TwitterProvider = ({ children }) => {
const [appStatus, setAppStatus] = useState()
const [currentAccount,setCurrentAccount] = useState('')
useEffect(() => {
checkIfWalletIsConnected()
}, [])
const checkIfWalletIsConnected = async () => {
if (!window.ethereum) return
try {
const addressArray = await window.ethereum.request({
method: 'eth_accounts',
})
if (addressArray.length > 0) {
setAppStatus('connected')
setCurrentAccount(addressArray[0])
} else {
router.push('/')
setAppStatus('notConnected')
}
}catch (error) {
console.log(error)
}
}
const connectToWallet = async () => {
if(!window.ethereum) return setAppStatus('noMetaMask')
try {
setAppStatus('loading')
const adressArray = await window.ethereum.request({
method: 'eth_requestAccounts',
})
if (addressArray.length > 0) {
setCurrentAccount(addressArray[0])
} else {
router.push('/')
setAppStatus('notConnected')
} catch (error) {
setAppStatus('error')
}
}
return (
<TwitterContext.Provider value={{appStatus, currentAccount,
connectToWallet }}
>
{children}
</TwitterContext.Provider>
)
}
Example code can be found below...
(Parent) App.tsx
import React, { useRef } from 'react';
import { Switch } from "react-router-dom";
import axios, { AxiosResponse } from 'axios';
import { AdminRoute } from './auth/protectedRoutes';
import Home from './views/Home';
export interface Data {
id: number;
name: string;
}
function App(): JSX.Element {
// variables
const searchDebouncerRef = useRef(false);
const [data, setData] = useRef<Array<Data>>([]);
// functions
async function updateData(searchString?: string | null) {
try {
const response: AxiosResponse<Array<Data>> = searchString
? await axios.get(`https://example.com/Api/Data$filter=contains(name, ${searchString})`)
: await axios.get('https://example.com/Api/Data');
if (searchDebouncerRef.current) {
return;
}
setData(response.data);
catch (e) {
console.log(e);
}
}
async function initData() {
try {
await updateData();
catch (e) {
console.log(e);
}
}
// setup
useEffect(() => {
initData();
}, []);
// render
return (
<>
<Switch>
<AdminRoute path="/">
<Home ref={searchDebouncerRef} updateData={updateData} data={data} />
</AdminRoute>
</Switch>
</>
)
}
export App;
(Child) Home.tsx
import React, { forwardRef } from 'react';
import { Data as DataRow } from '../App';
import Search from '../components/Search';
interface PROPS {
updateData: (searchString?: string | null) => void;
data: Array<DataRow>;
}
const Home: React.FC<any> = forwardRef(
({ updateData, data }: PROPS, ref) => {
return (
<div>
<Search isSearchDebouncingRef={ref} updateData={updateData} />
{data.map((row: DataRow) => ({
<p key={row.id}>{row.name}</p>
}))}
</div>
);
}
);
(Grandchild) Search.tsx
import React, { ChangeEvent, useCallback, useState } from 'react';
import { debounce } from 'lodash';
interface PROPS {
updateData: (searchString?: string | null) => void;
isSearchDebouncingRef: ???? // <-----------------------------------------------------
}
function Search({ updateData, isSearchDebouncingRef }: PROPS): JSX.Element {
// variables
const [searchText, setSearchText] = useState('');
const [searchDebouncerHasCompleted, setSearchDebouncerHasCompleted] = useState(false);
const searchDebouncer = useCallback(
debounce(() => {
setSearchDebouncerHasCompleted(true);
isSearchDebouncingRef.current = false;
}, 3000),
[]
);
// functions
function handleSearch(event: ChangeEvent<HTMLInputElement>) {
setSearchText(event.target.value);
isSearchDebouncingRef.current = true;
searchDebouncer();
}
// setup
useEffect(() => {
if (searchDebouncerHasCompleted) {
setSearchDebouncerHasCompleted(false);
updateData(searchText || null);
}
}, [searchDebouncerHasCompleted]);
// render
return <input type="text" value={searchText} onChange={(e) => handleSearch(e)} />; // ToDo: add icons(search, spinner, clear)
}
The grandchild file is where I am having trouble figuring out how to identify the type for the ref(see interface PROPS). Also the child file I would like to replace any, with the proper type if possible.
How do I test recoil using react jest?
I expect below test is going to be success but it gives me fail.
Any way to render the status of isLogin: false using jest?
// src/state/user.ts
import { atom } from "recoil";
export type UserType = {
isLogin: boolean;
};
const userState = atom<UserType>({
key: "user",
default: {
isLogin: true,
},
});
export default userState;
// src/pages/user/User.tsx
import { useNavigate, useParams } from "react-router-dom";
import { useRecoilValue } from "recoil";
import userState from "../../state/user";
export default function User() {
const navigate = useNavigate();
const { id } = useParams();
const { isLogin } = useRecoilValue(userState);
if (!isLogin) {
return <div>Login 후 이용 가능합니다.</div>;
}
return (
<div>
{id}
<button
type="button"
onClick={() => {
navigate("/");
}}
>
Go to Home
</button>
</div>
);
}
// src/pages/user/User.test.tsx
import { MemoryRouter } from "react-router-dom";
import { render, screen, renderHook, act } from "#testing-library/react";
import { RecoilRoot, useSetRecoilState } from "recoil";
import User from "./User";
import type { UserType } from "../../state/user";
import userState from "../../state/user";
const userStateMock = (user: UserType) => {
const { result } = renderHook(() => useSetRecoilState(userState), {
wrapper: RecoilRoot,
});
act(() => {
result.current(user);
});
return result;
};
describe("<User />", () => {
const renderUserComponent = () =>
render(
<RecoilRoot>
<MemoryRouter>
<User />
</MemoryRouter>
</RecoilRoot>
);
describe("When user hasn't logged in", () => {
it("Should render warning message", () => {
userStateMock({
isLogin: false,
});
renderUserComponent();
expect(screen.getByText(/Login 후 이용 가능합니다./)).toBeDefined();
});
});
});
Result of the test
I have this problem, can anyone help me?
TypeError: customers.map is not a function.
I've always used it that way and I've never had any problems.
Its about data integration.
Basically is that, please anyone can help me?
import React, { useState, useEffect } from "react";
import { List, Card } from "antd";
import { data } from "../../../mocks/customers";
import { DeleteCustomerButton } from "#components/atoms/DeleteCustomerButton";
import { CustomersEditButton } from "#components/atoms/CustomersEditButton";
import { useContext } from "../../../contexts/context";
const { Meta } = Card;
const CustomersCardList: React.FC = () => {
const customers: any = useContext();
return (
<div>
{customers.map((customer, key) => { })}</div>)
}
//context.tsx
import * as React from 'react';
import axios from 'axios';
export const AccountContext = React.createContext({});
export const useContext = () => React.useContext(AccountContext);
interface AccounterContextProviderProps {
value: any
};
export const AccounterContextProvider: React.FC<AccounterContextProviderProps> = ({ children, value }) => {
const [customers, setCustomers] = React.useState<any>([]);
React.useEffect(() => {
const getCustomers = async () => {
const result = await axios.get("http://localhost:3333/customers");
setCustomers(result.data);
}
getCustomers();
}, []);
console.log(customers);
return (
<AccountContext.Provider value={{ ...value, customers }}>
{children}
</AccountContext.Provider>
)
};
Any can be anything not only array, so it will not have a map method. Use const customers:any[] = useContext() instead
I'm trying to use the useSnack hook from notistack library but I keep getting this error
TypeError: Cannot destructure property 'enqueueSnackbar' of 'Object(...)(...)' as it is undefined.
Here is the code:
import React, { useContext, useEffect } from "react";
import AlertContext from "../context/alert/alertContext";
import { SnackbarProvider, useSnackbar } from "notistack";
const Alerts = (props) => {
const alertContext = useContext(AlertContext);
// This line below is where the error seems to be
const { enqueueSnackbar } = useSnackbar();
useEffect(() => {
alertContext.msg !== "" &&
enqueueSnackbar(alertContext.msg, {
variant: alertContext.type,
});
}, [alertContext]);
return <SnackbarProvider maxSnack={4}>{props.children}</SnackbarProvider>;
};
export default Alerts;
useSnackbar hook accessible anywhere down the tree from SnackbarProvider.
So you cannot use it in the same component as SnackbarProvier.
import AlertContext from "../context/alert/alertContext";
import { SnackbarProvider } from "notistack";
const Alerts = (props) => {
const alertContext = useContext(AlertContext);
const providerRef = React.useRef();
useEffect(() => {
alertContext.msg !== "" &&
providerRef.current.enqueueSnackbar(alertContext.msg, {
variant: alertContext.type,
});
}, [alertContext]);
return <SnackbarProvider ref={providerRef} maxSnack={4}>
{props.children}
</SnackbarProvider>;
};
export default Alerts;
Wrap you index file with SnapBar provider:
index.js
import { SnackbarProvider } from "notistack";
const Index = () => (
<SnackbarProvider maxSnack={1} preventDuplicate>
index
</SnackbarProvider>
)
export default Index
jsx file
import { useSnackbar } from "notistack";
const Logs = () => {
const { enqueueSnackbar } = useSnackbar();
const handler = () => {
enqueueSnackbar(`Successful.`, { variant: "success" });
};
return <span onClick={handler}>"Logs loading"</span>;
};
export default Logs;