queryByTestId is null after waitFor in unit test - javascript

In my unit test, I want to click an element in my wrapper component that affects the child component. The queryByTestId works before the await waitFor call, but the 2nd queryByTestID returns "null". I'm trying to test what happens in the child component when the language changes.
In my test I have the following:
const { queryByTestId, container } = render(
<TestIntlWrapper>
<MyComponent />
</TestIntlWrapper>
);
expect(queryByTestId("test-intl-wrapper")).toBeInTheDocument;
await waitFor(() => expect(mockedAxios.get).toBeCalledTimes(expectedNumOfAPICalls));
expect(mockedAxios.get).toBeCalledWith(expectedURL1);
expect(mockedAxios.get.mock.calls[1][0]).toBe(expectedURL2);
expect(mockedAxios.get.mock.calls[thirdCall][0]).toBe(expectedURL3);
expect(queryByTestId("test-intl-wrapper")).toBeInTheDocument; //queryByTestId returns null here
TestIntlWrapper.tsx
import React, { useEffect, useState } from "react";
import { IntlProvider } from "react-intl";
interface TestIntlWrapperProps {
children: JSX.Element
}
export default function TestIntlWrapper({children}: TestIntlWrapperProps) {
const languages = ["en", "es", "fr"]
const [currentLanguage, setCurrentLanguage] = useState(languages[0]);
const [clickCount, setClickCount] = useState(0);
const setClick = () => {
setClickCount(clickCount + 1)
}
useEffect(() => {
setCurrentLanguage(languages[clickCount % languages.length]);
},[clickCount] )
return (
<div data-testid="test-intl-wrapper" onClick={setClick}>
<IntlProvider locale={currentLanguage}>
{children}
</IntlProvider>
</div>
)
}
Any help is appreciated

The issue was the application was throwing an uncaught error in the waitFor which is why it was running an empty div and the data-testid was disappearing.

Related

Check whether React.Suspense is ready

I'm using the Intersection Observer API in React to add some animations. I am adding as Intersection Entries some elements.
The problem is that I have the app in multiple languages, and due to the implementation that the tool I am using to translate has, I need to wrap all my components into React.Suspense to wait for languages to load.
When useEffect queries for the elements, they aren't still in the DOM, and therefore they are not assigned as entries.
This is my custom hook:
hooks/useObserver.js
import { useState } from "react";
import { useEffect, useRef } from "react";
export function useObserver(config = {}) {
const [elements, setElements] = useState([]);
const [entries, setEntries] = useState([]);
const observer = useRef(
new IntersectionObserver(observedEntries => {
setEntries(observedEntries);
}, config)
);
useEffect(() => {
const { current: currentObserver } = observer;
currentObserver.disconnect();
if (elements.length > 0) {
elements.forEach(el => currentObserver.observe(el));
}
return () => {
if (currentObserver) {
currentObserver.disconnect();
}
};
}, [elements]);
return { observer: observer.current, setElements, entries };
}
and this is my main component:
App.jsx
import Header from "./components/Header";
import Hero from "./components/Hero";
import Footer from "./components/Footer";
import { Loader } from "./components/shared/Loader";
import { useObserver } from "./hooks/useObserver";
import { useEffect, Suspense } from "react";
function App() {
const { entries, setElements } = useObserver({});
useEffect(() => {
const sections = document.querySelectorAll("section.animated-section");
setElements(sections);
};
}, [setElements]);
useEffect(() => {
entries.forEach(entry => {
entry.target.classList.toggle("section-visible", entry.isIntersecting);
});
}, [entries]);
return (
<Suspense fallback={<Loader />}>
<Header />
<Hero />
<Footer />
</Suspense>
);
}
export default App;
I tried to set a timeout to wait some seconds and then add the elements as entries, and it works correctly:
useEffect(() => {
const observeElements = () => {
const sections = document.querySelectorAll("section.animated-section");
setElements(sections);
};
const observeElementsTimeout = setTimeout(observeElements, 3000);
return () => clearTimeout(observeElementsTimeout)
}, [setElements]);
I want to know if:
There is a way to know when React.Suspense is ready
There is a better approach to solve my problem
Thanks in advance!!

The object passed as the value prop to the Context provider changes every render. How to Fix it with useMemo?

I have the following problem,
The object passed as the value prop to the Context provider (at line 20) changes every render. To fix this consider wrapping it in a useMemo hook.
I don't know hot to use useMemo in this case. So how do I fix it?
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import Context from './Context';
function Provider({ children }) {
const [data, setData] = useState([]);
useEffect(() => {
const getDataAPI = async () => {
const response = await fetch('https://swapi-trybe.herokuapp.com/api/planets/');
const dataAPI = await response.json();
const alteredData = dataAPI.results.map(({ residents, ...params }) => params);
const result = [...alteredData];
setData(result.sort((a, b) => a.name.localeCompare(b.name)));
};
getDataAPI();
}, []);
return (
<Context.Provider value={ { data } }>
{ data[0] && children }
</Context.Provider>
);
}
Provider.propTypes = {
children: PropTypes.node.isRequired,
};
export default Provider;
Look at what you are passing:
value={ { data } }
It is an object {data : data}. This is defined everytime.
You better pass value={data} and change your useContext hooks accordingly in children

Importing React Autosuggest as Functional Component from Another JSX File

I'm currently making a simple web frontend with react using react-autosuggest to search a specified user from a list. I want to try and use the Autosuggest to give suggestion when the user's type in the query in the search field; the suggestion will be based on username of github profiles taken from github user API.
What I want to do is to separate the AutoSuggest.jsx and then import it into Main.jsx then render the Main.jsx in App.js, however it keeps giving me 'TypeError: _ref2 is undefined' and always refer to my onChange function of AutoSuggest.jsx as the problem.
Below is my App.js code:
import './App.css';
import 'bootstrap/dist/css/bootstrap.min.css';
import Header from './views/header/Header';
import Main from './views/main/Main';
import Footer from './views/footer/Footer';
const App = () => {
return (
<>
<Header/>
<Main/> <- the autosuggest is imported in here
<Footer/>
</>
);
}
export default App;
Below is my Main.jsx code:
import React, { useState } from 'react';
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import axios from 'axios';
import { useEffect } from 'react';
import AutoSuggest from '../../components/AutoSuggest';
const Main = () => {
const [userList, setUserList] = useState([]);
useEffect(() => {
axios.get('https://api.github.com/users?per_page=100')
.then((res) => setUserList(res.data))
.catch((err) => console.log(err));
}, [])
return (
<Container>
<br/>
<Row>
<AutoSuggest userList={userList} placeHolderText={'wow'} />
</Row>
</Container>
);
}
export default Main;
Below is my AutoSuggest.jsx code:
import React, { useState } from "react";
import Autosuggest from 'react-autosuggest';
function escapeRegexCharacters(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function getSuggestions(value, userList) {
const escapedValue = escapeRegexCharacters(value.trim());
if (escapedValue === '') {
return [];
}
const regex = new RegExp('^' + escapedValue, 'i');
return userList.filter(user => regex.test(user.login));
}
function getSuggestionValue(suggestion) {
return suggestion.name;
}
function renderSuggestion(suggestion) {
return (
<span>{suggestion.name}</span>
);
}
const AutoSuggest = ({userList, placeHolderText}) => {
const [value, setValue] = useState('');
const [suggestions, setSuggestions] = useState([]);
const onChange = (event, { newValue, method }) => { <- error from console always refer here, I'm not quite sure how to handle it..
setValue(newValue);
};
const onSuggestionsFetchRequested = ({ value }) => {
setValue(getSuggestions(value, userList))
};
const onSuggestionsClearRequested = () => {
setSuggestions([]);
};
const inputProps = {
placeholder: placeHolderText,
value,
onChange: () => onChange()
};
return (
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={() => onSuggestionsFetchRequested()}
onSuggestionsClearRequested={() => onSuggestionsClearRequested()}
getSuggestionValue={() => getSuggestionValue()}
renderSuggestion={() => renderSuggestion()}
inputProps={inputProps} />
);
}
export default AutoSuggest;
The error on browser (Firefox) console:
I have no idea what does the error mean or how it happened and therefore unable to do any workaround.. I also want to ask if what I do here is already considered a good practice or not and maybe some inputs on what I can improve as well to make my code cleaner and web faster. Any input is highly appreciated, thank you in advance!
you have to write it like this... do not use the arrow function in inputProps
onChange: onChange

ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `object`. Error

I am getting the following error
ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was object.
This error is encountered while I am trying to mock certain modules
My Main.test.js file
import React from 'react';
import { shallow } from 'enzyme';
jest.mock('../Elements', () =>
jest.fn().mockReturnValue({
SortIndicator: (props) => <div {...props}></div>,
ExchangeRate: (props) => <div {...props}></div>,
})
);
jest.mock('../../components/Default_Import', (props) => (
<div {...props}></div>
));
it('IT246 SearchListView is rendering without any error', async () => {
const MainContainer = await import('./MainContainer');
const wrapper = shallow(<MainContainer />)
})
My Main.js file
import { SortIndicator, ExchangeRate } from '../Elements';
export default class SearchListView extends React.Component {
render() {
return (
<>
<SortIndicator></SortIndicator>
<ExchangeRate></ExchangeRate>
</>
)
}
}
From the docs Importing defaults
When importing a default export with dynamic imports, it works a bit differently. You need to destructure and rename the "default" key from the returned object.
So, you need to dynamic import the ./MainContainer module like this:
describe('68279075', () => {
it('should pass', async () => {
const MainContainer = (await import('./main')).default;
const wrapper = shallow(<MainContainer />);
console.log(wrapper.debug());
});
});
An complete working example:
Main.jsx:
import React from 'react';
import { SortIndicator, ExchangeRate } from './Elements';
export default class SearchListView extends React.Component {
render() {
return (
<>
<SortIndicator></SortIndicator>
<ExchangeRate></ExchangeRate>
</>
);
}
}
Elements.jsx:
import React from 'react';
export function ExchangeRate() {
return <div>ExchangeRate</div>;
}
export function SortIndicator() {
return <div>SortIndicator</div>;
}
Main.test.jsx:
import React from 'react';
import { mount } from 'enzyme';
jest.mock('./Elements', () => ({
SortIndicator: (props) => <div {...props}>mocked SortIndicator</div>,
ExchangeRate: (props) => <div {...props}>mocked ExchangeRate</div>,
}));
describe('68279075', () => {
it('should pass', async () => {
const MainContainer = (await import('./main')).default;
const wrapper = mount(<MainContainer />);
console.log(wrapper.debug());
});
});
Debug messages:
PASS examples/68279075/Main.test.jsx (12.861 s)
68279075
✓ should pass (49 ms)
console.log
<SearchListView>
<SortIndicator>
<div>
mocked SortIndicator
</div>
</SortIndicator>
<ExchangeRate>
<div>
mocked ExchangeRate
</div>
</ExchangeRate>
</SearchListView>
at examples/68279075/Main.test.jsx:13:13
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 14.51 s

Custom hook error: Hooks can only be called inside of the body of a function component

I am trying to develop a custom hook which seems to be pretty easy but I am getting an error
Uncaught Invariant Violation: 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:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
This is my hook:
import React, { useState, useEffect } from 'react';
const useInfiniteScroll = (isLastPage: boolean, fetchFn: any) => {
const [pageCount, setPageCount] = useState(0);
const triggerFetchEvents = (): void => {
let response;
setPageCount(() => {
if (!isLastPage) {
response = fetchFn(pageCount + 1, 5, 'latest');
}
return pageCount + 1;
});
return response;
};
useEffect(() => {
triggerFetchEvents();
}, []);
return pageCount;
};
export default useInfiniteScroll;
And the component here I am calling it:
import React, { FC } from 'react';
import { connect } from 'react-redux';
import { fetchEvents } from '../../shared/actions/eventActions';
import { AppState } from '../../shared/types/genericTypes';
import EventModel from '../../shared/models/Event.model';
import EventListPage from '../../components/events/EventListPage';
import useInfiniteScroll from '../../shared/services/triggerInfiniteScroll';
type Props = {
fetchEvents?: any;
isLastPage: boolean;
eventsList?: EventModel[];
};
const mapState: any = (state: AppState, props: Props): Props => ({
eventsList: state.eventReducers.eventsList,
isLastPage: state.eventReducers.isLastPage,
...props
});
const actionCreators = {
fetchEvents
};
export const EventsScene: FC<Props> = props => {
const { eventsList, fetchEvents, isLastPage } = props;
const useIn = () => useInfiniteScroll(isLastPage, fetchEvents);
useIn();
// const [pageCount, setPageCount] = useState(0);
// const triggerFetchEvents = (): void => {
// let response;
// setPageCount(() => {
// if (!isLastPage) {
// response = fetchEvents(pageCount + 1, 1, 'latest');
// }
// return pageCount + 1;
// });
// return response;
// };
// useEffect(() => {
// triggerFetchEvents();
// }, []);
if (!eventsList || !eventsList.length) return null;
return (
<EventListPage
eventsList={eventsList}
isLastPage={isLastPage}
triggerFetchEvents={useIn}
/>
);
};
export default connect(
mapState,
actionCreators
)(EventsScene);
I left the commented code there to show you that if I uncomment the code and remove useInfiniteScroll then it works properly.
What could I be missing?
UPDATE:
This is EventListPage component
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import EventModel from '../../shared/models/Event.model';
import { formatDate } from '../../shared/services/date';
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import Card from 'react-bootstrap/Card';
type Props = {
eventsList?: EventModel[];
isLastPage: boolean;
triggerFetchEvents: any;
};
export const EventListPage: React.FC<Props> = props => {
const { eventsList, triggerFetchEvents, isLastPage } = props;
const [isFetching, setIsFetching] = useState(false);
const fetchMoreEvents = (): Promise<void> =>
triggerFetchEvents().then(() => {
setIsFetching(false);
});
const handleScroll = (): void => {
if (
document.documentElement.offsetHeight -
(window.innerHeight + document.documentElement.scrollTop) >
1 ||
isFetching
) {
return;
}
return setIsFetching(true);
};
useEffect(() => {
if (isFetching) return;
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
useEffect(() => {
if (!isFetching) return;
if (!isLastPage) fetchMoreEvents();
}, [isFetching]);
if (!eventsList) return null;
return (
<Container className='article-list mt-5'>
///...
</Container>
);
};
export default EventListPage;
In EventsScene, change useInfiniteScroll to be invoked directly at the function body top-level (not sure why you are creating this indirection in the first place):
// before
const useIn = () => useInfiniteScroll(isLastPage, fetchEvents);
useIn();
// after
useInfiniteScroll(isLastPage, fetchEvents)
React expects Hook calls to only happen at the top-level as it relies on the order of Hooks to be always the same. If you wrap the Hook in a function, you can potentially invoke this function in many code locations disturbing the Hooks' order.
There is an internal list of “memory cells” associated with each component. They’re just JavaScript objects where we can put some data. When you call a Hook like useState(), it reads the current cell (or initializes it during the first render), and then moves the pointer to the next one. This is how multiple useState() calls each get independent local state. Link

Categories