I am struggling to mock the delete function in the lists component.
My test looks like this at the moment
describe("delete a todo", () => {
test("should have todo removed", async () => {
const deleteItem = jest.fn();
const items = [{ id: 1, name: "ana", isComplete: false }];
const wrapper = shallowMount(Todo, items);
console.log(wrapper);
const deleteButton = ".delete";
wrapper.find(deleteButton).trigger("click");
expect(deleteItem).toHaveBeenCalledWith("1");
});
currently, when I run the tests the error reads.
Test Error
The application works fine, but I am not mocking the delete function correctly in my test as a "New Note" is still being passed through. What am I doing wrong?
just in case it helps, here is a part of the file I am testing.
methods: {
addItem() {
if (this.newItem.trim() != "") {
this.items.unshift({
// id: createUID(10),
id: uuid.v4(),
completed: false,
name: this.newItem
});
this.newItem = "";
localStorage.setItem("list", JSON.stringify(this.items));
this.itemsLeft = this.itemsFiltered.length;
}
},
removeItem(item) {
const itemIndex = this.items.indexOf(item);
this.items.splice(itemIndex, 1);
localStorage.setItem("list", JSON.stringify(this.items));
this.itemsLeft = this.itemsFiltered.length;
},
Also for more code, you can get it from the following link :
https://github.com/oliseulean/ToDoApp-VueJS
I think you have to make some changes to your original test case
Change jest.fn() to jest.spyOn(Todo.methods, 'deleteItem') since you have to track calls to methods object in Todo component. Refer: https://jestjs.io/docs/jest-object
Wait for the click event to be triggered with await
Use toHaveBeenCalledTimes not toHaveBeenCalledWith("1")
So your final test case will look like this
describe("delete a todo", () => {
test("should have todo removed", async () => {
const removeItem = jest.spyOn(Todo.methods, 'removeItem')
const items = [{ id: 1, name: "ana", isComplete: false }];
const wrapper = shallowMount(Todo, items)
await wrapper.find('.delete').trigger('click')
expect(removeItem).toHaveBeenCalledTimes(1);
});
});
Related
I'm trying to create an 'object of objects' in ReactJS using state hooks, but I'm unsure how to dynamically create it based on the data coming in.
The data arrives on a websocket, which I have placed in a Context and is being used by the component in question. The JSON data hits the onmessage, it invokes my useEffect state hook to then call a function to update the useState variable accordingly.
The inbound websocket data messages come in one at a time and look something like this (important keys listed, but there lots more props inside them) :
{
"name": "PipelineA",
"state": "succeeded",
"group": "Group1"
}
{
"name": "PipelineE",
"state": "succeeded",
"group": "Group1"
}
{
"name": "PipelineZ",
"state": "succeeded",
"group": "Group4"
}
...where the name and group are the values I want to use to create an 'object of objects'. So the group will be used to create a group of pipelines that are all part of that same group, which within that object, each pipeline will have its name as the 'key' for its entire data. So, the end state of the ‘object of objects’ would look something like this:
{
"Group1": {
"PipelineA": {
"name": "PipelineA",
"state": "running",
"group": "Group1"
},
"PipelineB": {
"name": "PipelineB",
"state": "running",
"group": "Group1"
}
},
"Group2": {
"PipelineC": {
"name": "PipelineC",
"state": "running",
"group": "Group2"
},
"PipelineD": {
"name": "PipelineD",
"state": "running",
"group": "Group2"
}
},
...etc...
}
So the idea being, pipelines of Group1 will be added to the Group1 object, if PipelineA already exists, it just overwrites it, if it does not, it adds it. And so on and so on.
I'm (somewhat) fine with doing this outside of React in plain JS, but I cannot for the life of me figure out how to do it in ReactJS.
const [groupedPipelineObjects, setGroupedPipelineObjects] = useState({});
const [socketState, ready, message, send] = useContext(WebsocketContext);
useEffect(() => {
if (message) {
updatePipelineTypeObjects(message)
}
}, [message]);
const updatePipelineGroupObjects = (data) => {
const pipelineName = data.name
const pipelineGroup = data.group
// let groupObj = {pipelineGroup: {}} // do I need to create it first?
setGroupedPipelineObjects(prevState => ({
...prevState,
[pipelineGroup]: {[pipelineName]: data} // <-- doesnt do what I need
}))
}
And help or suggestions would be appreciated. FYI the pipeline names are unique so no duplicates, hence using them as keys.
Also, why am I doing it this way? I already have it working with just an object of all the pipelines where the pipeline name is the key and its data is the value, which then renders a huge page or expandable table rows. But I need to condense it and have the Groups as the main rows for which I then expand them to reveal the pipelines within. I thought doing this would make it easier to render the components.
It's just that you haven't gone quite far enough. What you have will replace the group entirely, rather than just adding or replacing the relevant pipeline within it. Instead, copy and update the existing group if there is one:
const updatePipelineGroupObjects = (data) => {
const pipelineName = data.name;
const pipelineGroup = data.group;
// let groupObj = {pipelineGroup: {}} // do I need to create it first?
setGroupedPipelineObjects((prevState) => {
const groups = { ...prevState };
if (groups[pipelineGroup]) {
// Update the existing group with this pipeline,
// adding or updating it
groups[pipelineGroup] = {
...groups[pipelineGroup],
[pipelineName]: data,
};
} else {
// Add new group with this pipeline
groups[pipelineGroup] = {
[pipelineName]: data,
};
}
return groups;
});
};
Also, you're trying to use iterable destructuring ([]) here:
const [ socketState, ready, message, send ] = useContext(WebsocketContext);
but as I understand it, your context object is a plain object, not an iterable, so you'd want object destructuring ({}):
const { socketState, ready, message, send } = useContext(WebsocketContext);
Live Example:
const { useState, useEffect, useContext } = React;
const WebsocketContext = React.createContext({ message: null });
const Example = () => {
const [groupedPipelineObjects, setGroupedPipelineObjects] = useState({});
const { socketState, ready, message, send } = useContext(WebsocketContext);
useEffect(() => {
if (message) {
updatePipelineGroupObjects(message);
}
}, [message]);
const updatePipelineGroupObjects = (data) => {
const pipelineName = data.name;
const pipelineGroup = data.group;
// let groupObj = {pipelineGroup: {}} // do I need to create it first?
setGroupedPipelineObjects((prevState) => {
const groups = { ...prevState };
if (groups[pipelineGroup]) {
// Update the existing group with this pipeline,
// adding or updating it
groups[pipelineGroup] = {
...groups[pipelineGroup],
[pipelineName]: data,
};
} else {
// Add new group with this pipeline
groups[pipelineGroup] = {
[pipelineName]: data,
};
}
return groups;
});
};
return <pre>{JSON.stringify(groupedPipelineObjects, null, 4)}</pre>;
};
// Mocked messages from web socket
const messages = [
{
name: "PipelineA",
state: "succeeded",
group: "Group1",
},
{
name: "PipelineB",
state: "running",
group: "Group1",
},
{
name: "PipelineC",
state: "running",
group: "Group2",
},
{
name: "PipelineD",
state: "running",
group: "Group2",
},
{
name: "PipelineE",
state: "succeeded",
group: "Group1",
},
{
name: "PipelineZ",
state: "succeeded",
group: "Group4",
},
];
const App = () => {
const [fakeSocketContext, setFakeSocketContext] = useState({ message: null });
useEffect(() => {
let timer = 0;
let index = 0;
tick();
function tick() {
const message = messages[index];
if (message) {
setFakeSocketContext({ message });
++index;
timer = setTimeout(tick, 800);
}
}
return () => {
clearTimeout(timer);
};
}, []);
return (
<WebsocketContext.Provider value={fakeSocketContext}>
<Example />
</WebsocketContext.Provider>
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
I have a typescript function that receives a 'queryParameter' and then loops over an existing set of queryParameters from a state hook within the component. If it finds a matching parameter name I want it to update the queryParameters object into the const 'newQueryParams'.
note that the issue only occurs with the 'Date' query parameter. All code examples work fine for both other types ('getLatest' and 'fundCode'). Also this is not a question of date object equality
function (only the necessary part):
const handleQueryParameterChange = (queryParameter: DataProductQueryParameter) => {
console.log("existing parameters from state hook: ", queryParameters)
console.log("incoming parameter to function: ", queryParameter)
const newQueryParams = queryParameters.map((param, i) => {
console.log("loop: " + i);
if(queryParameter.name === param.name) {
console.log("match");
param.suggestedValue = queryParameter.suggestedValue;
}
return param;
});
console.log("new query params: ", newQueryParams);
I have logged the various bits to show what happens:
As you can see the incoming parameter tries to change the date to the 21st, but fails, despite a match in the loop.
I have also tried using deep copies:
const deepCopyFunction = (inObject: any) => {
let outObject: any, value, key
if (typeof inObject !== "object" || inObject === null) {
return inObject // Return the value if inObject is not an object
}
// Create an array or object to hold the values
outObject = Array.isArray(inObject) ? [] : {}
for (key in inObject) {
value = inObject[key]
// Recursively (deep) copy for nested objects, including arrays
outObject[key] = deepCopyFunction(value)
}
return outObject
}
const handleQueryParameterChange = (
queryParameter: DataProductQueryParameter
) => {
const queryParametersCopy = deepCopyFunction(queryParameters);
const deepCopyQueryParam = {...queryParameter};
console.log("existing parameters copy: ", queryParametersCopy)
console.log("incoming new parameter: ", queryParameter)
console.log("incoming parameter deep copy: ", deepCopyQueryParam);
const newQueryParams = queryParametersCopy.map((param, i) => {
console.log("loop: " + i);
if(deepCopyQueryParam.name === param.name) {
console.log("match");
param.suggestedValue = deepCopyQueryParam.suggestedValue;
}
return param;
});
console.log("new query params: ", newQueryParams);
which produced the same:
I have also tried just switching out the whole object when i get the match, rather than just editing a property:
const handleQueryParameterChange = (queryParameter: DataProductQueryParameter) => {
console.log("existing parameters: ", queryParameters)
console.log("incoming new parameter: ", queryParameter)
const newQueryParams = queryParameters.map(param => {
return queryParameter.name === param.name ? queryParameter : param;
});
console.log("new query params: ", newQueryParams);
...which didnt work either/ The below screenshot outlines the output, but also note the difference between the headline and expanded object in "incoming new parameter". I've seen this a few times and while stack overflow tells me its the async nature of the console, i can't figure out why its happening in the code.
again please note that the issue only occurs with the 'Date' query parameter. All code examples work fine for both other types ('getLatest' and 'fundCode')
Update
I have implemented one of the suggested improvements, but I'm still not getting the right output.
Here is a little more of the code, including a state hook and an effect hook, with the new implementation:
const [queryParameters, setQueryParameters] = useState<DataProductQueryParameter[]>(dataProductQueryParameters ?? []);
useEffect(() => {
console.log("effect called: ", queryParameters)
}, [queryParameters])
const handleQueryParameterChange = (queryParameter: DataProductQueryParameter) => {
if(queryParameter.name === "getLatest") {
setDatePickerDisabled(!datePickerDisabled);
}
const matchIndex = queryParameters.findIndex(param => queryParameter.name === param.name);
const queryParametersCopy = JSON.parse(JSON.stringify(queryParameters));
if (matchIndex !== -1) {
const suggestedValue = queryParameter.suggestedValue;
queryParametersCopy[matchIndex] = { ...queryParametersCopy[matchIndex], suggestedValue}
}
console.log("new query params: ", queryParametersCopy);
setQueryParameters(queryParametersCopy);
};
The state hook sets successfully on component load. When I invoke the handleChange function, my debugger shows that it updates the new array (newQueryParams) successfully.
The issue arises when setQueryParameters is called to update the state hook. It seems to trigger fine as the useEffect hook gets called. But both my debugger and console show that the suggestedValue field on the updated array doesnt get updated.
Again - this works absolutely fine for the other fields - getLatest and fundCode, so the logic is sound and the hooks are firing for setting new values, but the date one simply won't update.
Thanks
Your initially code works perfectly fine. If you run the following snippet you will see that the final new query params log show that the suggestedValue attribute has been updated successfully:
const queryParameters = [{
name: 'test',
suggestedValue: '2021-03-23'
},
{
name: 'foobar',
suggestedValue: '2022-03-23'
}
]
const handleQueryParameterChange = (queryParameter) => {
console.log("existing parameters from state hook: ", queryParameters)
console.log("incoming parameter to function: ", queryParameter)
const newQueryParams = queryParameters.map((param, i) => {
console.log("loop: " + i);
if (queryParameter.name === param.name) {
console.log("match");
param.suggestedValue = queryParameter.suggestedValue;
}
return param;
});
console.log("new query params: ", newQueryParams);
}
handleQueryParameterChange({
name: 'test',
suggestedValue: '2021-03-21'
})
However you could improve this code to avoid parsing all the elements of the array with .map(). You should better use the .findIndex() method for that. If you run the following snippet you will see that it will only log loop: 0:
const queryParameters = [{
name: 'test',
suggestedValue: '2021-03-23'
},
{
name: 'foobar',
suggestedValue: '2022-03-23'
}
]
const handleQueryParameterChange = (queryParameter) => {
console.log("existing parameters from state hook: ", queryParameters)
console.log("incoming parameter to function: ", queryParameter)
const matchIndex = queryParameters.findIndex((param, i) => {
console.log("loop: " + i);
return queryParameter.name === param.name
})
if (matchIndex !== -1) {
const suggestedValue = queryParameter.suggestedValue
queryParameters[matchIndex] = { ...queryParameters[matchIndex],
suggestedValue
}
}
console.log("new query params: ", queryParameters);
}
handleQueryParameterChange({
name: 'test',
suggestedValue: '2021-03-21'
})
I've tried to reproduce your setup with a small component having 2 states like you showed on your updated question. This code (which reuses your object swap) seems to update queryParameters properly. Could you try to reuse the part with setQueryParameters(prevQueryParameters => ...) to see if it solves your problem?
import React, { FC, Fragment, useCallback, useEffect, useState } from 'react';
interface DataProductQueryParameter {
name: string;
parameterType: string;
format: string;
description: string;
suggestedValue: string;
}
const dataProductQueryParameters: DataProductQueryParameter[] = [
{
name: 'test',
format: '',
description: '',
parameterType: '',
suggestedValue: '',
},
{
name: 'other',
format: '',
description: '',
parameterType: '',
suggestedValue: '',
},
];
export const StackOverflow: FC = () => {
const [datePickerDisabled, setDatePickerDisabled] = useState(false);
const [queryParameters, setQueryParameters] = useState<DataProductQueryParameter[]>(
dataProductQueryParameters,
);
useEffect(() => {
console.log('effect called: ', queryParameters);
}, [queryParameters]);
const handleQueryParameterChange = useCallback(
(queryParameter: DataProductQueryParameter) => {
if (queryParameter.name === 'getLatest') {
setDatePickerDisabled(prevDatePickerDisabled => !prevDatePickerDisabled);
}
setQueryParameters(prevQueryParameters =>
prevQueryParameters.map(param => {
if (param.name === queryParameter.name) {
return queryParameter;
}
return param;
}),
);
},
[setQueryParameters, setDatePickerDisabled],
);
return (
<Fragment>
<button
onClick={() =>
handleQueryParameterChange({
name: 'test',
description: 'updated',
format: 'updated',
parameterType: 'updated',
suggestedValue: 'updated',
})
}
>
Update !
</button>
</Fragment>
);
};
I am attempting to reconfigure a Vue.js Workflowy clone to store list items with Firebase. The clone is based on the following repo: https://github.com/9diov/myflowy. This repo enables the organization of list items into a list tree. List items can be shifted left and right in order to determine their relationship to parent list items. I was successful in implementing an addItem function that adds list items and their levels to Firebase. I am now attempting to implement a method that retrieves items from the database and returns them to the template. So far, I was able to set up a beforeCreate() life cycle hook to retrieve the list item values and levels and push them into the list array. I added console.log(this.list) to confirm that the data is being added to the array. However, the list values and their levels do not render on the screen. How can I beforeCreate() hook to return the value and level stored in Firebase to the tree list?
Here is my component:
<template>
<div id="app">
<span>{{ msg }}</span>
<ul>
<li v-for="(item, index) in list"
v-bind:class="item.level">
<span>•</span>
<input
v-model="item.value"
#keydown.down.prevent="moveDown"
#keydown.up.prevent="moveUp"
#keydown.tab.prevent="shiftRight(index, $event)"
#keydown.shift.tab.prevent="shiftLeft(index, $event)"
#keydown.enter.prevent="addItem(index)"
v-focus="index === focused"
#focus="focused = index"
#blur="focused = null">
</li>
</ul>
</div>
</template>
<script>
import { fblist } from './firebase'
import { focus } from 'vue-focus';
import Vue from 'vue';
const MAX_LEVEL = 10;
export default {
name: 'app',
directives: { focus: focus },
data () {
return {
msg: 'Welcome to Your Vue.js App',
list: [
{ value: 'new item', level: 0 }
],
focused: null
}
},
async created () {
let snapshot = await fblist.get()
const list = []
snapshot.forEach(doc => {
let appData = doc.data()
appData.id = doc.id
// this.list.push(appData)
this.list.push({ value: appData.value, level: appData.level })
console.log(this.list)
})
this.list = list
},
methods: {
moveDown: function() {
this.focused = Math.min(this.focused + 1, this.list.length - 1);
},
moveUp: function () {
this.focused = Math.max(this.focused - 1, 0);
},
shiftLeft: function (index, event) {
let self = this;
self.list[index].level = Math.max(self.list[index].level - 1, 0);
},
shiftRight: function (index, event) {
if (event.shiftKey)
return;
if (index === 0) return;
this.list[index].level = Math.min(this.list[index].level + 1, this.list[index - 1].level + 1, MAX_LEVEL);
},
async addItem (index) {
this.list.splice(index + 1, 0, {value: '', level: this.list[index].level});
this.focused = index + 1;
await fblist.add({
listItem: this.list
})
// this.getData()
}
}
}
</script>
created () hook
async created () {
let snapshot = await fblist.get()
const list = []
snapshot.forEach(doc => {
let appData = doc.data()
appData.id = doc.id
this.list.push({ value: appData.value, level: appData.level })
console.log(list)
})
this.list = list
},
The beforeCreate hook runs at the initialization of your component: data has not been made reactive, and events have not been set up yet. Have a look at https://v2.vuejs.org/v2/api/#beforeCreate and https://v2.vuejs.org/v2/guide/instance.html#Lifecycle-Diagram.
It is quite "standard" to use the created hook for fetching a Firestore database (or any other API). In this hook, you will be able to access reactive data.
So just modify
async beforeCreate () {
//.....
}
to
async created () {
//.....
}
I have the following unit test for my Vue component:
import { shallowMount } from '#vue/test-utils';
import OrganizationChildren from './OrganizationChildren.vue';
describe('OrganizationChildren', () => {
beforeEach(() => {
jest.resetModules();
});
it('passes', () => {
jest.doMock('#/adonis-api', () => {
return {
organization: {
family(id) {
return {
descendants: [],
};
},
},
};
});
const wrapper = shallowMount(OrganizationChildren, {
propsData: {
org: {
id: 1,
},
},
});
});
});
And in the Vue component, it does import { organization } from '#/adonis-api';. I'm temporarily just console.logging the imported organization object, to make sure it's correct. But I can see that it's not using the mocked version that I've specified. What am I doing wrong? My goal is to mock the family method differently in each it() block to test what happens if descendants is empty, if it contains 5 items, 100 items, etc.
Solved! I had a couple issues, as it turns out:
Not properly mocking #/adonis-api. I should mention that it only mocks stuff at the top level, so I had to use a factory function in jest.mock (see below).
I needed an await flushPromises() to allow the template to re-render after its created() method evaluated my mock function and stored the result in this.children.
Full test:
import { shallowMount, config } from '#vue/test-utils';
import flushPromises from 'flush-promises';
import OrganizationChildren from './OrganizationChildren.vue';
import { organization } from '#/adonis-api';
jest.mock('#/adonis-api', () => ({
organization: {
family: jest.fn(),
},
}));
describe('OrganizationChildren', () => {
config.stubs = {
'el-tag': true,
};
it('shows nothing when there are no children', async () => {
organization.family.mockResolvedValueOnce({
descendants: [],
});
const wrapper = shallowMount(OrganizationChildren, {
propsData: {
org: {
id: 1,
},
},
});
await flushPromises();
const h4 = wrapper.find('h4');
expect(h4.exists()).toBe(false);
});
it('shows children when provided', async () => {
organization.family.mockResolvedValueOnce({
descendants: [{ name: 'One' }, { name: 'Two' }],
});
const wrapper = shallowMount(OrganizationChildren, {
propsData: {
org: {
id: 1,
},
},
});
await flushPromises();
const h4 = wrapper.find('h4');
const list = wrapper.findAll('el-tag-stub');
expect(h4.exists()).toBe(true);
expect(list.length).toBe(2);
expect(list.at(0).text()).toBe('One');
expect(list.at(1).text()).toBe('Two');
});
});
I'm getting
Expected value to equal:
[{"id": 1, "text": "Run the tests"}, {"id": 0, "text": "Use Redux"}]
Received:
[{"id": 0, "text": "Use Redux"}, {"id": 1, "text": "Run the tests"}]
I don't really understand on how to make this reducer test pass. I'm referencing various github projects to have a better understanding on testing. I'm not sure what i can do to make the test pass. Here is what i have.
Im testing using jest.
actions/actions.js
let nextTodoId = 0;
export const addPost = text => ({
type: 'ADD_POST',
id: nextTodoId++,
text
})
reducers/myPosts
const initialState = [
{
text: 'Use Redux',
id: 0
}
]
const myPosts = (state = initialState, action) => {
switch(action.type){
case 'ADD_POST':
const post = {
id:state.reduce((maxId, post) => Math.max(post.id, maxId), -1) + 1,
text:action.text,
}
return [...state, post];
default:
return state
}
}
export default myPosts
tests/reducers.js
import { addPost } from '../actions/actions';
import myPosts from '../reducers/myPosts';
import uuid from 'uuid';
describe('myPosts myPosts', () => {
it('should return the initial state', () => {
expect(myPosts(undefined, {})).toEqual([
{
text: 'Use Redux',
id: 0
}
])
})
it('should handle ADD_POST', () => {
expect(
myPosts([], {
type: 'ADD_POST',
text: 'Run the tests'
})
).toEqual([
{
text: 'Run the tests',
id: 0
}
])
expect(
myPosts(
[
{
text: 'Use Redux',
id: 0
}
],
{
type: 'ADD_POST',
text: 'Run the tests',
id:0
}
)
).toEqual([
{
text: 'Run the tests',
id: 1
},
{
text: 'Use Redux',
id: 0
}
])
})
})
The problem is that you're expanding the previous state prior to adding the new post...
change your reducer to this:
return [post, ...state];
The way you wrote it... the new post is placed at the end of the state array. If you want the new post to show up first this will fix the issue.