Unit test with moment.js - javascript

I'm new to writing unit tests.
My function looks like this:
getData() {
return this.parameters.map(p => {
return {
name: p.name,
items: p.items.map(item => {
const toTime = item.hasOwnProperty('end') ? moment.utc(item.end._d).unix() : null;
const fromTime = item.hasOwnProperty('start') ? moment.utc(item.start._d).unix() : null;
return {
id: item.id,
fromTime: fromTime,
toTime: toTime,
};
}),
};
});
}
and so far my Jasmine test looks like this:
describe('getData()', function() {
it('should return json data', function() {
$ctrl.parameters = [{
name: 'test',
items: [{
id: 1,
fromTime: null,
toTime: null
}, {
id: 13,
fromTime: null,
toTime: null
}]
}];
expect($ctrl.getData()).toEqual([{
name: 'test',
items: [{
id: 1,
fromTime: null,
toTime: null
}, {
id: 13,
fromTime: null,
toTime: null
}]
}]);
});
});
This test is working/passing, but as you can see I am not testing the ternary if/else that uses Moment.js. Basically what the ternary does is check if items contains a property called start / end and if it does, convert that value to a epoch/Unix timestamp and assign it to either toTime or fromTime.
So if items had a property called end with a value of 'Sat Oct 31 2015 00:00:00 GMT+0000 (GMT)', then it would be converted to '1446249600' and assigned to toTime.
How can I write a test for it?

The simplest option is to just construct a couple example dates manually for the input. For example:
$ctrl.parameters = [{
name: 'test',
items: [{
id: 1,
start: moment.utc('2017-01-01T01:00:00'),
end: moment.utc('2017-01-01T06:00:00')
}, {
id: 13,
start: moment.utc('2017-01-02T08:00:00'),
end: null
}]
}];
(Note in the above example, I changed fromTime and toTime to start and end, respectively, since that is what getData is expecting for the input.)
Then figure out their unix timestamps. You can do this part externally - for example, I just opened up the browser developer tools (F12) on the moment.js website, evaluated the following statements in the console, and grabbed the timestamp values:
moment.utc('2017-01-01T01:00:00').unix()
moment.utc('2017-01-01T06:00:00').unix()
moment.utc('2017-01-02T08:00:00').unix()
Finally, back in the unit test, just verify that the timestamps match the expected values:
expect($ctrl.getData()).toEqual([{
name: 'test',
items: [{
id: 1,
fromTime: 1483232400,
toTime: 1483250400
}, {
id: 13,
fromTime: 1483344000,
toTime: null
}]
}]);
Alternatively, if you would rather not have hardcoded timestamps in your unit tests, you can instead store each example date in its own variable (e.g., start1, end1), and then compare to, e.g., start1.unix():
// Arrange
const start1 = moment.utc('2017-01-01T01:00:00');
const end1 = moment.utc('2017-01-01T06:00:00');
const start2 = moment.utc('2017-01-02T08:00:00');
$ctrl.parameters = [{
name: 'test',
items: [{
id: 1,
start: start1,
end: end1
}, {
id: 13,
start: start2,
end: null
}]
}];
// Act
const result = $ctrl.getData();
// Assert
expect(result).toEqual([{
name: 'test',
items: [{
id: 1,
fromTime: start1.unix(),
toTime: end1.unix()
}, {
id: 13,
fromTime: start2.unix(),
toTime: null
}]
}]);
That's perfectly fine, since the unit test is meant to test your code, not moment.js. It's up to you.
Note also that I am using the Arrange-Act-Assert pattern for organizing the test. Again, up to you, but once your unit tests start to get complicated, that tends to keep things easier to follow.
Either way, you will need to change how you compute toTime and fromTime in your getData method, since the code as written will not work if you pass in null for either start or end. In particular, item.hasOwnProperty('start') will return true if you pass in a null value for start, but it will error out because it tries to evaluate item.start._d.
Instead, I recommend changing those 2 lines to the following:
const toTime = item.end ? moment.utc(item.end._d).unix() : null;
const fromTime = item.start ? moment.utc(item.start._d).unix() : null;
I would also advise against using the _d property of the moment object, since that is an internal (private) variable. Since start and end are already moment objects, you may instead be able to just do this:
const toTime = item.end ? item.end.unix() : null;
const fromTime = item.start ? item.start.unix() : null;
A complete jsbin example containing all of the above recommended changes is available here:
https://jsbin.com/xuruwuzive/edit?js,output
Note that some tweaks had to be made so it runs outside the context of AngularJS (make getData a standalone function that accepts its parameters directly, rather than via $ctrl).

Related

Unable to find item in this array when I do it outside of the map function it works inside it doens't

I am trying to make a calendar with events that are rendered dynamically.
I use this to create the days for the calendar and create an array to display. I try to create an events object containing the details for the events. When I try to outside of the initialDays map fuction it works fine and is able to find the object with the correct date and time however when I try to use find function inside of the initialDays map function I get this error.
events.find((item) => {
return item.datetime == dates
})
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'length')
createDays()
{
this.today = startOfToday()
this.formatedDate = format(this.today, 'MMM, yyyy')
this.currentMonth = this.formatedDate
this.getNewDays = parse(this.currentMonth, 'MMM, yyyy', new Date())
this.firstDayCurrentMonth = parse(this.currentMonth, 'MMM, yyyy', new Date())
let initialDays = eachDayOfInterval({
start: startOfWeek(this.getNewDays, {weekStartsOn: 1}),
end: endOfWeek(endOfMonth(this.getNewDays), {weekStartsOn: 1})
})
let events = [{ id: 3, name: 'Date night', time: '6PM', datetime: '2022-10-10', href: '#' }, { id: 4, name: 'Date night', time: '6PM', datetime: '2022-10-12', href: '#' }, { id: 3, name: 'Date night', time: '6PM', datetime: '2022-10-13', href: '#' }]
events = events.map((events) => {
return {
id: events.id,
name: events.name,
time: events.time,
datetime: events.datetime,
href: events.href
}
})
const findevent = events.find((item) => {
return item.datetime === '2022-10-10'
})
console.log(findevent)
initialDays = initialDays.map((dates) => {
return {
date: format(dates, 'yyyy-MM-dd'),
isCurrentMonth: isSameMonth(this.getNewDays, dates),
isToday: isToday(dates),
isSelected: isSameDay(dates, this.today),
events: events.find((item) => {
return item.datetime == dates
})
}
})
this.days = initialDays
},
EDIT ***
InitialDays returns a array of dates from start of the current month to end of current month plus 1 week before and 1 week after.
I want the events to be filled in if they match dates. However currently it just fills every date with the all the events. I thought using the find method would work. Date-fns has a ```isSameDay`` function however it only returns true or false. Not sure how to continue...
{
"date": "2022-10-22",
"isCurrentMonth": true,
"isToday": false,
"isSelected": false,
"events": [
{
"id": 3,
"name": "Date night",
"time": "6PM",
"datetime": "2022-10-10",
"href": "#"
},
{
"id": 4,
"name": "Date night",
"time": "6PM",
"datetime": "2022-10-12",
"href": "#"
},
{
"id": 3,
"name": "Date night",
"time": "6PM",
"datetime": "2022-10-13",
"href": "#"
}
]
}
First, let's see the events you are working with:
let events = [{ id: 3, name: 'Date night', time: '6PM', datetime: '2022-10-10', href: '#' }, { id: 4, name: 'Date night', time: '6PM', datetime: '2022-10-12', href: '#' }, { id: 3, name: 'Date night', time: '6PM', datetime: '2022-10-13', href: '#' }]
The items are objects and
const findevent = events.find((item) => {
return item.datetime === '2022-10-10'
})
is working for you, which proves that this is operational.
In the snippet below we can see that map does not work when you want to call it for an object
let initialDays = {
start: new Date(),
end: new Date()
};
initialDays = initialDays.map((dates) => {
return dates.length
})
therefore eachDayOfInterval returns an array (otherwise you would get an error, complaining that map is not a function). So, your function of eachDayOfInterval does something that you did not share, so it's unclear what this array will contain.
However, it is very safe to assume that the eachDayOfInterval returns an array of days (in whatever format). When you do something like
initialDays.map((dates) => {
//...
}
you therefore will iterate your dates and each iteration is a date (day). So the naming of dates above shows that you misunderstood this part and you assumed that you will have an array of dates inside the map callback, but you will have an individual date inside that callback instead, looping through the whole array.
The error suggests that you tried to read the property of length on undefined. This proves the following:
your dates has at least one undefined element, which is the technical reason you get this error
you treat the individual date as if it was an array of dates
items.map(item => something(item))
will loop items and perform a mapping for each item.

Dynamically split an array into subarrays

const giftcards = [
{
fromuserid: 1,
amount: 10,
date: new Date(),
touserid: 11,
},
{
fromuserid: 1,
amount: 20,
date: new Date(),
touserid: 11,
},
{
fromuserid: 2,
amount: 10,
date: new Date(),
touserid: 11,
},
{
fromuserid: 2,
amount: 10,
date: new Date(),
touserid: 11,
},
{
fromuserid: 3,
amount: 10,
date: new Date(),
touserid: 11,
}
]
I achieved this, which is shown in the useEffect hook:
const giftcards = [
{
fromuserid: 1,
amounts: [{
amount: 10,
date: new Date(),
touserid: 11,
},
{
amount: 20,
date: new Date(),
touserid: 11,
}
]
},
{
fromuserid: 2,
amounts: [{
amount: 10,
date: new Date(),
touserid: 11,
},
{
amount: 10,
date: new Date(),
touserid: 11,
}
]
},
{
fromuserid: 3,
amounts: [{
amount: 10,
date: new Date(),
touserid: 11,
}]
}
]
The solution that was given works except that i would like to make it dynamic.
Meaning, in my app, I allow the user to arrange how the array will be sorted.
For example,
const [sort, setSort] = useState('fromuserid')
const [results, setResults] = useState([])
<div>
<select value={sort} onChange={(e)=> setSort(e.target.value)}>
<option value='fromuserid'>Sort by Buyer</option>
<option value='touserid'>Sort by Receiver</option>
<option value='date'>Sort by Date</option>
</select>
</div>
then in the:
useEffect(() => {
allgiftcards.forEach(({
fromuserid,
date,
from,
giftcardid,
message,
template,
touserid,
amount,
paid
}) => {
map.has(fromuserid) || map.set(fromuserid, {
fromuserid,
cards: []
})
map.get(fromuserid).cards.push({
date,
from,
giftcardid,
message,
template,
touserid,
amount,
paid
})
})
setResults([...map.values()])
}, [sort])
Here is what i mean by dynamic. If the user selected date, I would like for it to look something like:
useEffect(() => {
allgiftcards.forEach(({
fromuserid,
date,
from,
giftcardid,
message,
template,
touserid,
amount,
paid
}) => {
map.has(date) || map.set(date, {
date,
cards: []
})
map.get(date).cards.push({
date,
from,
giftcardid,
message,
template,
touserid,
amount,
paid
})
})
setResults([...map.values()])
}, [sort])
But It seems to me that having a bunch of if and else statements would be bad coding and would create a lot of extra code, so looking for a nice and clean solution
This really isn't a question of React (or where-ever useEffect comes from). This is really a general Javascript question, and it's a problem that suits itself well for solving with functional programming, or at least, with one of the staples of functional programming: reduce
In this case, you could supply the 2nd argument which is the initial value of the accumulator -- in this example an empty object works well. You can choose any key from the data to bucket the results:
// Choose a key that each object in the set has, e.g. 'fromuserid' or 'touserid'
const group_by = 'fromuserid';
let bucketed = giftcards.reduce(function (acc, x) {
let pivot = x[group_by]
let current_vals = (acc.hasOwnProperty(pivot) ? acc[pivot] : []);
current_vals.push(x);
acc[pivot] = current_vals;
return acc
}, {});
console.log(bucketed);
If you really needed to land on the second data structure you shared, you could jostle your initialValue and the exact placement of values into the accumulator, but hopefully this demonstrate the concept of how to dynamically choose how to group the data.
This codepen shows how to dynamically bucket an array of objects based on a specific property. Below is a sample of the callback function you can pass to the reducer -- included in the example.
function groupBy(accum, current, groupKey){
const val = current[groupKey]
const {[groupKey]:_, ...content} = current
if (val in accum){
accum[val].push(content)
}
else accum[val] = [content]
return accum
}

ES6 reduce function affecting array outside of scope

I've rewritten this into a simplified form to demonstrate, I have an array of pickers who have an array of time entries, I'm using reduce to summarise time entries by type on the pickers & then a second reduce to show global entries across both pickers.
The first reduce per picker works as expected.
The second reduce on global time entries works as expected but somehow changes the entries for the first picker ( Sam ).
Sam & John pick the same amount.
Apples 2h, Peaches 2h, Lemons 1h
Is there a better way to write this? Is there a concept I've failed to understand?
function testBug() {
// Reducer Function
function entryReducer(summary, entry) {
// find an index if the types of fruit are the same
let index = summary.findIndex((item) => {
return item.type.id === entry.type.id;
});
if (index === -1) {
summary.push(entry);
} else {
summary[index].hours = summary[index].hours + entry.hours;
}
return summary;
}
let pickers = [
{
id: 1,
identifier: "Sam Smith",
timeEntries: [
{
type: {
id: 1,
name: "Apples",
},
hours: 1,
},
{
type: {
id: 2,
name: "Peaches",
},
hours: 1,
},
{
type: {
id: 3,
name: "Lemons",
},
hours: 1,
},
{
type: {
id: 1,
name: "Apples",
},
hours: 1,
},
{
type: {
id: 2,
name: "Peaches",
},
hours: 1,
},
],
},
{
id: 2,
identifier: "John Snow",
timeEntries: [
{
type: {
id: 1,
name: "Apples",
},
hours: 1,
},
{
type: {
id: 2,
name: "Peaches",
},
hours: 1,
},
{
type: {
id: 3,
name: "Lemons",
},
hours: 1,
},
{
type: {
id: 1,
name: "Apples",
},
hours: 1,
},
{
type: {
id: 2,
name: "Peaches",
},
hours: 1,
},
],
},
];
let pickersSummary = [];
let timeEntriesSummary = [];
for (const picker of pickers) {
if (picker.timeEntries.length > 0) {
// reduce time entries into an array of similar types
picker.timeEntries = picker.timeEntries.reduce(entryReducer, []);
// push to pickers summary arr
pickersSummary.push(picker);
// push time entries to a summary array for later reduce
picker.timeEntries.map((entry) => timeEntriesSummary.push(entry));
}
}
// Reduce time entries for all pickers
// Sam & John pick the same amount
// Apples 2h
// Peaches 2h
// Lemons 1h
// **** If I run this Sam's entries are overwritten with the global time entries ***
timeEntriesSummary = timeEntriesSummary.reduce(entryReducer, []);
const results = { pickersSummary, timeEntriesSummary };
console.log(results);
}
testBug();
module.exports = testBug;
Even though with each reducer you pass a new array [], the actual objects contained by these arrays could be shared. This means when you edit one of the objects in array "A", the objects could also change in array "B".
You know how some languages let you pass variables by value or by reference and how this fundamentally changes how values are handled? JavaScript technically uses call-by-sharing. I suggest reading this other answer: Is JavaScript a pass-by-reference or pass-by-value language?
once an element in an array is pushed into a different array it is separate in memory?
No, it isn't. In JavaScript you will always remember when you made an individual copy of an object (or at least wanted to), because that needs some effort, see What is the most efficient way to deep clone an object in JavaScript? or How do I correctly clone a JavaScript object?
So, just like when you use a=b, push(a) into an array refers the original object. See this example where there is a single object accessible via two variables (x and y), and via both elements of array z. So modifying it as z[1] affects all the others:
let x={a:5};
let y=x;
let z=[x];
z.push(y);
z[1].a=4;
console.log(x);
console.log(y);
console.log(z[0]);
console.log(z[1]);
As your objects are value-like ones and do not have anything what JSON would not support (like member functions), JSON-based cloning can work on them:
function testBug() {
// Reducer Function
function entryReducer(summary, entry) {
// find an index if the types of fruit are the same
let index = summary.findIndex((item) => {
return item.type.id === entry.type.id;
});
if (index === -1) {
//summary.push(entry);
summary.push(JSON.parse(JSON.stringify(entry))); // <--- the only change
} else {
summary[index].hours = summary[index].hours + entry.hours;
}
return summary;
}
let pickers = [
{id: 1, identifier: "Sam Smith", timeEntries: [
{type: {id: 1, name: "Apples",}, hours: 1,},
{type: {id: 2, name: "Peaches",}, hours: 1,},
{type: {id: 3, name: "Lemons",}, hours: 1,},
{type: {id: 1, name: "Apples",}, hours: 1,},
{type: {id: 2, name: "Peaches",}, hours: 1,},],},
{id: 2, identifier: "John Snow", timeEntries: [
{type: {id: 1, name: "Apples",}, hours: 1,},
{type: {id: 2, name: "Peaches",}, hours: 1,},
{type: {id: 3, name: "Lemons",}, hours: 1,},
{type: {id: 1, name: "Apples",}, hours: 1,},
{type: {id: 2, name: "Peaches",}, hours: 1,},],},];
let pickersSummary = [];
let timeEntriesSummary = [];
for (const picker of pickers) {
if (picker.timeEntries.length > 0) {
// reduce time entries into an array of similar types
picker.timeEntries = picker.timeEntries.reduce(entryReducer, []);
// push to pickers summary arr
pickersSummary.push(picker);
// push time entries to a summary array for later reduce
picker.timeEntries.map((entry) => timeEntriesSummary.push(entry));
}
}
// Reduce time entries for all pickers
// Sam & John pick the same amount
// Apples 2h
// Peaches 2h
// Lemons 1h
// **** If I run this Sam's entries are overwritten with the global time entries ***
timeEntriesSummary = timeEntriesSummary.reduce(entryReducer, []);
const results = { pickersSummary, timeEntriesSummary };
console.log(results);
}
testBug();
Now it probably displays what you expected, but in the background it still alters the pickers themselves, you have that picker.timeEntries = ... line running after all. It may be worth mentioning that const something = xy; means that you can not write something = yz; later, something will stick with a given entity. But, if that entity is an object, its internals can still be changed, that happens with picker.timeEntries above (while writing picker = 123; would fail).

Javascript - updating an object property with another value from the same object

I have the following object:
{ id: 1, name: 'jdoe', currentDayHours: null, totalHours: [{ task: 'cleaning', hours: 10}, { task: 'reading', hours: 2 }]}
I am trying to create a function that will update the currentDayHours based on the task parameter passed to the function. So for example, if "cleaning" is passed to the function, the expected outcome of the object should be:
{ id: 1, name: 'jdoe', currentDayHours: 10, totalHours: [{ task: 'cleaning', hours: 10}, { task: 'reading', hours: 2 }]}
I'm still new to javascript but I think I should use foreach and filter, but not sure how to use both with each other. Any guidance would be appreciated!
Direct property access is enough. Use Array.find to find the object.
data = { id: 1, name: 'jdoe', currentDayHours: null, totalHours: [{ task: 'cleaning', hours: 10}, { task: 'reading', hours: 2 }]}
const updateHours = (data,key) => {
data.currentDayHours = (data.totalHours.find(({task})=>task===key)||[]).hours
return data
}
console.log(updateHours(data,'cleaning'))
You can use Array.find() to find the task object in the totalHours array, and then use that object's hours to assign to currentDayHours.
If you want to modify the object in-place, you can do this:
function updateCurrentDayHours(obj, taskName) {
const task = obj.totalHours.find(t => t.task === taskName)
if (task) obj.currentDayHours = task.hours
}
If you want to return a cloned object, you can use a deepClone function provided by some libaries like lodash. Here I'm using JSON.parse() and JSON.stringify() for simplicity.
function updateCurrentDayHours(obj, taskName) {
const task = obj.totalHours.find(t => t.task === taskName)
if (task) {
const clone = JSON.parse(JSON.stringify(obj))
clone.currentDayHours = task.hours
return clone
}
}

console.log not showing expected object properties

I have the following code in javascript in my node.js application.
However certain objects are not stored in my variable appointment. Even if I set them, when I directly access them it works: console.log(appointment.test);
What have I done wrong in this code?
var appointment = {
subscribed: false,
enoughAssis: false,
studentSlotsOpen: false
};
console.log(appointment);
for (var key in appointmentsDB[i]) {
appointment[key] = appointmentsDB[i][key];
}
appointment.test= "res";
console.log(appointment.test);
console.log(appointment);
And here is the produced output:
{ subscribed: false,
enoughAssis: false,
studentSlotsOpen: false }
res
{ comment: 'fsadsf',
room: 'dqfa',
reqAssi: 3,
maxStud: 20,
timeSlot: 8,
week: 31,
year: 2013,
day: 3,
_id: 51f957e1200cb0803f000001,
students: [],
assis: [] }
The variable console.log(appointmentsDB[i]) looks as:
{ comment: 'fsadsf',
room: 'dqfa',
reqAssi: 3,
maxStud: 20,
timeSlot: 8,
week: 31,
year: 2013,
day: 3,
_id: 51f957e1200cb0803f000001,
students: [],
assis: [] }
The following command:
console.log(Object.getOwnPropertyNames(appointmentsDB[i]), Object.getOwnPropertyNames(Object.getPrototypeOf(appointmentsDB[i])));
Shows:
[ '_activePaths',
'_events',
'errors',
'_maxListeners',
'_selected',
'_saveError',
'_posts',
'save',
'_pres',
'_validationError',
'_strictMode',
'isNew',
'_doc',
'_shardval' ] [ 'assis',
'timeSlot',
'db',
'_schema',
'id',
'base',
'day',
'collection',
'reqAssi',
'constructor',
'comment',
'year',
'room',
'students',
'week',
'_id',
'maxStud' ]
However I would expect that my last output also provides the entries test, subscribed, enoughAssis and studentSlotsOpen. What is wrong in this code?
The solution I found was to manually copy the elements I wanted to.
You probably have a Document object instead of a plain object. Those have a custom toJSON method which only yields the properties of your schema and the _id, but nothing else. If you are copying that method with your for-in-loop onto the appointment object, it will get serialized differently as well when logged.
Try
for (var key in appointmentsDB[i].toObject()) {
appointment[key] = appointmentsDB[i][key];
}
appointment.test= "res";
console.log(appointment);

Categories

Resources