I wonder why the test is failing when I use it with redux hooks:
The code is working finem but the tests are failing for some reason. I am unable to test if the component is being rendered or not.
Component:
import React, { useEffect, useState } from 'react';
import { fetchAllApis } from '../../../redux/actions/marketplace/marketplaceActions';
import { useDispatch, useSelector, connect } from 'react-redux';
import ApiCard from '../ApiCard/ApiCard';
import Spinner from '../../../components/Extras/Spinner/Spinner';
const ApiSection = ({ apiList, error, loading, categories }) => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchAllApis({ page, category: categories }));
}, [dispatch, categories]);
const renderApiCards = () => {
return apiList.map((each) => (
<ApiCard key={each.apiId} info={each} data-test="ApiCard" />
));
};
if (loading) {
return <Spinner data-test="Spinner" />;
}
if (error) {
return <h1 data-test="Error">Error while fetching</h1>;
}
return (
<div className="ApiSection" data-test="ApiSection">
<div className="ApiSection__cards">{renderApiCards()}</div>
</div>
);
};
const mapStateToProps = ({ marketplaceApiState }) => {
const { apiList, error, loading } = marketplaceApiState;
return {
error,
loading,
apiList: Object.values(apiList),
};
};
export default connect(mapStateToProps)(ApiSection);
Here is the test for the above component:
Test:
import React from 'react';
import { mount } from 'enzyme';
import ApiListSection from './ApiListSection';
import { findByTestAttr, createTestStore } from '../../../../testUtils';
import { Provider } from 'react-redux';
const setup = (props = {}) => {
let initialState = {
marketPlaceState: {
apiList: {
a: { apiId: 'a', name: 'name', description: 'desc', categories: 'cat'}
},
},
};
const store = createTestStore(initialState);
const wrapper = mount(
<Provider store={store}>
<ApiListSection {...props} />
</Provider>
);
return wrapper;
};
describe('ApiListSection Component', () => {
let component;
beforeEach(() => {
component = setup();
});
// assertions
it('Should render without failing', () => {
const apiSection = findByTestAttr(component, 'ApiSection');
expect(apiSection.length).toBe(1); // <===== FAILING HERE !!!!!
});
});
I would really appreciate the help, thanks in advance
Related
I have react project generated by vite, I get this error when I add eventListener to the DOM. I also use React context API. But I think there might be a problem with the StateProvider.jsx that contains the context API but I'm not sure.
The error says:
Cannot update a component (`StateProvider`) while rendering a different component (`DeltaY`). To locate the bad setState() call inside `DeltaY`, follow the stack trace as described in ...
Here is the snapshot of the error in the console:
Here is the code:
main.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
import { StateProvider } from './StateProvider.jsx';
import reducer, { initialState } from './reducer';
ReactDOM.createRoot(document.getElementById('root')).render(
<StateProvider initialState={initialState} reducer={reducer}>
<App />
</StateProvider>,
);
App.jsx
import './App.css';
import DeltaY from './DeltaY';
import { useStateValue } from './StateProvider';
function App() {
return (
<>
<DeltaY />
<Desc />
</>
);
}
const Desc = () => {
const [{ deltaY, scrollMode }, dispatch] = useStateValue();
return (
<>
<h1> deltaY: {deltaY} </h1>
<h1> scroll: {scrollMode} </h1>
</>
);
};
export default App;
DeltaY.jsx
import { useEffect, useState } from 'react';
import { useStateValue } from './StateProvider';
const DeltaY = () => {
// ------------------------------ context API ------------------------------- //
const [{ state }, dispatch] = useStateValue();
// ------------------------------ context API ------------------------------- //
const [scrollMode, setScrollMode] = useState(false);
const [deltaY, setDeltaY] = useState(0);
useEffect(() => {
function handleWheel(e) {
setDeltaY(e.deltaY);
setScrollMode(true);
}
window.addEventListener('wheel', handleWheel);
return () => window.removeEventListener('wheel', handleWheel);
}, []);
useEffect(() => {
setTimeout(() => {
setScrollMode(true);
}, 1000);
}, []);
// ------------------------------ dispatch ------------------------------- //
dispatch({
type: 'GET_DELTAY',
value: deltaY,
});
dispatch({
type: 'GET_SCROLLMODE',
value: scrollMode,
});
// ------------------------------ dispatch ------------------------------- //
return null;
};
export default DeltaY;
StateProvider.jsx
import React, { createContext, useContext, useReducer } from 'react';
// prepare the daya layer
export const StateContext = createContext();
// Wrap our app and provide the data layer
export const StateProvider = ({ reducer, initialState, children }) => {
return (
<>
<StateContext.Provider value={useReducer(reducer, initialState)}>
{children}
</StateContext.Provider>
</>
);
};
// PUll the information from the data layer
export const useStateValue = () => useContext(StateContext);
Any Idea how to fix it ? thankyou
Here is the solution:
instead of using it as a component, I use it as a custom Hook
Here is the code:
useDeltaY.jsx
import { useEffect, useState } from 'react';
const useDeltaY = () => {
const [scrollMode, setScrollMode] = useState(false);
const [deltaY, setDeltaY] = useState(0);
useEffect(() => {
function handleWheel(e) {
setDeltaY(e.deltaY);
setScrollMode(true);
}
window.addEventListener('wheel', handleWheel);
return () => window.removeEventListener('wheel', handleWheel);
}, []);
useEffect(() => {
setTimeout(() => {
setScrollMode(false);
}, 1000);
}, [scrollMode]);
return [deltaY, scrollMode];
};
export default useDeltaY;
App.jsx
import './App.css';
import useDeltaY from './useDeltaY';
function App() {
return (
<>
<Desc />
</>
);
}
const Desc = () => {
const [deltaY, scrollMode] = useDeltaY();
return (
<>
<h1> deltaY: {deltaY} </h1>
{scrollMode ? (
<h1> scrollMode: active </h1>
) : (
<h1> scrollMode: inActive </h1>
)}
</>
);
};
export default App;
I have a component that uses useLocation hook to get the path from the URL.
const { pathname } = useLocation();
useEffect(() => { }, [pathname]);
While I am trying to mock the location using ,
import React from 'react';
import ExampleComponent from './ExampleComponent';
import { fireEvent, render } from '#testing-library/react';
import { shallow } from 'enzyme';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: () => ({
pathname: 'https://URL/'
})
}));
describe('<ExampleComponent />', () => {
it('should render correctly', () => {
shallow(<ExampleComponent />);
});
});
I am getting this error while I run the test,
TypeError: Cannot read property 'location' of undefined
Try mocking the useLocation as jest.fn().mockImplementation
jest.mock('react-router', () => ({
...jest.requireActual("react-router") as {},
useLocation: jest.fn().mockImplementation(() => {
return { pathname: "/testroute" };
})
}));
Below is how I have done this in my tests. * Note I am using typescript
import routeData from 'react-router';
describe('Login Page UnitTests', () => {
const useLocation = jest.spyOn(routeData, 'useLocation');
beforeEach(() => {
useLocation.mockReturnValue({ search: 'testQueryParameters'} as any);
});
// Add unit tests
}
Ensure that you clear the mock to avoid issue with data in subsequent tests
The correct way to mock useLocation is below:
import React from 'react';
import ExampleComponent from './ExampleComponent';
import { fireEvent, render } from '#testing-library/react';
import { MemoryRouter} from 'react-router-dom';
import { shallow } from 'enzyme';
const renderComponent = () => {
return (
<MemoryRouter
initialEntries={["/one", "/two", { pathname: 'https://URL/' }]}
initialIndex={1}>
<ExampleComponent />
</MemoryRouter>
);
}
describe('<ExampleComponent />', () => {
it('should render correctly', () => {
shallow(renderComponent());
});
});
I am integrating redux with my react-native app. I have moved my state and action management to Container and integrated the container with component using 'connect'.
App.js
const AppNavigator = createSwitchNavigator({
SplashScreen: SplashScreen,
render() {
return(
<Provider store={store}>
<AppNavigator/>
</Provider>
)
}
});
const store = createStore(reducer);
export default createAppContainer(AppNavigator);
SignIn.js
import React from "react";
import {View} from "react-native";
import authenticateUser from "../../../services/api/authenticateUser";
const SignIn = (props) => {
const authenticate = async () => {
try {
return await authenticateUser.get('/abc', {
params: {
code,
}
});
}
catch (e) {
}
}
const validateUserCredentials = (isValid) => {
authenticate().then(response => {
const responseData = response.data;
props.updateEventRules(responseData);
});
}
}
return (
<View></View>
);
export default SignIn;
Sign-InContainer.js
import {eventRulesUpdated} from '../../../actions/actions';
import {connect} from 'react-redux';
import SignIn from './signin-screen';
const mapStateToProps = (state) => ({});
const mapDispatchToProps = dispatch => {
return {
updateEventRules: rules => {
dispatch(eventRulesUpdated(rules))
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SignIn);
When running the app I am getting an error that - props.updateEventRules() is not a function.
Can anyone please help me what am I doing wrong?
You should have the connect functions inside Signin.js like this
import React from "react";
import {View} from "react-native";
import authenticateUser from "../../../services/api/authenticateUser";
const SignIn = (props) => {
const authenticate = async () => {
try {
return await authenticateUser.get('/abc', {
params: {
code,
}
});
}
catch (e) {
}
}
const validateUserCredentials = (isValid) => {
authenticate().then(response => {
const responseData = response.data;
props.updateEventRules(responseData);
});
}
}
return (
<View></View>
);
const mapStateToProps = (state) => ({});
const mapDispatchToProps = dispatch => {
return {
updateEventRules: rules => {
dispatch(eventRulesUpdated(rules))
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SignIn);
Here is live example code https://codesandbox.io/embed/wonderful-moon-sc9o2
I want to implement sort of i18n (like react-i18next)
When I click "Change Language" button, I expect all my translations in different components are updated:
// Example.js
// t.ui.btn - is a path to string token
// { ui: { btn: 'Change Language' } }
<button>{t.ui.btn}</button>
The lang variable in my hook is changed all right, but the hook doesn't propagate changes to the app. Instead it renders initial lang value.
You can notice how lang receives initial value in console.
What is wrong with my hook? How to fix it?
index.js
import React from "react";
import ReactDOM from "react-dom";
import { Example } from "./Example";
import { Header } from "./Header";
import { useTranslation } from "./useTranslation";
import "./styles.css";
function App() {
const { t } = useTranslation();
return (
<div className="App">
<Header />
<h3>{t.app.title}</h3>
<div>
<Example />
</div>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Example.js
import React, { useState } from "react";
import { useTranslation } from "./useTranslation";
function Example() {
const [count, setCount] = useState({ count: 1 });
const { t } = useTranslation();
function onClick(ev) {
setCount(prevState => ({
count: ++prevState.count
}));
}
return (
<div>
<p>Clicks state: {JSON.stringify(count)}</p>
<button onClick={onClick}>{t.ui.btn}</button>
</div>
);
}
export { Example };
useTranslation.js hook
import React, { useState, useEffect } from "react";
import { dict as en } from "./en";
import { dict as de } from "./de";
const useTranslation = () => {
const [lang, setLang] = useState("en");
const dict = {
en,
de
};
function changeLang(lang) {
console.log("[useTranslation][changeLang] lang", lang);
setLang(lang);
}
console.log("[useTranslation] lang", lang);
return {
t: dict[lang],
changeLang: changeLang,
lang
};
};
export { useTranslation };
Header.js
import React, { useState } from "react";
import { useTranslation } from "./useTranslation";
function Header() {
const { changeLang, lang, t } = useTranslation();
function onChangeLang(ev) {
console.log("[index][onChangeLang] lang", lang);
changeLang(lang === "en" ? "de" : "en");
}
return (
<div>
<button onClick={onChangeLang}>{t.ui.changeLang}</button>
<span>{lang}</span>
</div>
);
}
export { Header };
en.js translations
const dict = {
ui: {
btn: "COUNT",
changeLang: "CHANGE LANGUAGE"
},
app: {
title: "Hook Example"
}
};
export { dict };
de.js translations
const dict = {
ui: {
btn: "Anzahl",
changeLang: "SPRACHE Ă„NDERN"
},
app: {
title: "Hook-Beispiel"
}
};
export { dict };
Hooks are intialised each time you call it from a component. When you are calling the same hook from Header and App two independent instances of the state lang is being created. Instead, what you require is React Context which can preserve the state across components.
const LanguageContext = createContext({
lang: "en",
setLang: () => {},
});
const LanguageProvider = ({ children }) => {
const [lang, setLang] = useState("en");
return (
<LanguageContext.Provider value={{ lang, setLang }}>
{children}
</LanguageContext.Provider>
)
}
const useTranslation = () => {
const langContext = useContext(LanguageContext);
const dict = {
en,
de
};
return {
t: dict[langContext.lang],
lang: langContext.lang,
changeLang: langContext.setLang,
};
};
export { useTranslation, LanguageContext, LanguageProvider };
Codesandbox | React Context
I've created a Higher Order Component / Composed Component to ensure a user is authenticated before loading the Component. It's very basic, but I'm having some trouble testing it. I want to test the points below, which are similar to the tests I already have elsewhere:
Renders the Component (I normally check by looking for a Component specific className)
Has correct props (in my case authenticated)
Renders the wrapped Component if authenticated and renders null if not
The HOC:
import React from 'react';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { makeSelectAuthenticated } from 'containers/App/selectors';
export default function RequireAuth(ComposedComponent) {
class AuthenticatedComponent extends React.Component {
static contextTypes = {
router: React.PropTypes.object,
}
static propTypes = {
authenticated: React.PropTypes.bool,
}
componentWillMount() {
if (!this.props.authenticated) this.context.router.push('/');
}
componentWillUpdate(nextProps) {
if (!nextProps.authenticated) this.context.router.push('/');
}
render() {
return (
<div className="authenticated">
{ this.props.authenticated ? <ComposedComponent {...this.props} /> : null }
</div>
);
}
}
const mapStateToProps = createStructuredSelector({
authenticated: makeSelectAuthenticated(),
});
return connect(mapStateToProps)(AuthenticatedComponent);
}
I'm using enzyme and jest for my tests, but haven't found a way of rendering the HOC successfully during my tests.
Any ideas?
Solution thanks to answer below:
import React from 'react';
import { shallow, mount } from 'enzyme';
import { Provider } from 'react-redux';
import { AuthenticatedComponent } from '../index';
describe('AuthenticatedComponent', () => {
let MockComponent;
beforeEach(() => {
MockComponent = () => <div />;
MockComponent.displayName = 'MockComponent';
});
it('renders its children when authenticated', () => {
const wrapper = shallow(
<AuthenticatedComponent
composedComponent={MockComponent}
authenticated={true}
/>,
{ context: { router: { push: jest.fn() } } }
);
expect(wrapper.find('MockComponent').length).toEqual(1);
});
it('renders null when not authenticated', () => {
const wrapper = shallow(
<AuthenticatedComponent
composedComponent={MockComponent}
authenticated={false}
/>,
{ context: { router: { push: jest.fn() } } }
);
expect(wrapper.find('MockComponent').length).toEqual(0);
});
});
The "tricky" part here is that your HOC returns a connected component, which makes testing harder because you have shallow render two layers (the connected component and the actual component) and you have to mock the redux store.
Instead you could define the AuthenticatedComponent upfront and export it as a named export. Than you can test it independently of connect like you test every other component:
export class AuthenticatedComponent extends React.Component {
static contextTypes = {
router: React.PropTypes.object,
}
static propTypes = {
authenticated: React.PropTypes.bool,
composedComponent: React.PropTypes.any.isRequired,
}
componentWillMount() {
if (!this.props.authenticated) this.context.router.push('/');
}
componentWillUpdate(nextProps) {
if (!nextProps.authenticated) this.context.router.push('/');
}
render() {
const ComposedComponent = this.props.composedComponent;
return (
<div className="authenticated">
{ this.props.authenticated ? <ComposedComponent {...this.props} /> : null }
</div>
);
}
}
export default function RequireAuth(ComposedComponent) {
const mapStateToProps = () => {
const selectIsAuthenticated = makeSelectAuthenticated();
return (state) => ({
authenticated: selectIsAuthenticated(state),
composedComponent: ComposedComponent,
});
};
return connect(mapStateToProps)(AuthenticatedComponent);
}
Example test:
import React from 'react';
import { shallow, mount } from 'enzyme';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import RequireAuth, { AuthenticatedComponent } from '../';
const Component = () => <div />;
Component.displayName = 'CustomComponent';
const mockStore = configureStore([]);
describe.only('HOC', () => {
const RequireAuthComponent = RequireAuth(Component);
const context = { router: { push: jest.fn() } };
const wrapper = mount(
<Provider store={mockStore({})}>
<RequireAuthComponent />
</Provider>,
{
context,
childContextTypes: { router: React.PropTypes.object.isRequired },
}
);
it('should return a component', () => {
expect(wrapper.find('Connect(AuthenticatedComponent)')).toHaveLength(1);
});
it('should pass correct props', () => {
expect(wrapper.find('AuthenticatedComponent').props()).toEqual(
expect.objectContaining({
authenticated: false,
composedComponent: Component,
})
);
});
});
describe('rendering', () => {
describe('is authenticated', () => {
const wrapper = shallow(
<AuthenticatedComponent
composedComponent={Component}
authenticated
/>,
{ context: { router: { push: jest.fn() } } }
);
it('should render the passed component', () => {
expect(wrapper.find('CustomComponent')).toHaveLength(1);
});
});
describe('is not authenticated', () => {
const wrapper = shallow(
<AuthenticatedComponent
composedComponent={Component}
authenticated={false}
/>,
{ context: { router: { push: jest.fn() } } }
);
it('should not render the passed component', () => {
expect(wrapper.find('CustomComponent')).toHaveLength(0);
});
});
});