I need some advice on testing functions in terms of how and what
say I have some state.
state = {
categories: [this is full of objects],
products: [this is also full of objects]
}
then I have this function:
filterProducts = () => {
return this.state.products.filter((product => (
product.categories.some((cat) => (
cat.title == this.state.chosenCategory
))
)))
}
this function filters the products array by working out if the products are part of the selected category.
how would you test this?
I've tried this
let productsStub = [
{id: 1, title: 'wine01', showDescription: false},
{id: 2, title: 'wine02', showDescription: false},
{id: 3, title: 'wine03', showDescription: false}
]
wrapper = shallow(<Menu
categories={categoriesStub}
products={productsStub}
/>);
it('should filter products when searched for', () => {
const input = wrapper.find('input');
input.simulate('change', {
target: { value: '01' }
});
expect(productsStub.length).toEqual(1);
});
what this test (I think) is saying, when I search for 01, I expect the product state (well the stub of the state) to filter and return only 1 result. however the test fails and says expected: 1 received: 3 i.e. the filtering isn't working.
I know I could also do wrapper.instance.filterProducts() but again, I'm not very comfortable on function testing in jest.
any advice? would be great to chat it through with someone
thanks
I replicated your problem statement, but not sure how are you maintaining the state model (props/state). But this might help. :)
Checkout the working example here: https://codesandbox.io/s/6zw0krx15k
import React from "react";
export default class Hello extends React.Component {
state = {
categories: [{ id: 1 }],
products: this.props.products,
selectedCat: 1,
filteredProducts: []
};
filterProducts = value => {
let filteredVal = this.props.products.filter(
product => product.id === parseInt(value)
);
this.setState({
filteredProducts: filteredVal
});
};
setValue = e => {
this.setState({
selectedCat: e.target.value
});
this.filterProducts(e.target.value);
};
render() {
return (
<div>
Filter
<input value={this.state.selectedCat} onChange={this.setValue} />
</div>
);
}
}
import { shallow } from "enzyme";
import Filter from "./Filter";
import React from "react";
let productsStub = [
{ id: 1, title: "wine01", showDescription: false },
{ id: 2, title: "wine02", showDescription: false },
{ id: 3, title: "wine03", showDescription: false }
];
let wrapper = shallow(<Filter products={productsStub} />);
it("should filter products when searched for", () => {
const input = wrapper.find("input");
input.simulate("change", {
target: { value: "1" }
});
expect(wrapper.state().filteredProducts.length).toEqual(1);
});
Related
The addProject function of my code is updating state correctly but the new project is not added to the DOM afterwards. WHY?
What I tried so far:
forceUpdate()
Made a deep copy of the array
Changed the key for the map to be the project title
import React from 'react'
const productBacklog = [
{ id: 1, text: 'FrontEnd'},
{ id: 2, text: 'Finished page - for Cluster'}
];
const parkingLot = [
{ id: 1, text: 'Home page: Google Log In/Github API ---Update: Have Google Cloud account for this'},
{ id: 2, text: 'Screenshots of steps needed to setup test class w/annotation & imports' },
];
const projects = [
{ id: 1, title: 'Parking Lot', children: parkingLot },
{ id: 2, title: 'Product Backlog', children: productBacklog }
];
export default class App extends React.Component {
constructor() {
super();
this.state = {
projects: projects,
}
}
addProject = () => {
const newProject = {
id: Math.round(Math.random() * 1000000),
title: 'newProject',
children: [],
}
const projects = [...this.state.projects];
projects.push(newProject);
this.setState({projects: [...projects]}, () => {
console.log(this.state.projects) // SHOWS THAT STATE IS UPDATED CORRECTLY
})
}
render () {
return (
<div className='App'>
<input type='button' value='Add project' onClick={this.addProject}/>
<div className='projects'>
{projects.map((project, projectId) =>
<div className='Project' key={projectId}>
Project-Title: { project.title}
</div>
)}
</div>
</div>
)
}
}
As I said in the comments, you need to map over this.state.projects. You should carefully name things.. this happened only because you had a global variable called projects (which is synced with the component's state, bad idea in my opinion), otherwise it would've been an obvious error. Also I think functional components are a little bit nicer:
import React, { useState } from 'react';
const productBacklog = [
{ id: 1, text: 'FrontEnd'},
{ id: 2, text: 'Finished page - for Cluster'}
];
const parkingLot = [
{ id: 1, text: 'Home page: Google Log In/Github API ---Update: Have Google Cloud account for this'},
{ id: 2, text: 'Screenshots of steps needed to setup test class w/annotation & imports' },
];
const projects = [
{ id: 1, title: 'Parking Lot', children: parkingLot },
{ id: 2, title: 'Product Backlog', children: productBacklog }
];
export default function App() {
const [state, setState] = useState({projects: projects})
const addProject = () => {
const newProject = {
id: Math.round(Math.random() * 1000000),
title: 'newProject',
children: [],
}
setState({projects: [...state.projects, newProject]})
}
return (
<div className='App'>
<input type='button' value='Add project' onClick={addProject}/>
<div className='projects'>
{state.projects.map((project, projectId) =>
<div className='Project' key={projectId}>
Project-Title: { project.title}
</div>
)}
</div>
</div>
)
}
Comment from Péter Leéh solved the problem: Map over this.state.projects instead of just projects. Thx!
this is my state
this.state = {
notification: [{
from: {
id: someid,
name: somename
},
message: [somemessage]
},
{..},
{..},
]
}
Now if i get a new message from someid, i have to push that new message into message array of someid
I tried to push that message in different ways, but nothing has worked
I tried it in this way, but im not able to push a new message into message array
if (this.state.notification) {
for (let q = 0; q < this.state.notification.length; q++) {
if (
this.state.notification[q] &&
this.state.notification[q].from &&
this.state.notification[q].from.id === r.from.id
) {
this.setState({
notification: [
...this.state.notification[q].messages,
this.state.notification[q].messages.push(r.message),
],
});
return console.log(this.state.notification[q].messages)
}
}
} else {
this.setState({
notification: [{
from: r.from,
messages: [r.message]
}, ]
});
return console.log(JSON.stringify(this.state.notification));
}
First of all, I think structuring your state as a 2d array is not a good idea. But you can try this
const pushMessage = (someId, someMessage) => {
this.setState({
notifications: this.state.notifications.map((notification) => {
if (notification.from.id === someId) {
return {
...notification,
messages: [...notification.messages, someMessage],
};
}
return notification;
}),
});
};
I'm pretty sure you can't do this: this.state.notification[q].messages.push(r.message). You can't mutate your state directly. You should work with a copy o your state, modify it with your code that seems to be ok, and then do the setState(...).
Here is a repro on Stackblitz that works. Here is the code :
import React, { Component } from "react";
import { render } from "react-dom";
import "./style.css";
class App extends Component {
constructor() {
super();
this.state = {
notifications: [
{
from: { id: 0, name: "Quentin" },
message: ["Message 1"]
},
{
from: { id: 1, name: "John" },
message: ["Message 1"]
},
{
from: { id: 2, name: "Henry" },
message: ["Message 1"]
}
]
};
this.pushMessage = this.pushMessage.bind(this);
}
pushMessage (id, message) {
const newState = Object.assign([], this.state);
newState.notifications.forEach(notif => {
if (notif.from.id === id) {
notif.message.push(message);
}
});
this.setState(newState, () => console.log(this.state));
};
render() {
return (
<div>
<button onClick={() => this.pushMessage(1, "Hello World")}>
Push message
</button>
</div>
);
}
}
render(<App />, document.getElementById("root"));
I only handle the push of a message in an existing notification, you already got the other case.
The first argument to setState is an updater function that takes the previous state as an argument. I think you should use this fact to update your state correctly.
Check out this answer https://medium.com/#baphemot/understanding-reactjs-setstate-a4640451865b.
This is a simple TO-DO app in react in which App.js takes data from TodosData.js and the list is shown with the component TodoItem.js. Now the checkboxes and data can be viewed when rendered. But when I click on the checkbox it doesn't work. I tried console logging handleChange function which seems to work. It seems like there maybe problems inside the handleChange function which I can't figure out.
I am following a freecodecamp tutorial on YT and I also checked it several times but can't find the problem here.
App.js code:
import React from 'react';
import TodoItem from './TodoItem'
import todosData from './todosData'
class App extends React.Component {
constructor() {
super ()
this.state = { todos: todosData }
this.handleChange = this.handleChange.bind(this)
}
handleChange(id) {
this.setState(prevState => {
const updatedTodos = prevState.todos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
return todo
})
return {
todos: updatedTodos
}
})
}
render() {
const todoItems = this.state.todos.map(itemEach => <TodoItem key = {itemEach.id} itemEach = {itemEach}
handleChange = {this.handleChange} />)
return (
<div className = "todo-list">
{todoItems}
</div>
)
}
}
export default App;
TodoItem.js code:
import React from 'react';
function TodoItem(props) {
return(
<div className = "todo-item">
<input type = "checkbox"
checked={props.itemEach.completed}
onChange={() => props.handleChange(props.itemEach.id)}
/>
<p>{props.itemEach.text}</p>
</div>
)
}
export default TodoItem
TodosData.js code:
const todosData = [
{
id: 1,
text: "Take out the trash",
completed: true
},
{
id: 2,
text: "Grocery shopping",
completed: false
},
{
id: 3,
text: "Clearn gecko tank",
completed: false
},
{
id: 4,
text: "Mow lawn",
completed: true
},
{
id: 5,
text: "Catch up on Arrested Development",
completed: false
}
]
export default todosData
Is there a better way to implement this? This way of doing checkbox seems quite complicated for a beginner like myself. Also how can I improve this code?
You are mutating the state in your handleChange function. Hence it gets the prevState twice. Once for the original previous state, next for the update that you make inside the handleChange.
You can probably lose the reference, by spreading the todo from state like this.
this.setState(prevState => {
const updatedTodos = prevState.todos.map(todo => {
const resTodo = { ...todo };
if (todo.id === id) {
resTodo.completed = !resTodo.completed;
}
return resTodo;
});
return {
todos: updatedTodos
};
});
Here's a working codesandbox
In state, I have an array of list objects and would like to toggle the displayAddTaskForm. Every time I set state, the list that gets clicked gets rearranged and moves to the front of the lists on the UI. I believe I know why this is happening, just not sure of the solution. When I setState in toggleAddTaskForm, toggledList is first and then the other lists. How do I update the toggledList without changing the order of the lists. Here is the code.
this.state = {
lists: [
{
id: 1,
header: "To Do",
displayAddTaskForm: false,
},
{
id: 2,
header: "Working on It",
displayAddTaskForm: false,
},
{
id: 3,
header: "Taken Care Of",
displayAddTaskForm: false,
}
],
toggleAddTaskForm: (list) => {
const toggledList = {...list, displayAddTaskForm: !list.displayAddTaskForm}
const otherLists = this.state.lists.filter(l => l.id !== list.id)
this.setState({
lists: [toggledList, ...otherLists]
})
},
}
Putting function in the state is not a common thing. I think you need to seperate that function from state.
You can easily toggle items using Array.map() and checking the clicked item id with the item id. This will not change the order of items.
You can use the following code to toggle only one item:
class App extends Component {
constructor() {
super();
this.state = {
lists: [
{
id: 1,
header: "To Do",
displayAddTaskForm: false
},
{
id: 2,
header: "Working on It",
displayAddTaskForm: false
},
{
id: 3,
header: "Taken Care Of",
displayAddTaskForm: false
}
]
};
}
handleClick = id => {
let newList = this.state.lists.map(item => {
if (item.id === id) {
return {
...item,
displayAddTaskForm: !item.displayAddTaskForm
};
} else {
return {
...item,
displayAddTaskForm: false
};
}
});
this.setState({ lists: newList });
};
render() {
return (
<div>
<ul>
{this.state.lists.map(({ id, header, displayAddTaskForm }) => {
return (
<li key={id} onClick={() => this.handleClick(id)}>
{header} - Toggle Value: {displayAddTaskForm ? "true" : "false"}
</li>
);
})}
</ul>
</div>
);
}
}
Playground
Or if you want to be able to toggle every item, you can change the handleClick function like this:
handleClick = id => {
let newList = this.state.lists.map(item => {
if (item.id === id) {
return {
...item,
displayAddTaskForm: !item.displayAddTaskForm
};
} else {
return {
...item
};
}
});
this.setState({ lists: newList });
};
Playground
You could find the index of the list, copy the lists, and insert the modified list into the new lists array at the same index.
toggleAddTaskForm: (list) => {
const toggledList = {...list, displayAddTaskForm: !list.displayAddTaskForm}
const newLists = [...this.state.lists];
newLists[this.state.lists.indexOf(list)] = toggledList;
this.setState({
lists: newLists
})
}
This might help.
lists = [{
id: 1,
header: "To Do",
displayAddTaskForm: false,
}, {
id: 2,
header: "Working on It",
displayAddTaskForm: false,
}, {
id: 3,
header: "Taken Care Of",
displayAddTaskForm: false,
}]
const toggle = (list)=>{
const toggledList = {
...list,
displayAddTaskForm: true
}
const indexOfList = lists.findIndex(({id}) => id === list.id)
const newLists = [...lists]
newLists[indexOfList] = toggledList
setState({
lists: newLists
})
}
const setState = (o) => console.log(o)
toggle(lists[1])
I have something like this on React:
const CheckboxItems = (t) => [ // that t is just a global prop
{
checked: true,
value: 'itemsCancelled',
id: 'checkBoxItemsCancelled',
labelText: t('cancellations.checkBoxItemsCancelled'),
},
{
checked: true,
value: 'requestDate',
id: 'checkboxRequestDate',
labelText: t('cancellations.checkboxRequestDate'),
},
{
checked: true,
value: 'status',
id: 'checkboxStatus',
labelText: t('cancellations.checkboxStatus'),
},
{
checked: true,
value: 'requestedBy',
id: 'checkboxRequestedBy',
labelText: t('cancellations.checkboxRequestedBy'),
},
];
class TableToolbarComp extends React.Component {
state = {
items: CheckboxItems(),
};
onChange = (value, id, event) => {
const { columnsFilterHandler } = this.props;
this.setState(({ items }) => {
const item = items.slice().find(i => i.id === id);
if (item) {
item.checked = !item.checked;
columnsFilterHandler(id, item.value, item.checked);
return { items };
}
});
};
render() {
const { items } = this.state;
return(
<>
{items.map(item => (
<ToolbarOption key={item.id}>
<Checkbox
id={item.id}
labelText={item.labelText}
value={item.value}
checked={item.checked}
onChange={this.onChange}
/>
</ToolbarOption>
))}
</>
)
}
export default compose(
connect(
({ cancellations }) => ({
columnId: cancellations.columnId,
columnValue: cancellations.columnValue,
isChecked: cancellations.isChecked,
}),
dispatch => ({
columnsFilterHandler: (columnId, columnValue, isChecked) => {
dispatch(columnsFilterAction(columnId, columnValue, isChecked));
},
}),
),
)(translate()(TableToolbarComp));
That works very well and it is dispatching the data I would need to use later.
But I have a mess on the Redux part which is changing the state of all of the checkboxes at once and not separately as it should. So, once I uncheck one of the checkboxes the other 3 also get checked: false. I don't see this change to checked: false on the UI, only I see it on the Redux console in the browser.
This is what I have in the reducer
const initialState = {
checkboxes: [
{
checked: true,
value: 'itemsCancelled',
id: 'checkBoxItemsCancelled',
},
{
checked: true,
value: 'requestDate',
id: 'checkboxRequestDate',
},
{
checked: true,
value: 'status',
id: 'checkboxStatus',
},
{
checked: true,
value: 'requestedBy',
id: 'checkboxRequestedBy',
},
],
}
[ActionTypes.COLUMNS_FILTER](state, action) {
return initialState.checkboxes.map(checkbox => {
if (!checkbox.id === action.payload.id) {
return checkbox;
}
return {
...checkbox,
checked: action.payload.isChecked,
};
});
}
Action:
const columnsFilterAction = (columnId, columnValue, isChecked) => ({
type: ActionTypes.COLUMNS_FILTER,
payload: { columnId, columnValue, isChecked },
});
So all I need to know is what I have to do manage the state of those checkboxes on Redux as it working on React. As all I see is that when I toggle the checkboxes all of them reach the same state.
You have !checkbox.id === action.payload.id as your condition logic. As all of your checkbox IDs are 'truthy', then this !checkbox.id evaluates to false, and is the same as writing if(false === action.payload.id).
I suspect you meant to write: if(checkbox.id !== action.payload.id).
What you want to do is pass the id of the checkbox you want to toggle in an action. That's all you need in an action to toggle state. Then in the reducer you want to map over the current state and just return the checkbox for any that don't match the id passed in the action. When the id does match, return a new option spreading the current checkbox's properties into the new object and setting the checked property to it's opposite.
Given this action:
const TOGGLE_CHECKBOX = 'TOGGLE_CHECKBOX'
function toggleCheckbox(id) {
return {
type: TOGGLE_CHECKBOX,
id
}
}
Actions - Redux - Guide to actions and action creators provided by the author of Redux.
This reducer will do the job.
function checkboxReducer(state = [], action = {}) {
switch(action.type) {
case TOGGLE_CHECKBOX:
return state.map(checkbox => {
if (checkbox.id !== action.id) {
return checkbox;
}
return {
...checkbox,
checked: checkbox.isChecked ? false : true,
}
})
default:
return state;
}
}
Reducers - Redux - Guide to reducers and how to handle actions provided by the author of Redux.
Here is a working Code Sandbox to demonstrate it working. You can click the checkboxes to see them toggling as expected.