I have the following a.jsx file which I am trying to test:
import React, { Component } from "react";
import { SideNavigation } from "#xxx/react-components-xxx";
import { withTranslation } from "react-i18next";
class Navigation extends Component {
constructor(props) {
super(props);
}
render() {
const { t } = this.props;
const ITEMS = [
{
type: "section",
text: t("navigation.abc"),
items: [
{
type: "link",
text: t("navigation.abc"),
href: "#/abc"
},
{ type: "link", text: t("navigation.def"), href: "#/def" }
]
}
];
const HEADER = {
href: "#/",
text: t("navigation.title")
};
return (
<SideNavigation
header={HEADER}
items={ITEMS}
/>
);
}
}
export default withTranslation()(Navigation);
And following is the test case I have written:
import React from "react";
import { shallow } from 'enzyme';
import Navigation from '../src/a';
import { SideNavigation } from '#xxx/react-components-xxx';
describe('Navigation component', () => {
it('should render consistently', () => {
const wrapper = shallow(
<Navigation />
);
console.log(wrapper.render());
expect(wrapper).not.toBeNull();
const sideNav = wrapper.find(SideNavigation);
console.log(sideNav.render());
const sideNavProps = sideNav.props();
console.log(sideNavProps);
});
});
And with this, I get following eror:
Method “type” is meant to be run on 1 node. 0 found instead.
14 |
15 | const sideNav = wrapper.find(SideNavigation);
> 16 | console.log(sideNav.render());
| ^
17 | const sideNavProps = sideNav.props();
18 | console.log(sideNavProps);
19 | });
at ShallowWrapper.single (node_modules/enzyme/src/ShallowWrapper.js:1636:13)
at ShallowWrapper.type (node_modules/enzyme/src/ShallowWrapper.js:1372:17)
at ShallowWrapper.render (node_modules/enzyme/src/ShallowWrapper.js:1106:17)
at Object.<anonymous> (tst/components/Navigation.test.js:16:29)
But if I use mount instead of shallow, everything works fine here. Can someone tell what is the issue here?
It's because of the WithTranslation.
They mention it in their doc
For testing purpose of your component you should export the pure component without extending with the withTranslation hoc and test that:
export MyComponent;
export default withTranslation('ns')(MyComponent);
In the test, test the myComponent export passing a t function mock:
import { MyComponent } from './myComponent';
<MyComponent t={key => key} />
Related
iam having trouble with covering function inside react context
Before that, let me show you the snippet of the code
import {
useCallback,
useEffect,
useState,
createContext,
ReactNode,
FC
} from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import PropTypes from 'prop-types';
import { pushBackRoute } from '../libs/helpers/backRouteHelper';
export interface BottomBarProviderProps {
children: ReactNode;
}
export interface IBottomBarProvider {
selectedMenu: string;
handleClickMenu: (item: { name: string; route: string }) => void;
}
export const defaultValueBottomBarContext = {
selectedMenu: '',
handleClickMenu: () => {}
};
export const BottomBarContext = createContext<IBottomBarProvider>(
defaultValueBottomBarContext
);
export const BottomBarProvider: FC<BottomBarProviderProps> = ({
children,
...props
}) => {
const history = useHistory();
const location = useLocation();
const [selectedMenu, setSelectedMenu] = useState('Belanja');
const handleClickMenu = useCallback(
(item) => {
setSelectedMenu(item.name);
pushBackRoute(location.pathname);
history.push(item.route);
},
[selectedMenu, location?.pathname]
);
useEffect(() => {
Iif (location.pathname === '/marketplace/history') {
setSelectedMenu('Riwayat');
}
}, [location.pathname]);
const value: IBottomBarProvider = {
selectedMenu,
handleClickMenu
};
return (
<BottomBarContext.Provider
value={value}
{...props}
data-testid="bottom-bar-context"
>
{children}
</BottomBarContext.Provider>
);
};
BottomBarProvider.propTypes = {
children: PropTypes.node
};
BottomBarProvider.defaultProps = {
children: null
};
So i managed to cover most of the hooks and render, but the function/handle function inside this context is really hard
And this is my current test code, below :
/* eslint-disable jest/prefer-called-with */
import { render, screen } from '#testing-library/react';
import { BottomBarProvider } from './BottomBarContext';
import '#testing-library/jest-dom/extend-expect';
const mockHistoryPush = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: () => ({
pathname: '/marketplace'
}),
useHistory: () => ({
push: mockHistoryPush
})
}));
describe('BottomBarProvider', () => {
it('should render the children and update the selected menu when handleClickMenu is called', () => {
//Arrange
const children = <div data-testid="children">Hello, World!</div>;
//Act
render(<BottomBarProvider>{children}</BottomBarProvider>);
//Assert
expect(screen.getByTestId('children')).toHaveTextContent('Hello, World!');
});
});
Do you guys have any idea how to cover the function inside this context ?
Mock functions, hooks, and components of the module you don't own are not recommended. The useLocation and useHistory hooks in your case. Incorrect mock will break their functions which causes your tests to pass based on incorrect mock implementation.
Use <MemoryRouter/> component for testing history change. See official example of RTL about how to test react router
You should also create a test component to consume the React Context for testing the <BottomBarProvider/> component and BottomBarContext. The key is to trigger the handleClickMenu event handler by firing a click event on some element of the test component.
E.g.
BottomBarContext.tsx:
import React from 'react';
import {
useCallback,
useEffect,
useState,
createContext,
ReactNode,
FC
} from 'react';
import { useHistory, useLocation } from 'react-router-dom';
// import { pushBackRoute } from '../libs/helpers/backRouteHelper';
export interface BottomBarProviderProps {
children: ReactNode;
}
export interface IBottomBarProvider {
selectedMenu: string;
handleClickMenu: (item: { name: string; route: string }) => void;
}
export const defaultValueBottomBarContext = {
selectedMenu: '',
handleClickMenu: () => { }
};
export const BottomBarContext = createContext<IBottomBarProvider>(
defaultValueBottomBarContext
);
export const BottomBarProvider: FC<BottomBarProviderProps> = ({
children,
...props
}) => {
const history = useHistory();
const location = useLocation();
const [selectedMenu, setSelectedMenu] = useState('Belanja');
const handleClickMenu = useCallback(
(item) => {
setSelectedMenu(item.name);
// pushBackRoute(location.pathname);
history.push(item.route);
},
[selectedMenu, location?.pathname]
);
useEffect(() => {
if (location.pathname === '/marketplace/history') {
setSelectedMenu('Riwayat');
}
}, [location.pathname]);
const value: IBottomBarProvider = {
selectedMenu,
handleClickMenu
};
return (
<BottomBarContext.Provider value={value} {...props} data-testid="bottom-bar-context">
{children}
</BottomBarContext.Provider>
);
};
BottomBarContext.test.tsx:
import { fireEvent, render, screen } from '#testing-library/react';
import '#testing-library/jest-dom';
import React, { useContext } from 'react';
import { MemoryRouter, Route, Switch } from 'react-router-dom';
import { BottomBarContext, BottomBarProvider } from './BottomBarContext';
describe('BottomBarProvider', () => {
test('should pass', async () => {
const TestComp = () => {
const bottomBarContext = useContext(BottomBarContext);
const menuItems = [
{ name: 'a', route: '/a' },
{ name: 'b', route: '/b' },
];
return (
<>
<ul>
{menuItems.map((item) => (
<li key={item.route} onClick={() => bottomBarContext.handleClickMenu(item)}>
{item.name}
</li>
))}
</ul>
<p>selected menu: {bottomBarContext.selectedMenu}</p>
<Switch>
{menuItems.map((item) => (
<Route key={item.route} path={item.route} component={() => <div>{item.name} component</div>} />
))}
</Switch>
</>
);
};
render(
<MemoryRouter initialEntries={['/']}>
<BottomBarProvider>
<TestComp />
</BottomBarProvider>
</MemoryRouter>
);
const firstListItem = screen.getAllByRole('listitem')[0];
fireEvent.click(firstListItem);
expect(screen.getByText('selected menu: a')).toBeInTheDocument();
expect(screen.getByText('a component')).toBeInTheDocument();
});
});
Test result:
PASS stackoverflow/74931928/BottomBarContext.test.tsx (16.574 s)
BottomBarProvider
✓ should pass (116 ms)
----------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------------------|---------|----------|---------|---------|-------------------
All files | 95 | 66.67 | 75 | 94.74 |
BottomBarContext.tsx | 95 | 66.67 | 75 | 94.74 | 50
----------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 18.184 s, estimated 23 s
Coverage HTML reporter:
Having the following component:
import React, { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useRouteMatch, Link } from 'react-router-dom';
interface MyComponentProps {
myId?: string;
link?: string;
}
export const MyComponent: React.FunctionComponent<MyComponentProps> = ({
myId = 'default-id',
link,
children
}) => {
const [myOutlet, setMyOutlet] = useState<HTMLOListElement>();
const match = useRouteMatch();
useEffect(() => {
const outletElement = document.getElementById(myId) as HTMLOListElement;
if (outletElement) {
setMyOutlet(outletElement);
}
}, [myId]);
if (!myOutlet) {
return null;
}
return createPortal(
<li>
<Link to={link || match.url}>{children}</Link>
</li>,
myOutlet
);
};
export default MyComponent;
I want to write unit tests using React Testing Library for it, the problem is that it keeps throwing an error because of useRouteMatch.
Here is my code:
import { render, screen } from '#testing-library/react';
import { MyComponent } from './my-component';
describe('MyComponent', () => {
const testId = 'default-id';
const link = '/route';
it('should render MyComponent successfully', () => {
const element = render(<MyComponent myId={testId} link={link} />);
expect(element).toBeTruthy();
});
});
The error appears at the line with const match = useRouteMatch();, is there a way to include this part in the test?
You should use <MemoryRouter>:
A <Router> that keeps the history of your “URL” in memory (does not read or write to the address bar)
Provide mock locations in the history stack by using the initialEntries props.
Then, use <Route> component to render some UI when its path matches the current URL.
The following example, assuming that the location pathname in the browser current history stack is /one, <Route>'s path prop is also /one, The two matching, rendering MyComponent.
E.g.
my-component.tsx:
import React, { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useRouteMatch, Link } from 'react-router-dom';
interface MyComponentProps {
myId?: string;
link?: string;
}
export const MyComponent: React.FunctionComponent<MyComponentProps> = ({ myId = 'default-id', link, children }) => {
const [myOutlet, setMyOutlet] = useState<HTMLOListElement>();
const match = useRouteMatch();
console.log('match: ', match);
useEffect(() => {
const outletElement = document.getElementById(myId) as HTMLOListElement;
if (outletElement) {
setMyOutlet(outletElement);
}
}, [myId]);
if (!myOutlet) {
return null;
}
return createPortal(
<li>
<Link to={link || match.url}>{children}</Link>
</li>,
myOutlet
);
};
export default MyComponent;
my-component.test.tsx:
import React from 'react';
import { render, screen } from '#testing-library/react';
import { MyComponent } from './my-component';
import { MemoryRouter, Route } from 'react-router-dom';
describe('MyComponent', () => {
const testId = 'default-id';
const link = '/route';
it('should render MyComponent successfully', () => {
const element = render(
<MemoryRouter initialEntries={[{ pathname: '/one' }]}>
<Route path="/one">
<MyComponent myId={testId} link={link} />
</Route>
</MemoryRouter>
);
expect(element).toBeTruthy();
});
});
test result:
PASS examples/70077434/my-component.test.tsx (8.433 s)
MyComponent
✓ should render MyComponent successfully (46 ms)
console.log
match: { path: '/one', url: '/one', isExact: true, params: {} }
at MyComponent (examples/70077434/my-component.tsx:13:11)
------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------------|---------|----------|---------|---------|-------------------
All files | 87.5 | 28.57 | 100 | 86.67 |
my-component.tsx | 87.5 | 28.57 | 100 | 86.67 | 18,26
------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 8.951 s, estimated 9 s
package versions:
"react": "^16.14.0",
"react-router-dom": "^5.2.0"
I want to start my React microapp with props I'm passing from Single SPA (customProps). The only way I've figured out is:
import React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import App from './where/my/root/is.js';
function domElementGetter() {
return document.getElementById("mounting-node")
}
let EnhancedRootComponent = App; /* 1 */
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: EnhancedRootComponent, /* 1 */
domElementGetter,
})
export const bootstrap = [
(args) => {
/* 2 */ EnhancedRootComponent = () => <App myArgs={args.thePropsIWannaPass} />;
return Promise.resolve();
},
reactLifecycles.bootstrap,
];
export const mount = [reactLifecycles.mount];
export const unmount = [reactLifecycles.unmount];
This does work (I can see and use the passed props in my component) but I'm not completely OK with the fact that the root component changes in between calling singleSpaReact (1) and calling bootstrap(2). Would there be side effects to this that I'm not seeing now? Does anyone know a better approach for this?
You have this value inside the props variable without this reassign.
Check this out:
Root-config.js, file responsible for passing prop to microfrontend
import { registerApplication, start } from 'single-spa';
import * as isActive from './activity-functions';
registerApplication('#company/micro2', () => System.import('#company/micro2'), isActive.micro2);
registerApplication('#company/micro1', () => System.import('#company/micro1'), isActive.micro1, { "authToken": "test" });
start();
micro1 Root.tsx
import React from 'react';
export default class Root extends React.Component {
constructor(props: any){
super(props)
}
state = {
hasError: false,
};
componentDidCatch() {
this.setState({ hasError: true });
}
render() {
console.log(this.props)
return (
<div>test</div>
);
}
}
console.log output:
props:
authToken: "test" <---- props which you pass
name: "#company/micro1"
mountParcel: ƒ ()
singleSpa: {…}
__proto__: Object
for more advance usage
const lifecycles = singleSpaReact({
React,
ReactDOM,
loadRootComponent: (props) =>
new Promise((resolve, reject) => resolve(() =>
<Root {...props} test2={'test2'}/>)),
domElementGetter,
});
Not sure why I'm getting this error in my simple Main.test file.
The constructor of Main.js
export class Main extends Component {
constructor(props) {
super(props);
this.state = {
location: splitString(props.location.pathname, '/dashboard/')
}
if (R.isEmpty(props.view)) {
isViewServices(this.state.location)
? this.props.gotoServicesView()
: this.props.gotoUsersView()
}
}
Main.test
import React from 'react'
import * as enzyme from 'enzyme'
import toJson from 'enzyme-to-json'
import { Main } from './Main'
import Sidebar from '../../components/Common/sidebar'
const main = enzyme.shallow(<Main />);
describe('<Main /> component', () => {
it('should render', () => {
const tree = toJson(main);
expect(tree).toMatchSnapshot();
});
it('contains the Sidebar', () => {
expect(main.find(Sidebar).length).toBe(1);
});
});
Is there a way to mock up the 'pathname'?
It seems you might have a few errors one being that in your test your not passing in any props.
And another from you accessing this.props in your constructor.
See your if statement but I'll put the fix here to be explicit
if (R.isEmpty(props.view)) {
isViewServices(this.state.location)
? props.gotoServicesView()
: props.gotoUsersView()
}
In Main.test
const location = { pathname: '/dashboard/' };
const main = enzyme.shallow(<Main location={ location }/>);
I am trying to write the test case for my jsx file...
i took the sample test case from another jsx file...
i am facing syntax error....when passing proptypes....
I think because of this its breaking my test case...
can you guys tell me how to fix it..
providing my code below...
clear code below https://gist.github.com/js08/d590e78e8923e68b191a
SyntaxError: C:/codebase/sports/test/sports-tests.js: Unexpected token (20:73)
18 |
19 | it('should render correctly', () => {
> 20 | shallowRenderer.render();
| ^
21 | /*let renderedElement = shallowRenderer.getRenderOutput();
22 |
test case
import {expect} from 'chai';
import React from 'react';
import TestUtils from 'react-addons-test-utils';
import sportsPageDefault from '../src/sports-Page-Default';
import initializeJsDom from './test-utils/dom.js';
import {getGridLayoutClasses} from 'sports-css-grids';
//import _difference from 'lodash/array/difference';
describe('shallow renderer tests for sports-Page-Default ', function() {
let shallowRenderer = TestUtils.createRenderer();
console.log("shallowRenderer" + JSON.stringify(shallowRenderer));
it('should render correctly', () => {
shallowRenderer.render(<sportsPageDefault headerClass='8887' layout= {id: 100, text: 'hello world'} sidebar= {id: 100, text: 'hello world'} title="Demo" />);
let renderedElement = shallowRenderer.getRenderOutput();
console.log(renderedElement);
/* let actualTitleEl = renderedElement.props.children[0].props.children[0];
let expectedTitleEl = <h1 className="transactionalPageHeader-appName font-serif">Demo</h1>;
expect(actualTitleEl).to.deep.equal(expectedTitleEl);*/
});
});
actual code
import './css/sports-bottom-layout.css';
import './css/sports-Page.css';
import './css/sports-leftCornerLayout.css';
import React from 'react';
import PageHeader from './components/page-header/page-header';
import MainContent from './components/main-content';
import sports-bottom-layout from './components/sports-bottom-layout/sports-bottom-layout';
import {getPanelLayoutState} from './util/PageLayout';
import {getGridLayoutClasses} from 'sports-css-grids/lib/js/gridLayout';
import PagePureRenderMixin from './util/PagePureRenderMixin';
import {connect} from 'react-redux';
import {setHeaderPanelState, setRightPanelState} from './redux/layout/layout-actions';
console.log("inside");
let customMixin = PagePureRenderMixin({
state: {
mainPanelGridClassList: function(classArray) {
return classArray.length;
console.log("classArray.length" + classArray.length);
}
}
});
let PT = React.PropTypes;
let sportsPageDefault = React.createClass({
propTypes: {
headerClass: React.PropTypes.string,
layout: PT.object.isRequired,
sports-leftCornerLayout: PT.oneOfType([
PT.func,
PT.object
]),
title: PT.string.isRequired
},
//cpomponent m,ount code
});
function sportsShallow(itemA, itemB) {
for (let i in itemA) {
if (itemA[i] !== itemB[i]) {
return false;
}
}
return true;
}
export default connect(state => ({
layout: state.Page.layout
}))(sportsPageDefault);
**but another file it works fine, for example**
propTypes: {
footer: React.PropTypes.bool,
onAppExit: React.PropTypes.func.isRequired,
title: React.PropTypes.string.isRequired
},
let component = TestUtils.renderIntoDocument(<sports title="Demo" onAppExit={onAppExit}/>);