I have such component. It is a wrapper component for another component. There is onClick function, which should call the log if is mouse event
import log from './log';
export function withPressedLog(
Component,
options,
) {
class WithPressedLogComponent extends React.Component {
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
}
public render() {
const { ...props } = this.props;
return <Component {...props} onClick={this.onClick} />;
}
private onClick(e) {
if (this.props.onClick !== undefined) {
this.props.onClick(e);
}
if (e) {
this.props.log();
}
}
}
const mapDispatchToProps = {
log: () => log(options),
};
return connect(
undefined,
mapDispatchToProps,
)(WithPressedLogComponent);
}
I need to test is it called this.props.log. I have a unit test, but it not works. How I can do it using jest, enzyme?
it("should not log if has not mouse event", () => {
const onClickMock = jest.fn();
const logMock = jest.fn();
const ButtonWithLog = withPressedLog(Button, {
type: "BUTTON_PRESSED",
});
const subject = mountProvider(ButtonWithLog, { onClick: onClickMock, log: logMock });
const mockedEvent = { target:{} };
subject.find(ButtonWithLog).simulate("click", mockedEvent);
expect(onClickMock.mock.calls).toHaveLength(1);
expect(logMock.mock.calls).toHaveLength(0); // not works correctly, always return []
});
store
const store = createStore(() => ({}));
const dispatchMock = jest.fn();
store.dispatch = dispatchMock;
mountProvider function
function mountProvider(
Component,
props,
) {
return mount(
<Provider store={store}>
<Component {...props} />
</Provider>,
);
}
I think the problem here is that you are actually testing the connected component, not the unwrapped component.
Try to isolate the components you are testing more. For example, you can use enzyme's shallow wrapper and the dive method on the connected component, to directly get to the unwrapped component.
Specifically, your problem could be that your connected component is getting the log prop from the store (through mapDispatchToProps), but the store is mocked, so it does not work. In your test, you pass some mock function as prop to the component, but the reference gets lost once the components connects.
helpful thread on github
Related
I'm trying to create GlobalContext but when I update this inside Class Component it didn't work in Class Component it's showing value of globalState but it's not updating globalState via setGlobalState
GlobalContext
import React, { useState ,ReactNode} from 'react'
const initialMapContext: { globalState: any; setGlobalState: React.Dispatch<React.SetStateAction<any>> } = {
globalState: {},
// will update to the reducer we provide in MapProvider
setGlobalState: () => {},
};
const GlobalContext = React.createContext(initialMapContext );
interface Props { children?: ReactNode;}
export function GlobalProvider({children}:Props){
const [ globalState, setGlobalState ] = useState({name:'pranjal'});
return <GlobalContext.Provider value={{ globalState, setGlobalState }}>{children}</GlobalContext.Provider>;
}
export default GlobalContext
my code in classComponent is
static contextType = GlobalContext;
getData = async () =>{
const { globalState, setGlobalState } = this.context;
console.log(globalState); // pranjal
setGlobalState({name:"please login"});
console.log(globalState); // pranjal
// my rest code
}
but setGlobalState is not updating globalState value .
Although it works fine in the Functional component
Function.js
const { globalState, setGlobalState } = useContext(GlobalContext);
setGlobalState({name:'Please login'})
Rather than using static contextType = GlobalContext; , I would recommend you to use a Higher Order Component (HOC) which wraps a component with your GlobalContext.
Impementation:
GlobalContext
Export one HOC method called withGlobalContext as follows,
export const withGlobalContext = (Component) => (props) => (
<GlobalContext.Consumer>
{({ globalState, setGlobalState }) => (
<Component
globalState={globalState}
setGlobalState={setGlobalState}
{...props}
/>
)}
</GlobalContext.Consumer>
)
ClassComponent
Wrap the component with the HOC, to get the global context values as the props. And being available in the props, you can use it anywhere in the component, even outside render()
class ClassComponent extends Component {
componentDidMount() {
console.log(this.props.globalState)
console.log(this.props.setGlobalState)
}
render() {
return (
// Your JSX here
)
}
export default withGlobalContext(ClassComponent)
Also, I prefer exporting a custom hook, for using context in functional components, rather than using useContext
Implementation:
export function useGlobalContext() {
const context = useContext(GlobalContext)
if (context === undefined) {
throw new Error('You did something wrong')
}
return [context.globalState, context.setGlobalState]
}
Then in your functional component, use it like following:
function FunctionalComponent(){
const [globalState, setGlobalState] = useGobalContext()
return (
// Your JSX here
)
}
Cheers!
I have a component CustomerAccountDetails which is wrapped inside a Provider component.
I am spying on the method 'fetchAccountDetails' inside component CustomerAccountDetails for accountId that I have provided
Following is the code of the component CustomerAccountDetails:
class CustomerAccountDetails extends React.Component {
componentDidMount() {
if(this.props.accountId) {
this.props.fetchAccountDetails(this.props.accountId);
}
}
render() {
return <AccountDetails details={this.props.accountDetails}/>
}
}
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
fetchAccountDetails: accountId => getAccountDetails(accountId)
},
dispatch
);
const mapStateToProps = state => {
return {
accountDetails: state.accountDetails
}
}
export default connect(mapStateToProps, mapDispatchToProps)(CustomerAccountDetails);
Following is the sample code snippet of enzyme:
it('should fetch account details for the account id provided', () => {
const spyOn = jest.spyOn(
CustomerAccountDetails.prototype,
'fetchAccountDetails'
);
const wrapper = mount(
<Provider store={store}>
<CustomerAccountDetails accountId="1234" />
</Provider>
);
expect(spyOn).toHaveBeenCalled();
});
But when I run this above test case, it is showing the error as "Cannot spyOn on a primitive value; undefined given".
How can I solve this issue ?
You can apply the Inversion of Control principle and pass the function as a prop for the component.
It will be easier to test and will make your component more reusable.
it('should fetch account details for the account id provided', () => {
const fetchAccountDetailsMock = jest.fn();
const wrapper = mount(
<Provider store={store}>
<CustomerAccountDetails
accountId="1234"
fetchAccountDetails={fetchAccountDetailsMock} // now you have control over it
/>
</Provider>
);
expect(fetchAccountDetailsMock).toHaveBeenCalled();
});
Thats my component
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = {
...
};
this.input = React.createRef();
}
componentDidMount() {
const id = 'bar';
let element = document.getElementById(id);
element.addEventListener('transitionend', () => {
this.setState({ ... });
}, false);
}
...
When I set up my test like so
import React from 'react';
import { mount } from 'enzyme';
import 'jsdom-global/register';
import Foo from './';
it('renders the component correctly', () => {
const component = mount(
<Foo />
);
component
.unmount();
});
I get
console.error node_modules/react-dom/cjs/react-dom.development.js:16647
The above error occurred in the component:
in Foo (created by WrapperComponent)
in WrapperComponent
Consider adding an error boundary to your tree to customize error handling behavior.
● renders the component correctly
TypeError: Cannot read property 'addEventListener' of null
I tried
ReactDOM.render(<Foo />, document.body);
or adding this
const map = {};
Window.addEventListener = jest.genMockFn().mockImpl((event, cb) => {
map[event] = cb;
});
as well as this
const map = {};
document.addEventListener = jest.fn((event, cb) => {
map[event] = cb;
})
before mounting <Foo /> in the test. But it all comes back with the same error. Why is that?
One of reasons why direct DOM access is discouraged in React is because it makes testing more complicated and unpredictable.
DOM can be mocked entirely before mounting a component:
const elementMock = { addEventListener: jest.fn() };
jest.spyOn(document, 'getElementById').mockImplementation(() => elementMock);
A stub can be tested that is was called correctly:
expect(elementMock.addEventListener).toBeCalledWith('transitionend', expect.any(Function), false);
And event listener can be tested that it changes the state as expected:
const handler = elementMock.mock.calls[0][1];
handler();
...
I written a custom logic for handling async route loading bundles in react-router-dom .v4. It's work perfectly. But also I heard about useful package with nice API to do the same, like React-Loadable. It has one problem, I cannot get the props/state pushed from Redux on the mount of the component throw this package.
My code is rewritten from the custom style to react-loadable style in two examples below. The last one is react-loadable version, that does not throw state/props.
My personal code:
const asyncComponent = getComponent => {
return class AsyncComponent extends React.Component {
static Component = null;
state = { Component: AsyncComponent.Component };
componentWillMount() {
const { Component } = this.state
if (!Component) {
getComponent().then(({ default: Component }) => {
const { store } = this.props // CAN GET THE REDUX STORE
AsyncComponent.Component = Component;
this.setState({ Component });
});
}
}
render() {
const { Component } = this.state;
if (Component) {
return <Component {...this.props} />
}
return null;
}
};
};
export default withRouter(asyncComponent(() => import(/* webpackChunkName: "chunk_1" */ './containers/Component')))
The same code, but with React-Loadable:
const Loading = () => {
return <div>Loading...</div>;
}
const asyncComponent = Loadable({
loader: () => import(/* webpackChunkName: "" */ './containers/Component')
.then(state => {
const { store } = this.props // CANNOT GET THE REDUX STORE!!
}),
loading: Loading
})
export default withRouter(asyncComponent)
To get the state from Redux store via Provider you should place your asyncComponent in Stateful Component wrapper, like you do in your custom async logic (1st case).
It because Loadable library returns you asyncComponent like a function, not a constructor, that way he cannot get access to current Redux store. So, the working solution will be the next:
const Loading = () => {
return <div>Loading...</div>;
}
const asyncComponent = Loadable({
loader: () => import(/* webpackChunkName: "" */ './containers/Component')
.then(state => {
const { store } = this.props // YOU WILL GET THE REDUX STORE!!
}),
loading: Loading
})
class asyncComponentWrapper extends Component{ // Component wrapper for asyncComponent
render() {
return <asyncComponent {...this.props} />
}
}
export default withRouter(asyncComponentWrapper)
P.S.
I do not know what you try to do, but in case how to make reducer injection inside the current store (probably it's exactly what you trying to do), you need to include you Redux store explicitly by import, not from the Provider state.
How do I properly dispatch actions in React Lifecycle Methods?
Hello, friends
I have an action called checkSession() inside my very parent component (App.jsx). This action helps me to get user info.
This is how App.jsx looks like:
class App extends Component {
componentWillMount() {
this.props.checkSession();
}
render() {
if (this.props.auth === null) {
return <div>Loading...</div>;
} else {
return (
<BrowserRouter>
<Route path="/cart" component={Cart} />
</BrowserRouter>
);
}
}
}
const mapStateToProps = ({ auth }) => { auth };
export default connect(mapStateToProps, { checkSession })(App);
How do I properly dispatch another action inside the Cart.jsx component.
I try to dispatch the action inside the componentWillReceiveProps methods but it creates an infinite loop.
My Cart.jsx component looks like this:
import { getCart } from "../../actions";
class Cart extends Component {
componentWillReceiveProps(nextProps, nextState) {
if (nextProps.auth) {
this.props.getCart(nextProps.auth.googleId);
} else {
this.props.getCart(null);
}
}
render() {
......some staff
}
}
const mapStateToProps = ({ auth }) => { auth };
export default connect(mapStateToProps, { getCart })(Cart);
I have just tried to dispatch this action inside the ComponentWillMount - but my props are not ready yet so I got an error.
Help me please with any advice, and sorry for my English.
Maybe you have to detect if the props have changed before firing the dispatch:
componentWillReceiveProps(nextProps, nextState) {
const currentId = this.props.auth && this.props.auth.googleId;
const nextId = nextProps.auth && nextProps.auth.googleId;
if (currentId !== nextId) {
this.props.getCart(nextProps.auth.googleId);
}
}