I have my InputLocation Component with children:
InputName
DrawLocation
I want to make some unit test to check my component, but I have a problem with simulating componentDidMount cycle.
InputName component depends on and displays when DrawLocation is mounted. See below please:
input-location.js
class InputLocation extends React.Component {
state = {
isMapReady: false
}
setMapReady = () => this.setState({ isMapReady: true })
onScrollTop = () => {
const { handlers } = this.props
handlers.onScrollTop && handlers.onScrollTop()
}
render() {
const {
autoFocus, captionTip, children, id, handlers, trackEvents,
mapId, value, coordinates, inputNameErrorMessage, isNameError
} = this.props
const { isMapReady } = this.state
return (
<div className='input-location'>
<div className='input-location__module'>
{!!isMapReady && (
<InputName
autoFocus={autoFocus}
caption={i18n.t('INPUT_LOCATION.NAME_LABEL')}
captionTip={captionTip}
id={id}
isError={isNameError}
handlers={handlers}
placeholder={i18n.t('INPUT_LOCATION.NAME_PLACEHOLDER')}
requiredMessage={inputNameErrorMessage}
value={value}
/>
)}
</div>
{children}
<div className='input-location__module'>
<DrawLocation
coordinates={coordinates}
mapId={mapId}
handlers={{
...handlers,
onMount: callAll(
this.setMapReady,
this.onScrollTop
)
}}
trackEvents={trackEvents}
/>
</div>
</div>
)
}
}
And a small piece of DrawLocation component to show where is onMount handlers there.
draw-location.js
class DrawLocation extends React.PureComponent {
componentDidMount() {
const { handlers } = this.props
handlers.onMount && handlers.onMount()
}
...
render() {
return (
<div>
Something...
</div>
)
}
}
I've started to create some test using nwb and expect
input-location-test.js
import expect from 'expect'
const containerPath = '.input-location'
const inputNamePath = '.input-location__name'
const errorPath = '.validation'
const injectInputLocation = require('inject-loader!./input-location')
const InputLocation = injectInputLocation({
'./input-name': () => <div className='input-location__name'></div>,
'./tip': () => <div />,
'cm/components/map/draw-location': ({ handlers, children }) => (
<div className="map__container" handlers={handlers.onMount}>
{children}
</div>
)
}).default
describe('InputLocation component', () => {
let node
beforeEach(() => {
node = document.createElement('div')
})
afterEach(() => {
ReactDOM.unmountComponentAtNode(node)
})
it('renders component', () => {
ReactDOM.render(
<InputLocation />,
node
)
const container = node.querySelector(containerPath)
expect(container).toExist()
})
it('doesn\'t render input-name if no properties specified', () => {
ReactDOM.render(
<InputLocation />,
node
)
const elementNode = node.querySelector(inputNamePath)
expect(elementNode).toNotExist()
})
it('renders input-name', () => {
ReactDOM.render(
<InputLocation
handlers={{
onMount: () => {}
}}
/>,
node
)
const elementNode = node.querySelector(inputNamePath)
expect(elementNode).toExist()
})
})
First and second test is OK and give me positive results, but the last one failed.
Please about your help to solve the issue.
Related
I tried using the "withref" in react-sortable-hoc and I need to have my parent component access the children components for some calls I need to invoke on the parent side. I'm not sure where to even call the getWrappedInstance, which seems to be providing access to the children component.
I'm aware of forwarding but it seems like react-sortable-hoc have a different implementation.
To be more specific, I have something like this:
const SortableItem = SortableElement((props) => (
<div className="sortable">
<MyElement {...props}/>
</div>
), {withRef: true});
const MidasSortableContainer = SortableContainer(({ children }: { children: any }) => {
return <div>{children}</div>;
}, {withRef: true});
<MySortableContainer
axis="xy"
onSortEnd={this.onSortEnd}
useDragHandle
>{chartDivs}</MySortableContainer>
Before I wrapped in HOC, I was able to do the following
const chartDivs = elements.map(({childName}, index) => {
return <MyElement
ref={r => this.refsCollection[childName] = r}
...
Does anyone have any ideas how to achieve the same after wrapping with HOC? Thanks.
The key is from source code: https://github.com/clauderic/react-sortable-hoc/blob/master/src/SortableElement/index.js#L82
getWrappedInstance() function.
I guess, after is your origin code:
// this is my fake MyElement Component
class MyElement extends React.Component {
render () {
return (
<div className='my-element-example'>This is test my element</div>
)
}
}
// this is your origin ListContainer Component
class OriginListContainer extends React.Component {
render () {
const elements = [
{ childName: 'David' },
{ childName: 'Tom' }
]
return (
<div className='origin-list-container'>
{
elements.map(({ childName }, index) => {
return <MyElement key={index} ref={r => this.refsCollection[childName] = r} />
})
}
</div>
)
}
}
Now you import react-sortable-hoc
import { SortableElement, SortableContainer } from 'react-sortable-hoc'
First you create new Container Component:
const MySortableContainer = SortableContainer(({ children }) => {
return <div>{children}</div>;
})
Then make MyElement be sortable
/**
* Now you have new MyElement wrapped by SortableElement
*/
const SortableMyElement = SortableElement(MyElement, {
withRef: true
})
Here is import:
You should use SortableElement(MyElement, ... to SortableElement((props) => <MyElement {...props}/>, second plan will make ref prop be null
{ withRef: true } make your can get ref by getWrappedInstance
OK, now you can get your before ref like after ref={r => this.refsCollection[childName] = r.getWrappedInstance()} />
Here is full code:
const MySortableContainer = SortableContainer(({ children }) => {
return <div>{children}</div>;
})
/**
* Now you have new MyElement wrapped by SortableElement
*/
const SortableMyElement = SortableElement(MyElement, {
withRef: true
})
class ListContainer extends React.Component {
refsCollection = {}
componentDidMount () {
console.log(this.refsCollection)
}
render () {
const elements = [
{ childName: 'David' },
{ childName: 'Tom' }
]
return (
<MySortableContainer
axis="xy"
useDragHandle
>
{
elements.map(({ childName }, index) => {
return (
<SortableMyElement
index={index}
key={index}
ref={r => this.refsCollection[childName] = r.getWrappedInstance()} />
)
})
}
</MySortableContainer>
)
}
}
Append
Ehh...
before I said: You should use SortableElement(MyElement, ... to SortableElement((props) => <MyElement {...props}/>, second plan will make ref prop be null
if you really wanna use callback function, you can use like after:
const SortableMyElement = SortableElement(forwardRef((props, ref) => <MyElement ref={ref} {...props} />), {
withRef: true
})
But here NOT the true use of forwardRef
Ehh... choose your wanna.
What I want to do, is create a HOC that has a method that can be triggered by whatever Parent Component is using that HOC to wrap.
For this HOC, I'm trying to fade out the HOC and any components inside it:
HOC:
export function fadeOutWrapper(WrappedComponent) {
return class extends Component {
constructor(props) {
super(props);
this.state = {
showElement: true,
removeElement: false,
};
}
_triggerFade = () => {
this._fadeOut(this.props.time).then(time => this._removeElement(time));
}
_fadeOut = time => {
let _this = this;
return new Promise((resolve, reject) => {
_this.setState({
showElement: false
});
setTimeout(() => {
resolve(time);
}, time);
});
};
_removeElement = time => {
let _this = this;
setTimeout(() => {
_this.setState({
removeElement: true
});
}, time + 500);
};
render() {
return this.state.removeElement ? null : (
<div
className={
this.state.showElement
? "cfd-container"
: "cfd-container cfd-fadeout"
}
>
<WrappedComponent {...this.props} />
</div>
);
}
};
}
How this component is being used in parent component:
import ComponentToBeFaded from '...';
import { fadeOutWrapper } from '...';
const WrappedComponent = fadeOutWrapper(ComponentToBeFaded);
class ParentComponent extends Component {
const...
super...
handleChildClick = () => {
// ? how to trigger the HOC _triggerFade method?
// WrappedComponent._triggerFade()
}
render() {
return (
<WrappedComponent time={1000} handleClick={this.handleChildClick} {...other props component needs} />
)
}
}
What I want to be able to do is call a method that is inside the HOC, can't seem to check for a change in props inside the HOC... only inside the HOC's render()
Need to keep writing more to meet the submission quota. Any thoughts on how to do this is appreciated. Hope your day is going well!
You don't need showElement in local state of the wrapped component because it's not controlled by that component. Pass it as props and use componentDidUpdate to start fading out.
const { Component, useState, useCallback } = React;
const Button = ({ onClick }) => (
<button onClick={onClick}>Remove</button>
);
function App() {
const [show, setShow] = useState(true);
const onClick = useCallback(() => setShow(s => !s), []);
return (
<WrappedButton
time={1000}
onClick={onClick}
showElement={show}
/>
);
}
function fadeOutWrapper(WrappedComponent) {
return class extends Component {
constructor(props) {
super(props);
this.state = {
removeElement: false,
fadeout: false,
};
}
componentDidUpdate(prevProps) {
if (
this.props.showElement !== prevProps.showElement &&
!this.props.showElement
) {
this._triggerFade();
}
}
_triggerFade = () => {
this._fadeOut(this.props.time).then(() =>
this._removeElement()
);
};
_fadeOut = time => {
this.setState({ fadeout: true });
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, time);
});
};
_removeElement = time => {
this.setState({
removeElement: true,
});
};
render() {
return this.state.removeElement ? null : (
<div>
{JSON.stringify(this.state)}
<WrappedComponent {...this.props} />
</div>
);
}
};
}
const WrappedButton = fadeOutWrapper(Button);
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
When creating a component that uses the children prop I would like to adjust its onclick event. This is easy if the child is a DOM element e.g. a Div but if it is a react component I can only edit the component props not the props of the resulting DOM element.
Edit: child component should be abstract and is not accessible for editing.
The following works for a DOM element:
export const Wrapper = ({ children }) => {
const handleOnClick = () => {};
return (
React.cloneElement(children, { ...children.props, onClick: handleOnClick })
)
}
const App = () => {
return (
<Wrapper>
<div /> // This div has a onclick event injected by wrapper
</Wrapper>
)
}
The following does not work
export const Wrapper = ({ children }) => {
const handleOnClick = () => {};
return (
React.cloneElement(children, { ...children.props, onClick: handleOnClick })
)
}
const InnerWrap = () => {
return (
<div /> // This div does not get onClick injected
)
}
const App = () => {
return (
<Wrapper>
<InnerWrap />
</Wrapper>
)
}
I have also tried with Refs but can't work out a way to apply to event to the ref
...
const childRef = useRef(null);
useLayoutEffect(() => {
//childRef.current is Null!
childRef.current.addEventListener('onClick', handleOnClick);
}, [childRef]);
React.cloneElement(children, { ref: childRef })
...
Maybe try something like this?
const InnerWrap = ({props}) => {
return (
<div {...props} />
)
}
Getting this error
Matcher error: received value must be a mock or spy function
Received has type: object
Received has value: {}
However, i think i shouldn't be getting this error because im using jest.fn. So im mocking the function.
describe('Should simulate button click', ()=> {
it('should simulate button click', () => {
// add the name of the prop, which in this case ites called onItemAdded prop,
// then use jest.fn()
const wrapper = shallow(<TodoAddItem onItemAdded={() => jest.fn()}/>)
// console.log('props',wrapper.find('button').props());
wrapper.find('button').simulate('click');
expect(wrapper).toHaveBeenCalled(); // error happens when this executes
})
})
todo-add-item.js
import React, { Component } from 'react';
import './todo-add-item.css';
export default class TodoAddItem extends Component {
render() {
return (
<div className="todo-add-item">
<button
className="test-button btn btn-outline-secondary float-left"
onClick={() => this.props.onItemAdded('Hello world')}>
Add Item
</button>
</div>
);
}
}
app.js (using the component in this file)
import React, { Component } from 'react';
import AppHeader from '../app-header';
import SearchPanel from '../search-panel';
import TodoList from '../todo-list';
import ItemStatusFilter from '../item-status-filter';
import TodoAddItem from '../todo-add-item';
import './app.css';
export default class App extends Component {
constructor() {
super();
this.createTodoItem = (label) => {
return {
label,
important: false,
done: false,
id: this.maxId++
}
};
this.maxId = 100;
this.state = {
todoData: [
this.createTodoItem('Drink Coffee'),
this.createTodoItem('Make Awesome App'),
this.createTodoItem('Have a lunch')
]
};
this.deleteItem = (id) => {
this.setState(({ todoData }) => {
const idx = todoData.findIndex((el) => el.id === id);
const newArray = [
...todoData.slice(0, idx),
...todoData.slice(idx + 1)
];
return {
todoData: newArray
};
});
};
this.addItem = (text) => {
const newItem = this.createTodoItem(text);
this.setState(({ todoData }) => {
const newArray = [
...todoData,
newItem
];
return {
todoData: newArray
};
});
};
this.onToggleImportant = (id) => {
console.log('toggle important', id);
};
this.onToggleDone = (id) => {
console.log('toggle done', id);
};
};
render() {
return (
<div className="todo-app">
<AppHeader toDo={ 1 } done={ 3 } />
<div className="top-panel d-flex">
<SearchPanel />
<ItemStatusFilter />
</div>
<TodoList
todos={ this.state.todoData }
onDeleted={ this.deleteItem }
onToggleImportant={ this.onToggleImportant }
onToggleDone={ this.onToggleDone } />
<TodoAddItem onItemAdded={ this.addItem } />
</div>
);
};
};
I'm not 100% sure, but I believe you should do something like this:
describe('should simulate button click', () => {
it('should simulate button click', () => {
const mockedFunction = jest.fn();
const wrapper = shallow(<TodoAddItem onItemAdded={ mockedFunction } />);
wrapper.find('button').simulate('click');
expect(mockedFunction).toHaveBeenCalled();
});
});
You are testing if the onItemAdded function gets called when you click the <TodoAddItem /> component. So you have to mock it first using jest.fn and then check if the mocked function got called after you simulated the click.
For me works replacing the next one:
const setCategories = () => jest.fn();
With this one:
const setCategories = jest.fn();
I suppose that you should to set just jest.fn or jest.fn() in your code.
I'm following some code from a guide and I'm getting that error.
In my App.js I have:
deleteComments = async (commentId) => {
try {
await axios.delete(`/comments/${commentId}`);
const comments = await this.getComments();
this.setState({ comments });
} catch (error) {
console.log(error);
}
};
CommentsList component
const CommentsList = (props) => {
const comments = props.comments.map((comment) => {
return (
<Comment
{...comment}
deleteComments={props.deleteComments}
key={comment.id}
/>
);
});
Comment component where I call the function.
import React from 'react';
const Comment = (props) => {
const deleteComments = () => {
props.deleteComments(props.id);
};
return (
<div>
...
<div>
<button onClick={deleteComments}>Delete</button>
</div>
</div>
);
};
It's quite obvious that CommentList doesn't get deleteComments prop passed. You need to pass it:
<CommentList deleteComments={deleteComments} />