I have this connected component that i am trying to test, i import two actions that dispatch a call to the store. The actual button click i am trying to test it should toggle between css classes.
When i simulate the click in my test it i get a error that one of my actions triggered by the click event is not a function.
TypeError: setLikedProducts is not a function
13 |
14 | const handleLike = () => {
> 15 | return like ? (setLike(false), removeLikedProduct(product)) : (setLike(true), setLikedProducts(product));
| ^
16 | }
17 |
18 | return (
My Component:
export function LikeProduct (props) {
const [like, setLike] = useState(false);
const { product, setLikedProducts, removeLikedProduct } = props;
const handleLike = () => {
return like ? (setLike(false), removeLikedProduct(product)) : (setLike(true), setLikedProducts(product));
}
return (
<div className="LikeProduct">
<Button
className={like ? "LikeProduct__like" : "LikeProduct__button"}
variant="link"
onClick={handleLike}>
<FaRegThumbsUp />
</Button>
</div>
);
}
const mapDispatchToProps = () => {
return {
setLikedProducts,
removeLikedProduct
}
}
export default connect(null, mapDispatchToProps())(LikeProduct);
my Test:
const props = {
info: {
product: "",
setLikedProducts: jest.fn(),
removeLikedProduct: jest.fn()
}
}
describe('Does LikeProduct Component Render', () => {
let wrapper = shallow(<LikeProduct {...props}/>);
it('LikeProduct render its css class', () => {
expect(wrapper.find('.LikeProduct').length).toBe(1);
});
it('Trigger the button on LikeProduct', () => {
console.log(wrapper.debug())
wrapper.find('Button').simulate('click');
});
Not sure why i am getting this error
your props are incorrectly defined, given your props contract
it should be
const props = {
product: "",
setLikedProducts: jest.fn(),
removeLikedProduct: jest.fn()
}
By the way, just in case you don't know, you can use useDispatch hook from react-redux in order to access dispatch function, instead of using connect
Related
I'm new to jest and want to write my first tests, where I want to test a slide change in my swiper. The onClick on my next-slide button has a callback function that comes from a react useContext.
When I run my test, i get this error:
SwiperComponent › Swiper slide › renders the name of the next item of my mockData, after clicking next button
TypeError: trackClickWithParam is not a function
116 | onClick={(event) => {
117 | (event.target as HTMLButtonElement).blur();
> 118 | trackClickWithParam(trackingParamNext);
| ^
119 | }}
This is my context:
//context
export const TrackingProvider: React.FC = ({ children }) => {
const [webtrekk, setWebtrekk] = useState<WebTrekk | null>(null);
const [showCookieConsentLayer, setShowCookieConsentLayer] = useState<boolean>(false);
const initWebtrekkWithConfig = (config: WebtrekkConfig): void => {
setWebtrekk(new webtrekkV3(config));
};
const trackClickWithParam = (param: string): void => {
TRACKING_ACTIVE && webtrekk && webtrekk.sendinfo({ linkId: param });
};
useEffect(() => {
TRACKING_ACTIVE && webtrekk && webtrekk.sendinfo();
window.wt = webtrekk;
}, [webtrekk]);
return (
<TrackingContext.Provider
value={{
initWebtrekkWithConfig,
trackClickWithParam,
showCookieConsentLayer,
setShowCookieConsentLayer,
}}
>
{children}
</TrackingContext.Provider>
);
};
and this is my test:
//test
import { render, screen } from '#testing-library/react';
import SwiperComponent from './SwiperComponent';
import user from '#testing-library/user-event';
const props = {
onSwiperSlide: jest.fn(),
currentSwipeLabel: mockData[0].name,
currentContentItem: mockData[0],
contentData: mockData,
contentType: 0,
trackingParamPrev: '',
trackingParamNext: '',
};
describe('SwiperComponent', () => {
beforeEach(() => {
render(<SwiperComponent {...props} />);
});
it('renders the name of the first item of my mockData', () => {
const swiperActiveContentName = screen.getByText(mockData[0].name);
expect(swiperActiveContentName).toBeInTheDocument();
});
it('renders the next-slide button', () => {
const nextSlideButton = screen.getByRole('button', { name: 'Nächster Slide' });
expect(nextSlideButton).toBeInTheDocument();
});
describe('Swiper slide', () => {
beforeEach(() => {
user.click(screen.getByRole('button', { name: 'Nächster Slide' }));
});
it('renders the name of the next item of my mockData, after clicking next button', () => {
expect(screen.getByText(mockData[1].name)).toBeInTheDocument();
});
});
});
Does any1 know why trackClickWithParam shouldn't be a function?
The code doesn't throw any errors outside of my test.
Can I somehow tell the test to ignore the callback?
I'm thankful for any hints =)
I have question about react-testing-library with custom hooks
My tests seem to pass when I use context in custom hook, but when I update context value in hooks cleanup function and not pass.
So can someone explain why this is or isn't a good way to test the custom hook ?
The provider and hook code:
// component.tsx
import * as React from "react";
const CountContext = React.createContext({
count: 0,
setCount: (c: number) => {},
});
export const CountProvider = ({ children }) => {
const [count, setCount] = React.useState(0);
const value = { count, setCount };
return <CountContext.Provider value={value}>{children}</CountContext.Provider>;
};
export const useCount = () => {
const { count, setCount } = React.useContext(Context);
React.useEffect(() => {
return () => setCount(50);
}, []);
return { count, setCount };
};
The test code:
// component.spec.tsx
import * as React from "react";
import { act, render, screen } from "#testing-library/react";
import { CountProvider, useCount } from "./component";
describe("useCount", () => {
it("should save count when unmount and restore count", () => {
const Wrapper = ({ children }) => {
return <ContextStateProvider>{children}</ContextStateProvider>;
};
const Component = () => {
const { count, setCount } = useCount();
return (
<div>
<div data-testid="foo">{count}</div>
</div>
);
};
const { unmount, rerender, getByTestId, getByText } = render(
<Component />, { wrapper: Wrapper }
);
expect(getByTestId("foo").textContent).toBe("0");
unmount();
rerender(<Component />);
// I Expected: "50" but Received: "0". but I dont understand why
expect(getByTestId("foo").textContent).toBe("50");
});
});
When you call render, the rendered component tree is like this:
base element(document.body by default) -> container(createElement('div') by default) -> wrapper(CountProvider) -> Component
When you unmount the component instance, Wrapper will also be unmounted. See here.
When you rerender a new component instance, it just uses a new useCount hook and the default context value(you doesn' provide a context provider for rerender) in the useContext. So the count will always be 0. From the doc React.createContext:
The defaultValue argument is only used when a component does not have a matching Provider above it in the tree.
You should NOT unmount the CountProvider wrapper, you may want to just unmount the Component. So that the component will receive the latest context value after mutate it.
So, the test component should be designed like this:
component.tsx:
import React from 'react';
const CountContext = React.createContext({
count: 0,
setCount: (c: number) => {},
});
export const CountProvider = ({ children }) => {
const [count, setCount] = React.useState(0);
return <CountContext.Provider value={{ count, setCount }}>{children}</CountContext.Provider>;
};
export const useCount = () => {
const { count, setCount } = React.useContext(CountContext);
React.useEffect(() => {
return () => setCount(50);
}, []);
return { count, setCount };
};
component.test.tsx:
import React, { useState } from 'react';
import { fireEvent, render } from '#testing-library/react';
import { CountProvider, useCount } from './component';
describe('useCount', () => {
it('should save count when unmount and restore count', () => {
const Wrapper: React.ComponentType = ({ children }) => {
const [visible, setVisible] = useState(true);
return (
<CountProvider>
{visible && children}
<button data-testid="toggle" onClick={() => setVisible((pre) => !pre)}></button>
</CountProvider>
);
};
const Component = () => {
const { count } = useCount();
return <div data-testid="foo">{count}</div>;
};
const { getByTestId } = render(<Component />, { wrapper: Wrapper });
expect(getByTestId('foo').textContent).toBe('0');
fireEvent.click(getByTestId('toggle')); // unmount the Component
fireEvent.click(getByTestId('toggle')); // mount the Component again
expect(getByTestId('foo').textContent).toBe('50');
});
});
Test result:
PASS stackoverflow/67749630/component.test.tsx (8.46 s)
useCount
✓ should save count when unmount and restore count (54 ms)
---------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 80 | 100 |
component.tsx | 100 | 100 | 80 | 100 |
---------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 8.988 s, estimated 9 s
Also take a look at this example: Codesandbox
my code like this:
Info component:
import {
getAttachData,
} from '#src/actions/creators/account'
const Info: React.FC = () => {
const info = useSelector<any, Account>(state => state.getIn(['account', 'info']).toJS())
const list = useSelector<any, Data[]>(state => state.getIn(['account', 'list']).toJS())
const attach = useSelector<any, AttachData[]>(state => state.getIn(['account', 'attach']).toJS())
...
const handleChange = ({ select }) => {
dispatch(getAttachData({v: select}))
}
const Template = (params) => {
return (
<div>
<BaseSelect onChange={(val) => handleChange(val)} list={list} />}
</div>
)
}
return (
...
<Template data={info} />
{attach.map((child, cidx) => (<Template data={child} />))}
)
}
export default Info
BaseSelect component:
const BaseSelect: React.FC<Props> = props => {
const [selectId, setSelectId] = useState('')
const { list } = props
useEffect(() => {
if (!isEmpty(list)) {
...
}
console.log('init')
}, [])
const handleChange = (value) => {
setSelectId(value)
props.onChange({
select: value,
})
}
return (
<Select
data={list}
value={selectId}
onChange={handleChange}
/>
)
}
export default BaseSelect
when excute handleChange event in BaseSelect component, the props.onChange function will call handleChange event in info component, and dispatch http request getAttachData which will change attach data in redux store, but useEffect in BaseSelect component will also excute and in console will print 'init' two times.
console:
It's because your Template component re-creates every time when redux store is changing.
Just move Template component outside the Info component.
I want to add functionality that will collect custom events in redux in entire react app.
What I want to achieve is to place all event functions it in one place and only use this functions in components in my app when I want to trigger some event.
interface IEventsLoggerContext {
[key: string]: (val?: any) => void
}
export const EventsLoggerContext = createContext<IEventsLoggerContext>({})
class EventsLogger extends Component<{}, any> {
constructor (props: Readonly<{}>) {
super(props)
}
// event methods like below
pageLoaded = () => {
// here will be saving the event to redux store
console.log('page loaded')
}
targetClick = (e: SyntheticEvent) => {
// here will be saving the event to redux store
console.log('target click', e.target)
}
// ...
render () {
return (
<EventsLoggerContext.Provider
value={{
pageLoaded: this.pageLoaded,
targetClick: this.targetClick
}}
>
{this.props.children}
</EventsLoggerContext.Provider>
)
}
}
export default EventsLogger
I want to make all event log actions available in app so I wrapped all into my event provider:
<EventsLogger>
...
</EventsLogger>
And using in component like this:
const MainApp: React.FC = () => {
const { pageLoaded } = useContext(EventsLoggerContext)
useEffect(() => {
pageLoaded()
}, [pageLoaded])
return (
<div>Main App page</div>
)
}
Is this correct way to do this or is there maybe better approach to get functionality like this?
Using React Context is a clever way to solve this although it will require more code when adding more events compared to simply using the browser native window.dispatchEvent() function.
// SomeComponent.tsx
export const SomeComponent : FC = props => {
useEffect(() => {
const pageLoadEvent = new CustomEvent(
"pageLoaded",
{
detail : {
at: Date.now()
}
}
);
window.dispatchEvent(pageLoadEvent);
}, []):
// ...
}
// SomeOtherComponent.tsx
export const SomeOtherComponent : FC = props => {
useEffect(() => {
window.addEventListener("pageLoaded", onPageLoaded);
return () => window.removeEventListener("pageLoaded", onPageLoaded);
}, []);
function onPageLoaded(e: CustomEvent) {
// Do whatever you want here :)
}
// ...
}
I am trying to do a complete istanbul coverage test with jest. At this moment I have a component almost all tested but there is a handleSubmit function where I make a dispatch receiving form event data and when I run the test it tells me
TypeError: Cannot read property 'value' of undefined
10 | payload: {
11 | name: name.value,
> 12 | item: item.value,
| ^
13 | status: status.value }
14 | })
15 | }
I am loading a mockstore, mounted all the component, its all tested but the submit still fails. My test function is as simple as:
it('testing submit', () => {
const form = component.find(`[data-test="submit"]`).first()
form.simulate('submit')
... and continues expecting some existences, but there aren't problems there
I already tried this: enzyme simulate submit form, Cannot read property 'value' of undefined
And tried to parse the event values in the simulate action...
The complete module code is...
class Filters extends Component {
handleSubmit = event => {
event.preventDefault()
const {name, items, status} = event.target;
this.props.dispatch({
type: 'SEARCH_PLAYER',
payload: {
name: name.value,
item: item.value,
status: status.value }
})
}
render() {
return(
<div>
<form onSubmit={this.handleSubmit} data-test="submit">
<div className="form-group col-md-12 text-center"> ...
Another really crazy thing is that my test recognize the "event.target.name.value" and not the items and status. In fact if i delete items and status from the dispatch the test runs successfully.
Looks like you are using item on line 12, but extracting items from the event.target.
The way you chose to handle values is a bit strange. Instead, handle values through state like so: Controlled Components
Then you can test that this.props.dispatch() was called with the correct values.
Side note: Avoid using data attributes when unnecessary, as they'll start to clog up your DOM with superfluous attributes. You have plenty of options to find by element, element.className, className, ...and so on.
Working example: https://codesandbox.io/s/5j4474rkk (you can run the test defined below by clicking on the Tests tab at the bottom left of the screen.
components/Form/Form.js
import React, { Component } from "react";
import PropTypes from "prop-types";
export default class Form extends Component {
state = {
test: ""
};
static propTypes = {
dispatch: PropTypes.func.isRequired
};
handleChange = ({ target: { name, value } }) => {
this.setState({ [name]: value });
};
handleSubmit = e => {
e.preventDefault();
this.props.dispatch({
type: "SEARCH_PLAYER",
payload: {
test: this.state.test
}
});
};
render = () => (
<form onSubmit={this.handleSubmit} className="form-container">
<h1>Form Testing</h1>
<input
className="uk-input input"
type="text"
name="test"
placeholder="Type something..."
onChange={this.handleChange}
value={this.state.test}
/>
<button type="submit" className="uk-button uk-button-primary submit">
Submit
</button>
</form>
);
}
components/Form/__tests__/Form.js (shallowWrap and checkProps are custom functions that can be found in test/utils/index.js)
import React from "react";
import { shallowWrap, checkProps } from "../../../test/utils";
import Form from "../Form";
const dispatch = jest.fn();
const initialProps = {
dispatch
};
const initialState = {
test: ""
};
const wrapper = shallowWrap(<Form {...initialProps} />, initialState);
describe("Form", () => {
it("renders without errors", () => {
const formComponent = wrapper.find(".form-container");
expect(formComponent).toHaveLength(1);
});
it("does not throw PropType warnings", () => {
checkProps(Form, initialProps);
});
it("submits correct values to dispatch", () => {
const name = "test";
const value = "Hello World!";
const finalValues = {
type: "SEARCH_PLAYER",
payload: {
[name]: value
}
};
wrapper.find("input").simulate("change", { target: { name, value } }); // simulates an input onChange event with supplied: name (event.target.name) and value (event.target.value)
wrapper
.find(".form-container")
.simulate("submit", { preventDefault: () => null }); // simulates a form submission that has a mocked preventDefault (event.preventDefault()) to avoid errors about it being undefined upon form submission
expect(dispatch).toBeCalledWith(finalValues); // expect dispatch to be called with the values defined above
});
});