I am just getting acquainted with testing and I have a little problem. I'm trying to test the functions of a component that change state and call functions of a document. In the documentation of Jest and Enzyme, I did not find the right example. Here is a sample code:
class EditableText extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
isEditing: false,
currentValue: null
};
this.textAreaRef = React.createRef();
}
onClick = () => {
if (!this.state.isEditing) {
this.setState({ isEditing: true });
setTimeout(() => {
this.textAreaRef.current.focus();
}, 100);
document.addEventListener('keydown', this.onKeyDown);
document.addEventListener('click', this.onOutsideClick);
}
};
onOutsideClick = e => {
if (e.target.value !== this.state.currentValue) {
this.closeEditMode();
}
};
closeEditMode() {
this.setState({ isEditing: false });
document.removeEventListener('keydown', this.onKeyDown);
document.removeEventListener('click', this.onOutsideClick);
}
onKeyDown = e => {
if (e.code === 'Enter') {
this.onUpdateValue();
} else if (e.code === 'Escape') {
this.closeEditMode();
}
};
onUpdateValue = () => {
this.props.onSubmit(this.state.currentValue || this.props.value);
this.closeEditMode();
};
render() {
const { isEditing, currentValue } = this.state;
const { value } = this.props;
return isEditing ? (
<TextArea
className="editable-text__textarea"
ref={this.textAreaRef}
onClick={this.onClick}
value={currentValue === null ? value || '' : currentValue}
onChange={(_e, { value }) => this.setState({ currentValue: value })}
/>
) : (
<div className="editable-text__readonly" onClick={this.onClick}>
<div>{this.props.children}</div>
<div className="editable-text__icon-container">
<Icon name="edit" className="editable-text__icon" />
</div>
</div>
);
}
}
How to test functions, that receive and change state and calls document.addEventListener? Please, help.
UPDATE > Tests i already wrote:
describe('tests of component', () => {
test('Component renders correctly with isEditing=false', () => {
const component = renderer
.create(<EditableText children="I'm text in EditableText" />)
.toJSON();
expect(component).toMatchSnapshot();
});
test('Component changes to isEditing=true when clicked on it', () => {
const component = renderer
.create(<EditableText />);
let tree = component.toJSON();
tree.props.onClick();
tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});
describe('tests of logic', () => {
test.only('OnClick should change state and add event listeners', ()=> {
const state = { isEditing: false, currentValue: null }
global.document.addEventListener = jest.fn();
EditableText.onClick(() => {
expect(global.document.addEventListener).toHaveBeenCalled();
});;
});
});
Related
I'm working on some code that uses a checkbox function (I've stripped off a lot of functionality)
export class MyLovelyCheckBox extends React.Component {
constructor(props) {
super(props);
if (this.props.isRequired) this.props.updateType('default');
}
handleChecked(checkbox) {
if (checkbox.checked) {
this.props.updateType(checkbox.value);
} else {
this.props.updateType('');
}
}
render() {
return <div>
<Checkbox
disabled = {
this.props.isRequired
}
checkboxType = 'default'
handleChecked = {
this.handleChecked.bind(this)
}
value = {
this.props.type
}
/>
</div>;
}
}
The Checkbox function is (again stripped back):
export default function Checkbox(props) {
const handleChange = (event) => {
props.handleChecked(event.target);
};
const { checkboxType } = props;
return (
<div>
<input
disabled={props.disabled}
id="thisCheckBox"
value={checkboxType}
checked={checkboxType === props.value}
type="checkbox"
onChange={handleChange}
aria-checked={checkboxType === props.value} />
<label
id="thisCheckBox-label"
htmlFor="thisCheckBox"
>
<Icon icon={checkboxType === props.value ? 'checkbox' : 'checkbox-outline'} size={20} viewBox={'0 0 20 20'}></Icon>
<span>{props.checkboxLabel}</span>
</label>
</div>
);
}
The problem comes from my tests, as I'm trying to make sure that the handleChecked code is covered by tests. To that end, I've written this test:
it('when clicked, updateTypes is called', () => {
const updateType = jest.fn().mockReturnValue(true);
const mountedComponent = create({ updateType }, false, false);
const checkBox = mountedComponent.find('Checkbox');
checkBox.simulate('change', { target: { checked: true } });
expect(updateType).toHaveBeenLastCalledWith(true);
});
Where the utility method create is:
const create = (params, required = {}, shallowlyRender = true) => {
const props = {
type: 'generic',
updateType: () => true,
};
const allProps = { ...props, ...params };
return shallowlyRender
? shallow(<MyLovelyCheckBox {...allProps} />)
: mount(
<Provider store={store}>
<MyLovelyCheckBox {...allProps}/>
</Provider>,
);
};
Despite my best efforts, I cannot get the handleChecked function to be called by the test.
Instead jest outputs:
expect(jest.fn()).toHaveBeenLastCalledWith(...expected)
Expected: true
Received: ""
Number of calls: 0
Where have I gone wrong on this test?
I am converting a button component with MouseEvent<HTMLElement> in React to use Hooks. However, I am now getting an error of RangeError: Maximum call stack size exceeded on each mouse event.
The new code is
const [isActive, setIsActive] = useState(false);
const [isFocus, setIsFocus] = useState(false);
const [isHover, setIsHover] = useState(false);
useEffect(() => {
if (autoFocus && button instanceof HTMLButtonElement) {
button.focus();
}
}, [autoFocus, button]);
const isInteractive = () => !isDisabled && !isLoading;
const onMouseEnter = (e: MouseEvent<HTMLElement>) => {
setIsHover(true);
if (onMouseEnter && !isHover) {
onMouseEnter(e);
}
};
const onMouseLeave = (e: MouseEvent<HTMLElement>) => {
setIsActive(false);
setIsHover(false);
if (onMouseLeave && isHover) {
onMouseLeave(e);
}
};
const onMouseDown = (e: MouseEvent<HTMLElement>) => {
e.preventDefault();
setIsActive(true);
if (onMouseDown && !isActive) {
onMouseDown(e);
}
};
const onMouseUp = (e: MouseEvent<HTMLElement>) => {
setIsActive(true);
if (onMouseUp && !isActive) {
onMouseUp(e);
}
};
const onFocus: FocusEventHandler<HTMLButtonElement> = event => {
setIsFocus(true);
if (onFocus) {
onFocus(event);
}
};
const onBlur: FocusEventHandler<HTMLButtonElement> = event => {
setIsFocus(false);
if (onBlur) {
onBlur(event);
}
};
const onInnerClick: MouseEventHandler<HTMLElement> = e => {
if (!isInteractive()) {
e.stopPropagation();
}
return true;
};
const StyledButton: ReactType = CustomComponent || getElement();
render() {
return (
<StyledButton
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onFocus={onFocus}
onBlur={onBlur}
>
{children}
</StyledButton>
);
};
Whereas the original code is (that works)
state = {
isActive: false,
isFocus: false,
isHover: false,
};
componentDidMount() {
if (this.props.autoFocus && this.button instanceof HTMLButtonElement) {
this.button.focus();
}
}
isInteractive = () => !this.props.isDisabled && !this.props.isLoading;
onMouseEnter = (e: React.MouseEvent<HTMLElement>) => {
this.setState({ isHover: true });
if (this.props.onMouseEnter) {
this.props.onMouseEnter(e);
}
};
onMouseLeave = (e: React.MouseEvent<HTMLElement>) => {
this.setState({ isHover: false, isActive: false });
if (this.props.onMouseLeave) {
this.props.onMouseLeave(e);
}
};
onMouseDown = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
this.setState({ isActive: true });
if (this.props.onMouseDown) {
this.props.onMouseDown(e);
}
};
onMouseUp = (e: React.MouseEvent<HTMLElement>) => {
this.setState({ isActive: false });
if (this.props.onMouseUp) {
this.props.onMouseUp(e);
}
};
onFocus: React.FocusEventHandler<HTMLButtonElement> = event => {
this.setState({ isFocus: true });
if (this.props.onFocus) {
this.props.onFocus(event);
}
};
onBlur: React.FocusEventHandler<HTMLButtonElement> = event => {
this.setState({ isFocus: false });
if (this.props.onBlur) {
this.props.onBlur(event);
}
};
onInnerClick: React.MouseEventHandler<HTMLElement> = e => {
if (!this.isInteractive()) {
e.stopPropagation();
}
return true;
};
const StyledButton: React.ReactType = CustomComponent || this.getElement();
render() {
return (
<StyledButton
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onFocus={onFocus}
onBlur={onBlur}
>
{children}
</StyledButton>
);
};
What have I missed in the new code with hooks that is causing the event to repeat?
I think the issue is your use of useEffect. The two values you have provided for the deps Array in the useEffect is what I believe is causing the issue. The equivalent for componentDidMount which would be called once, is to have an empty array or no array at all.
This would mean changing your useEffect code to as follows
useEffect(() => {
if (autoFocus && button instanceof HTMLButtonElement) {
button.focus();
}
}, []);
The reason for this is if either of those values are changed the useEffect is called again, which would cause re-rendering and reevalation of button and autofocus (I Assume) which would then call useEffect again and thus cause infinite recursion.
More information can be found in the first example here https://reactjs.org/docs/hooks-effect.html#example-using-hooks
Hopefully this helps :D
Good luck!
Note:
I didn't actually run your code, I am just assuming this is the issue
I found a lot of solutions about this problem but none of them work.
I have a view which renders dynamically components depending on the backend response
/**
* Module dependencies
*/
const React = require('react');
const Head = require('react-declarative-head');
const MY_COMPONENTS = {
text: require('../components/fields/Description'),
initiatives: require('../components/fields/Dropdown'),
vuln: require('../components/fields/Dropdown'),
severities: require('../components/fields/Dropdown'),
};
const request = restclient({
timeout: 5000,
baseURL: '/api',
});
const { DropdownItem } = Dropdown;
class CreateView extends React.Component {
constructor(props) {
super(props);
this.state = {
modal: false,
states: props.states,
error: props.error,
spinner: true,
state: props.state,
prevState: '',
components: [],
};
this.handleChange = this.handleChange.bind(this);
this.getRequiredFields = this.getRequiredFields.bind(this);
this.onChangeHandler = this.onChangeHandler.bind(this);
this.changeState = this.changeState.bind(this);
this.loadComponents = this.loadComponents.bind(this);
}
componentDidMount() {
this.loadComponents();
}
onChangeHandler(event, value) {
this.setState((prevState) => {
prevState.prevState = prevState.state;
prevState.state = value;
prevState.spinner = true;
return prevState;
}, () => {
this.getRequiredFields();
});
}
getRequiredFields() {
request.get('/transitions/fields', {
params: {
to: this.state.state,
from: this.state.prevState,
},
})
.then((response) => {
const pComponents = this.state.components.map(c => Object.assign({}, c));
pComponents.forEach((c) => {
c.field.required = 0;
c.field.show = false;
});
response.data.forEach((r) => {
const ob = pComponents.find(c => c.field.name === r.name);
if (ob) {
ob.field.required = r.required;
ob.field.show = true;
}
});
this.setState({
components: pComponents,
fields: response.data,
spinner: false,
});
})
.catch(err => err);
}
loadComponents() {
this.setState((prevState) => {
prevState.components = Object.keys(MY_COMPONENTS).map((k) => {
const field = {
name: k,
required: 0,
show: true,
};
return {
field, component: MY_COMPONENTS[k],
};
});
return prevState;
});
}
handleChange(field, value) {
this.setState((prevState) => {
prevState[field] = value;
return prevState;
});
}
changeState(field, value) {
this.setState((prevState) => {
prevState[`${field}`] = value;
return prevState;
});
}
render() {
const Components = this.state.components;
return (
<Page name="CI" state={this.props} Components={Components}>
<Script src="vendor.js" />
<Card className="">
<div className="">
<div className="">
<Spinner
show={this.state.spinner}
/>
{Components.map((component, i) => {
const Comp = component.component;
return (<Comp
key={i}
value={this.state[component.field.name]}
field={component.field}
handleChange={this.handleChange}
modal={this.state.modal}
changeState={this.changeState}
/>);
})
}
</div>
</div>
</div>
</Card>
</Page>
);
}
}
module.exports = CreateView;
and the dropdown component
const React = require('react');
const request = restclient({
timeout: 5000,
baseURL: '/api',
});
const { DropdownItem } = Dropdown;
class DrpDwn extends React.Component {
constructor(props) {
super(props);
this.state = {
field: props.field,
values: [],
};
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log('state', this.state.field);
console.log('prevState', prevState.field);
console.log('prevProps', prevProps.field);
console.log('props', this.props.field);
}
render() {
const { show } = this.props.field;
return (show && (
<div className="">
<Dropdown
className=""
onChange={(e, v) => this.props.handleChange(this.props.field.name, v)}
label={this.state.field.name.replace(/^./,
str => str.toUpperCase())}
name={this.state.field.name}
type="form"
value={this.props.value}
width={100}
position
>
{this.state.values.map(value => (<DropdownItem
key={value.id}
value={value.name}
primary={value.name.replace(/^./, str => str.toUpperCase())}
/>))
}
</Dropdown>
</div>
));
}
module.exports = DrpDwn;
The code actually works, it hide or show the components correctly but the thing is that i can't do anything inside componentdidupdate because the prevProps prevState and props are always the same.
I think the problem is that I'm mutating always the same object, but I could not find the way to do it.
What I have to do there is to fill the dropdown item.
Ps: The "real" code works, i adapt it in order to post it here.
React state is supposed to be immutable. Since you're mutating state, you break the ability to tell whether the state has changed. In particular, i think this is the main spot causing your problem:
this.setState((prevState) => {
prevState.components = Object.keys(MY_COMPONENTS).map((k) => {
const field = {
name: k,
required: 0,
show: true,
}; return {
field, component: MY_COMPONENTS[k],
};
});
return prevState;
});
You mutate the previous states to changes its components property. Instead, create a new state:
this.setState(prevState => {
const components = Object.keys(MY_COMPONENTS).map((k) => {
const field = {
name: k,
required: 0,
show: true,
};
return {
field, component: MY_COMPONENTS[k],
};
});
return { components }
}
You have an additional place where you're mutating state. I don't know if it's causing your particular problem, but it's worth mentioning anyway:
const pComponents = [].concat(this.state.components);
// const pComponents = [...this.state.components];
pComponents.forEach((c) => {
c.field.required = 0;
c.field.show = false;
});
response.data.forEach((r) => {
const ob = pComponents.find(c => c.field.name === r.name);
if (ob) {
ob.field.required = r.required;
ob.field.show = true;
}
});
You do at make a copy of state.components, but this will only be a shallow copy. The array is a new array, but the objects inside the array are the old objects. So when you set ob.field.required, you are mutating the old state as well as the new.
If you want to change properties in the objects, you need to copy those objects at every level you're making a change. The spread syntax is usually the most succinct way to do this:
let pComponents = this.state.components.map(c => {
return {
...c,
field: {
...c.field,
required: 0,
show: false
}
}
});
response.data.forEach(r => {
const ob = pComponents.find(c => c.field.name === r.name);
if (ob) {
// Here it's ok to mutate, but only because i already did the copying in the code above
ob.field.required = r.required;
ob.field.show = true;
}
})
Expected effect: click button -> call function setEditing() -> call function item() inside setEditing() -> this.state.isEditing changes to true -> in parent this.state.isEdit changes to true. When I call the item () function, the value of isEditing does not change
App
class App extends React.Component {
constructor() {
super();
this.state = {
isEdit = false;
};
}
handleSomething = (value) => {
this.setState(prevState => {
return {
isEdit: value
};
});
}
render() {
return (
<div>
<ul>
{
this.state.todos
.map((todo, index) =>
<Todo
key={index}
index={index}
todo={todo}
handleSomething={this.handleSomething}
/>
)
}
</ul>
</div>
);
}
}
Todo
class Todo extends Component {
state = {
isEditing: false
}
setEditing = () => {
this.setState({
isEditing: !this.state.isEditing
})
this.item();
}
item = () => {
const { isEditing} = this.state;
this.props.handleSomething(isEditing);
}
render() {
return (
<button onClick={() => this.setEditing()}>Edit</button>
)
}
}
You'll need to call this.item after the state was changed, something like
setEditing = () => {
this.setState({
isEditing: !this.state.isEditing
}, this.item)
}
Also, if you want to derive a new state form the old one, you'll have to use something like this:
setEditing = () => {
this.setState(prevState => ({
isEditing: !prevState.isEditing
}), this.item)
}
Try basing your state change on the previous state, and call parent function in a callback :
setEditing = () => {
this.setState(prevState => ({
isEditing: !prevState.isEditing
}), this.item)
}
Because as written in the React doc :
setState() does not always immediately update the component. It may
batch or defer the update until later. This makes reading this.state
right after calling setState() a potential pitfall. Instead, use
componentDidUpdate or a setState callback (setState(updater,
callback)), either of which are guaranteed to fire after the update
has been applied. If you need to set the state based on the previous
state, read about the updater argument below.
(https://reactjs.org/docs/react-component.html#setstate)
class Todo extends React.Component {
state = {
isEditing: false
}
setEditing = () => {
this.setState({
isEditing: !this.state.isEditing
},this.item())
}
item = () => {
const { isEditing} = this.state;
this.props.handleSomething(isEditing);
}
render() {
return (
<button onClick={() => this.setEditing()}>
Edit
</button>
)
}
}
class App extends React.Component {
constructor() {
super();
this.state = {
isEdit : false,
todos : [
"test 1",
"test 2"
]
};
}
handleSomething = (value) => {
this.setState(prevState => {
return {
isEdit: value
};
});
}
render() {
return (
<div>
<ul>
{
this.state.todos
.map((todo, index) =>
<Todo
key={index}
index={index}
todo={todo}
handleSomething={this.handleSomething}
/>
)
}
</ul>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
This is probably related to me not understanding how (very strange) JavaScript works, but I really don't understand why this is the case. I have this piece React/React-select code, almost verbatim from the react-select readme:
class Status extends Component {
constructor(props) {
super(props)
this.state = {
selectedOption: null,
}
this.statusOptions = [];
}
handleChange = (selectedOption) => {
this.setState({ selectedOption });
console.log(`Option selected:`, selectedOption);
}
componentDidMount = () => {
console.log(this.statusOptions);
axios.get(host+'/StatusList')
.then((response) => {
for(var i = 0; i < response.data.status_list.length; i++) {
this.statusOptions.push({value: response.data.status_list[i][0], label: response.data.status_list[i][1]});
}
})
.catch((error) =>{
console.log(error)
})
.then(() =>{
});
}
render() {
const { selectedOption } = this.state;
return (
<Select
value={selectedOption}
onChange={this.handleChange}
options={this.statusOptions}
/>
);
}}
This does not populate my react-select list: However, moving the list/array initialization to componentDidMount makes it work. Why??
class Status extends Component {
constructor(props) {
super(props)
this.state = {
selectedOption: null,
}
}
handleChange = (selectedOption) => {
this.setState({ selectedOption });
console.log(`Option selected:`, selectedOption);
}
componentDidMount = () => {
this.statusOptions = [];
console.log(this.statusOptions);
axios.get(host+'/StatusList')
.then((response) => {
for(var i = 0; i < response.data.status_list.length; i++) {
this.statusOptions.push({value: response.data.status_list[i][0], label: response.data.status_list[i][1]});
}
})
.catch((error) =>{
console.log(error)
})
.then(() =>{
});
}
render() {
const { selectedOption } = this.state;
return (
<Select
value={selectedOption}
onChange={this.handleChange}
options={this.statusOptions}
/>
);
}}
The proper way to do it would be to initialise a statusOptions property in the state of the React component, and then populate it in the Axios call happy path so the setState method triggers a re-render filling up the select component.
class Status extends Component {
constructor(props) {
super(props)
this.state = {
selectedOption: null,
statusOptions: []
}
}
handleChange = (selectedOption) => {
this.setState({ selectedOption });
console.log(`Option selected:`, selectedOption);
}
componentDidMount = () => {
axios.get(host+'/StatusList')
.then((response) => {
const statusOptions = []
for(var i = 0; i < response.data.status_list.length; i++) {
statusOptions.push({value: response.data.status_list[i][0], label: response.data.status_list[i][1]});
}
this.setState({ statusOptions })
})
.catch((error) =>{
console.log(error)
})
}
render() {
const { selectedOption, statusOptions } = this.state;
return (
<Select
value={selectedOption}
onChange={this.handleChange}
options={statusOptions}
/>
);
}
}