Update element in array of objects that have certain criteria - javascript

I have a an array of chat rooms, each room has a messages array property.
// state
const INITIAL_STATE = {
myRooms: {
rooms: [],
isLoading: false,
isLoaded: false,
isError: false,
},
};
A room and its elements is defined as
{
"creator": {
"username": "LangCodex"
},
"joiner": {
"username": "Bingo"
},
"date_created": "2020-10-04T19:23:01.380918",
"messages": [],
"name": "9496dd0a223f712f4a9d2f3fba4a0ab0",
"status": "offline"
}
A message and its elements is defined as below:
{
"author": {
"username": "Bingo"
},
"recipient": {
"username": "LangCodex"
},
"body": "hello",
"date": "2020-10-07T11:11:25.828269",
"id": 602,
"room": "9496dd0a223f712f4a9d2f3fba4a0ab0",
"seen": false,
"type": "text"
},
My reducer is defined as below and it's receiving data of type String (roomName) by which I select the room whose messages's seen properties are to be set to true.
case 'MARK_CHAT_AS_READ': {
const roomIndex = state.myRooms.rooms.findIndex(r => r.name === action.payload.roomName);
// magic happens here
return {...state}
}
I have been struggling with this one for quite a while now. Any help would be much appreciated.
Edit:
What I am trying to do is to set the 'seen' property of every message of the room (whose index is returned by roomIndex) to true.

Something like this should do the trick
case 'MARK_CHAT_AS_READ': {
return {
...state, // copy state
myRooms: {
...state.myRooms, // copy myRooms
rooms: state.myRooms.rooms.map(room => { // create new rooms array
// keep room untouched if the name is different
if (room.name !== action.payload.roomName) return room;
return { // create new room
...room,
// with new messages array, containing updated values
messages: room.messages.map(message => ({...message, seen: true}))
};
}),
}
}
}
BUT. You should better normalize redux structure and use immer to avoid nested updates.

Related

javascript compare two multidimensional arrays for changes then update the second array with those changes

I have a water monitoring web-page that shows which sprinklers in a list are running, and the amount of remaining time left until they stop/turn off.
I am using an array as a simple state machine to remember data received via web socket from server-side nodejs code. I've got Vue.js on the client-side to reactively watch the list array for changes and update the page.
For simplicity, the arrays looks something like this:
"data": [
{
"id": "1",
"isRunning": false,
"runningEntries": [],
},
{
"id": "2",
"isRunning": true,
"runningEntries": [
{
"timerID": "2",
"remainTime": "1730",
},
{
"timerID": "3",
"remainTime": "550",
}
]
}
]
"runningEntries" may have 0-n entries. The watering hardware I get data from can have overlapping timers with a cumulative effect on the water delivered - in the example above, Sprinkler Id #2 will water it's target with twice the volume for 550 seconds then timer #3 ends but timer #2 keeps the sprinkler going for it's remaining time.
The server periodically sends a new list that may contain updated info somewhere down the line. I could get a duplicate of the last data which causes erratic client countdown timers (Appears to be a problem at the server-side, but I can't fix that at the moment (yet)).
I know how to compare simple arrays ( [1,2,3] !== [1,2,4] ) but I don't have much experience in searching through multi-dimensional arrays in Javascript. I'm thinking good old recursive for() loops or something more modern like .forEach() or some kind of .map()/.filter() combo. Just not sure where to start with multi-dimensional arrays.
socketData.data.forEach( sprinkler =>
sprinkler.runningEntries.forEach( runningEntry => {
if( runningEntry.remainTime does not exist or it's value is less than the matching remainTime entry in the state array) {
overwrite the matching record
or pushing a new one on to the runningEntries stack for that sprinkler
} else {
do nothing/skip to the next one
}
})
)
I can visualize what to do (hence the pseudo-code above) and know how to get down to the data to check, but after that the brain goes mush. Any helpful hints, known similar existing examples to learn from, or pokes in the right direction would be greatly appreciated.
I'll give you a few options. First, I'll answer your original question. Second, I'll give you a suggestion that will make it more performant. Third, I'll offer another option that changes how you store your state.
Option 1
I believe the main issue is that as you iterate over the source array you need to find the matching elements in the state machine array. There are many methods to doing that but the easiest is to simply "do it". Meaning as you find an element to compare, then find the matching element in the other array. Since you have nested data, you'll do that at two levels.
This code will work (but is not performant):
const data = [
{
id: "1",
isRunning: false,
runningEntries: [],
},
{
id: "2",
isRunning: true,
runningEntries: [
{
timerID: "2",
remainTime: "1730",
},
{
timerID: "3",
remainTime: "550",
},
],
},
];
const stateData = [
{
id: "1",
isRunning: false,
runningEntries: [],
},
{
id: "2",
isRunning: false,
runningEntries: [
{
timerID: "2",
remainTime: "1730",
},
],
},
];
function findSprinkler(data, desiredId) {
// For let's us break out early, forEach doesn't
for (const sprinkler of data) {
if (sprinkler.id === desiredId) {
return sprinkler;
}
}
return null;
}
function findTimer(data, desiredId) {
for (const timer of data) {
if (timer.timerID === desiredId) {
return timer;
}
}
return null;
}
for (const sprinkler of data) {
const stateSprinkler = findSprinkler(stateData, sprinkler.id);
if (stateSprinkler) {
console.log("We have a matching sprinkler");
// Do some comparison and/or updates to the sprinkler data
for (const timer of sprinkler.runningEntries) {
const stateTimer = findTimer(stateSprinkler.runningEntries, timer.timerID);
if (stateTimer) {
console.log("We have a matching timer");
// Do some comparison and/or updates to the timer data
}
}
}
}
Option 2
If you are open to it, change your data structure to use object keys instead of array indexes. Your data already has unique keys so then it is as simple as a direct comparison.
Your new data structure would look like:
"data": {
"sprinkler1": {
"id": "1",
"isRunning": false,
"runningEntries": {},
},
"sprinkler2": {
"id": "2",
"isRunning": true,
"runningEntries": {
"timer2": {
"timerID": "2",
"remainTime": "1730",
},
"timer3": {
"timerID": "3",
"remainTime": "550",
}
}
}
}
I left the majority of the data intact because I'm unsure of how it is generated. But in this case you could just iterate over the object keys and do a direct comparison between the two data points (incoming data and your state). This would definitely speed up your processing on the client side but I'm unsure of the required time to generate the proper data from the server perspective - however, if you can offload work to the server, that is always better.
Here is the code:
const data = {
sprinkler1: {
id: "1",
isRunning: false,
runningEntries: {},
},
sprinkler2: {
id: "2",
isRunning: true,
runningEntries: {
timer2: {
timerID: "2",
remainTime: "1730",
},
timer3: {
timerID: "3",
remainTime: "550",
},
},
},
};
const stateData = {
sprinkler1: {
id: "1",
isRunning: false,
runningEntries: {},
},
sprinkler2: {
id: "2",
isRunning: true,
runningEntries: {
timer2: {
timerID: "2",
remainTime: "1730",
},
},
},
};
Object.keys(data).forEach((key) => {
const sprinkler = data[key];
const stateSprinkler = stateData[key];
if (stateSprinkler) {
console.log("We have a matching sprinkler");
// Do some comparison and/or updates to the sprinkler data
Object.keys(sprinkler.runningEntries).forEach((key) => {
const timer = sprinkler.runningEntries[key];
const stateTimer = stateSprinkler.runningEntries[key];
if (stateTimer) {
console.log("We have a matching timer");
// Do some comparison and/or updates to the timer data
}
});
}
});
Option 3
Instead of storing your state in the same structure as the data you receive, simply store your state records in a cache that is keyed by a key that you can build based upon the incoming data. This solution is a hybrid of both solutions above. You still iterate over the incoming data as an array but for each item you iterate over build the cache key and see if that item is in the cache. If so, perform your logic to update it. If not, put it in the cache. The only thing you would have to add is a garbage collection routine that goes through the cache and removes items that are no longer in your incoming data. It seems this would be natural with the data you have but on the surface I'm not sure.

Flatten JSON data

I am trying to use Tabulator to create a list of tickets, The data is imported via AJAX url from the ticket system as a JSON as below.
{
"results": [
{
"cc_emails": [
"ram#freshdesk.com",
"diana#freshdesk.com"
],
"fwd_emails": [],
"reply_cc_emails": [
"ram#freshdesk.com",
"diana#freshdesk.com"
],
"ticket_cc_emails": [
"ram#freshdesk.com",
"diana#freshdesk.com"
],
"fr_escalated": false,
"spam": false,
"email_config_id": null,
"group_id": 35000204315,
"priority": 1,
"requester_id": 35020281588,
"responder_id": 35004154466,
"source": 2,
"company_id": null,
"status": 2,
"subject": "Support Needed...",
"association_type": null,
"to_emails": null,
"product_id": null,
"id": 188261,
"type": null,
"due_by": "2019-09-17T15:12:07Z",
"fr_due_by": "2019-07-01T15:12:07Z",
"is_escalated": false,
"description": "<div>Details about the issue...</div>",
"description_text": "Details about the issue...",
"custom_fields": {
"cf_category": null,
"cf_firstname": null,
"cf_surname": null,
"cf_user_trainging": null,
"cf_email_address": null,
"cf_office_365": null,
"cf_start_date": null,
"cf_permission_level": null,
"cf_hardware_type": null,
"cf_additional_information_specsoftware_etc": null,
"cf_vpn_access_required": false,
"cf_securitydistribution_group_membership": null,
"cf_mapped_network_driveslogin_script": null,
"cf_printers": null,
"cf_phone_extension": null,
"cf_ddi": null,
"cf_phone_group_membership": null,
"cf_user_who_requires_the_equipment": null,
"cf_requirment_date": null,
"cf_correctclosureused": null,
"cf_location": "A1"
},
"created_at": "2019-06-24T15:11:47Z",
"updated_at": "2019-06-24T15:59:00Z",
"associated_tickets_count": null,
"tags": []
}
],
"total": 1
}
The problem is the "custom_fields" is a JSON Object inside the main JSON object, is there a way to flatten this data out and display this as all one row in Tabulator? Any help appreciated?
My current result in Tabulator is it returns [object Object] for the custom_fields column. I would like to be able to see each of custom_fields in the row.
Handling Nested Data
There is no need to flatten the object, Tabulator can handle nested data for columns, if you use dot notation in the field name:
var table = new Tabulator("#example-table", {
columns:[
{title:"Category", field:"custom_fields.cf_category"}, //link column to nested field
],
});
Full details about nested data handling can be found in the Columns Documentation
Column Grouping
If you wanted to you could also use column grouping to show that the fields are a subset of a another property, for example we could define the top level columns as usual and then add column group to hold the custom columns
var table = new Tabulator("#example-table", {
columns:[
{title:"Subject", field:"subject"}, //standard column
{title:"Priorty", field:"priority"}, //standard column
{title:"Custom", columns:[ //column group to hold columns in custom_fields property
{title:"Category", field:"custom_fields.cf_category"},
{title:"First Name", field:"custom_fields.cf_firstname"},
]},
],
});
Full details can be found in the Column Grouping Documentation
If you're using es6+, you could easily achieve this by using rest in object destructuring and object spread.
const input = {
"results": [
{
"custom_fields": {...},
...
}
],
"total": 1
}
const expanded = input.results.map(result => {
const { custom_fields, ...rest } = result;
return { ...rest, ...custom_fields };
})
Here is a slightly different solution relying on function generators to traverse the original object, giving the possibility to eventually detect some duplicated keys.
This example can, of course, be altered by adding further checks (like whether you want to traverse all objects inside the main object and so on).
The current example takes care of:
Traversing the original object by excluding primitives and arrays.
Providing a flattenObject method that accepts an object as an argument and a callback as an eventual second argument that will be raised when a duplicated key is met. In that case, the default behavior is to take the "next" nested value as the new one. If false is returned in the callback, the current value is kept. The callback will provide the key and the value of the new value.
So, in a nutshell, the "real" code to acquire the desired result is this one:
// Case usage:
// Map the existing values.
input.results = input.results.map(i => flattenObject(i, (duplicatedKeyValuePair) => {
return false; // <-- keep the existing value if a duplicate is matched.
}));
console.log(input.results)
Of course, it's slightly more complicated then just flattening the desired property, but I wanted to give a more elastic flavour to it.
const input = {
"results": [
{
"cc_emails": [
"ram#freshdesk.com",
"diana#freshdesk.com"
],
"fwd_emails": [],
"reply_cc_emails": [
"ram#freshdesk.com",
"diana#freshdesk.com"
],
"ticket_cc_emails": [
"ram#freshdesk.com",
"diana#freshdesk.com"
],
"fr_escalated": false,
"spam": false,
"email_config_id": null,
"group_id": 35000204315,
"priority": 1,
"requester_id": 35020281588,
"responder_id": 35004154466,
"source": 2,
"company_id": null,
"status": 2,
"subject": "Support Needed...",
"association_type": null,
"to_emails": null,
"product_id": null,
"id": 188261,
"type": null,
"due_by": "2019-09-17T15:12:07Z",
"fr_due_by": "2019-07-01T15:12:07Z",
"is_escalated": false,
"description": "<div>Details about the issue...</div>",
"description_text": "Details about the issue...",
"test_duplicated_key": "hello! I should keep this!",
"custom_fields": {
"cf_category": null,
"cf_firstname": null,
"cf_surname": null,
"cf_user_trainging": null,
"cf_email_address": null,
"cf_office_365": null,
"cf_start_date": null,
"cf_permission_level": null,
"cf_hardware_type": null,
"cf_additional_information_specsoftware_etc": null,
"cf_vpn_access_required": false,
"cf_securitydistribution_group_membership": null,
"cf_mapped_network_driveslogin_script": null,
"cf_printers": null,
"cf_phone_extension": null,
"cf_ddi": null,
"cf_phone_group_membership": null,
"cf_user_who_requires_the_equipment": null,
"cf_requirment_date": null,
"cf_correctclosureused": null,
"cf_location": "A1",
"test_duplicated_key": "You should not see that."
},
"created_at": "2019-06-24T15:11:47Z",
"updated_at": "2019-06-24T15:59:00Z",
"associated_tickets_count": null,
"tags": []
}
],
"total": 1
}
/**
Traverse every property of the desired object, by returning the currently key-value pair looped. If the value is an object, it keeps traversing.
*/
function* traverseObject(obj) {
for ([key, value] of Object.entries(obj)) {
if (value && typeof(value) === 'object' && !Array.isArray(value)) {
yield* traverseObject(obj[key]);
}
else yield {key: key, value: value};
}
}
/**
Flattens the object by traversing every object inside it.
*/
function flattenObject(obj, onDuplicatedKey) {
let res = {};
for (keyValuePair of traverseObject(obj)) {
let add = true;
if (res.hasOwnProperty(keyValuePair.key)) {
add = onDuplicatedKey ? onDuplicatedKey.call(onDuplicatedKey, keyValuePair) : true; // default behavior: override with nested propeties.
}
if (add) res[keyValuePair.key] = keyValuePair.value;
}
return res;
}
/*
Sample usage.
const flattened = flattenObject(input.results[0], (record) => {
console.log('detected key value pair duplicate. Key:', record.key, ' value: ', record.value);
// true will override the value, false will not override the value.
return false;
});
*/
//console.log(flattened);
// Case usage:
// Map the existing values.
input.results = input.results.map(i => flattenObject(i, (duplicatedKeyValuePair) => {
return false; // <-- keep the existing value if a duplicate is matched.
}));
console.log(input.results);
Please note that the above case is just an example, I didn't spend much time testing every single property type, hence it can (of course) be reviewed and code quality and performances can be improved. It was just an example to show a different approach relying on different operators and logics.
As a (final) side note, I Think you can handle that with tabulator in some way, though I'm not sure you can render multiple columns out of a single property, which leads me to believe that altering the original object is probably the desired solution.

convert nested object to one level up object in javascript

given json : -
{
"_id": "5c1c4b2defb4ab11f801f30d",
"name": "Ray15",
"email": "ray15#gmail.com",
"deviceToken": "dgtssgeegwes",
"deviceType": "IOS",
"tokens": [
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1YzFjNGIyZGVmYjRhYjExZjgwMWYzMGQiLCJhY2Nlc3MiOiJhdXRoIiwiaWF0IjoxNTQ1MzU4MTI2fQ.YdK0MjOm7Lff22uTFITQdic0gKdMZRpsmRee-yejDpQ"
}
]
}
desired json: -
{
"_id": "5c1c4b2defb4ab11f801f30d",
"name": "Ray15",
"email": "ray15#gmail.com",
"deviceToken": "dgtssgeegwes",
"deviceType": "IOS",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1YzFjNGIyZGVmYjRhYjExZjgwMWYzMGQiLCJhY2Nlc3MiOiJhdXRoIiwiaWF0IjoxNTQ1MzU4MTI2fQ.YdK0MjOm7Lff22uTFITQdic0gKdMZRpsmRee-yejDpQ"
}
I want to convert JSON with the help of lodash library of npm in javascript or suggest any other library,
it might be a silly question, Please explain it properly, I am a newbie in javascript and try to learn node.js. comment me if you need more explanation.
Thanks for help
You don't really need a library, you can just assign the property and delete the other.
However tokens is an array, which suggest there might be more than one. This will only take the first one (obj.tokens[0].token). Since objects can't have duplicate keys, you will only be able to have one token with your desired format (if that matters).
let obj = {
"_id": "5c1c4b2defb4ab11f801f30d",
"name": "Ray15",
"email": "ray15#gmail.com",
"deviceToken": "dgtssgeegwes",
"deviceType": "IOS",
"tokens": [
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1YzFjNGIyZGVmYjRhYjExZjgwMWYzMGQiLCJhY2Nlc3MiOiJhdXRoIiwiaWF0IjoxNTQ1MzU4MTI2fQ.YdK0MjOm7Lff22uTFITQdic0gKdMZRpsmRee-yejDpQ"
}
]
}
obj.token = obj.tokens[0].token
delete obj.tokens
console.log(obj)
There are a number of ways to solve this problem and no one "right" way. However, you may want to consider creating a new object, rather than mutating the original object. Objects are always passed by reference in JavaScript and it's easy to accidentally modify an object inside a function, not realizing that you just changed that object everywhere else it's referenced as well.
Since you mentioned it, here is a way to solve this with Lodash.
const obj = {
"_id": "5c1c4b2defb4ab11f801f30d",
"name": "Ray15",
"email": "ray15#gmail.com",
"deviceToken": "dgtssgeegwes",
"deviceType": "IOS",
"tokens": [
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1YzFjNGIyZGVmYjRhYjExZjgwMWYzMGQiLCJhY2Nlc3MiOiJhdXRoIiwiaWF0IjoxNTQ1MzU4MTI2fQ.YdK0MjOm7Lff22uTFITQdic0gKdMZRpsmRee-yejDpQ"
}
]
};
// create a new object without the tokens property
const newObj = _.omit(obj, 'tokens');
// get the first token object from the tokens array
const tokenObj = _.head(obj.tokens);
// get the token string from the token object, defaulting to empty string if not found
newObj.token = _.get(tokenObj, 'token', '');
console.log(newObj);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
Lodash is a great library and used by many projects. It can be especially helpful for new developers. For example, _.head(arr) will return undefined if arr is undefined. However, arr[0] would crash in the same scenario.
Here's one way to solve it without a library.
const obj = {
"_id": "5c1c4b2defb4ab11f801f30d",
"name": "Ray15",
"email": "ray15#gmail.com",
"deviceToken": "dgtssgeegwes",
"deviceType": "IOS",
"tokens": [
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1YzFjNGIyZGVmYjRhYjExZjgwMWYzMGQiLCJhY2Nlc3MiOiJhdXRoIiwiaWF0IjoxNTQ1MzU4MTI2fQ.YdK0MjOm7Lff22uTFITQdic0gKdMZRpsmRee-yejDpQ"
}
]
};
// create a copy of the original object.
// note that Object.assign will make a shallow copy of our object,
// so newObj.tokens will be a pointer to obj.tokens.
// in this instance, we don't care, as we are going to remove newObj.tokens anyway.
const newObj = Object.assign({}, obj);
// throw away the tokens property.
// OK to mutate newObj as we know it is not used anywhere else.
delete newObj.tokens;
// get the first token object from the tokens array.
// the (expectedArray || []) pattern ensures we have an array if obj.tokens is null or undefined.
const tokenObj = (obj.tokens || [])[0];
// get the token string from the token object.
// again, using the (expectedObject || {}) pattern in case tokenObj is null or undefined.
const token = (tokenObj || {}).token;
// create a new property called "token" on our newObj object.
// set it to our token value or an empty string if token is null or undefined.
newObj.token = token || '';
// of course, if you know the tokens array will always have a valid token object,
// you can simply use newObj.token = obj.tokens[0].token.
console.log(newObj);
Using destructuring assignment with "empty" representations of your types works nicely. transform produces a reliable output when tokens contains zero, one, or many { token: ... } values.
const emptyUser =
{ _id: 0, name: "", tokens: [] }
const emptyToken =
{ token: "" }
const toSingleTokenUser =
({ tokens: [ { token } = emptyToken ], ...u } = emptyUser) =>
({ ...u, token })
console .log
( toSingleTokenUser ({ _id: 1, name: "a", tokens: [ { token: "t" } ] })
// { _id: 1, name: "a", token: "t" }
, toSingleTokenUser ({ _id: 1, name: "a", tokens: [] })
// { _id: 1, name: "a", token: "" }
, toSingleTokenUser ({ _id: 1, name: "a", tokens: [ { token: "t1" }, { token: "t2" } ] })
// { _id: 1, name: "a", token: "t1" }
, toSingleTokenUser ({ foo: "bar", tokens: [ { token: "t" } ] })
// { foo: "bar", token: "t" }
)

Using Jest property matchers on arrays of objects

I am attempting to use Jest's new Property Matcher feature (since Jest 23.0.0) to match on an array of objects that contain a generated field. I have tried putting both a plain object and a matcher definition using expect.arrayContaining and expect.objectContaining like I might when matching manually. Is there any way to do this currently?
const sportsBallPeople = [
{
createdAt: new Date(),
name: 'That one famous guy from Cleveland'
},
{
createdAt: new Date(),
name: 'That tall guy'
}
];
expect(sportsBallPeople).toMatchSnapshot(<something goes here>);
Version Info
As is noted in the question, property matchers were introduced in Jest 23.0.0. Note that apps bootstrapped with create-react-app as of today (Aug 5, 2018) are still < 23.
OBJECT
Here is an example using a property matcher for a single object:
test('sportsBallPerson', () => {
expect(sportsBallPeople[0]).toMatchSnapshot({
createdAt: expect.any(Date)
})
});
The snapshot generated:
exports[`sportsBallPerson 1`] = `
Object {
"createdAt": Any<Date>,
"name": "That one famous guy from Cleveland",
}
`;
This will correctly match createdAt to any date and the name to exactly "That one famous guy from Cleveland".
ARRAY
To test an array of objects using property matchers use forEach to loop over the array and snapshot test each object individually:
test('sportsBallPeople', () => {
sportsBallPeople.forEach((sportsBallPerson) => {
expect(sportsBallPerson).toMatchSnapshot({
createdAt: expect.any(Date)
});
});
});
The snapshots generated:
exports[`sportsBallPeople 1`] = `
Object {
"createdAt": Any<Date>,
"name": "That one famous guy from Cleveland",
}
`;
exports[`sportsBallPeople 2`] = `
Object {
"createdAt": Any<Date>,
"name": "That tall guy",
}
`;
forEach ensures that the objects are tested in order, and each object is properly snapshot tested as described above.
Additional Info
It is interesting to note that directly testing an array using property matchers does not work properly and has unexpected side-effects.
My first attempt to test an array was to create the following test:
test('sportsBallPeople as array', () => {
expect(sportsBallPeople).toMatchSnapshot([
{ createdAt: expect.any(Date) },
{ createdAt: expect.any(Date) }
]);
});
It generated the following snapshot:
exports[`sportsBallPeople as array 1`] = `
Array [
Object {
"createdAt": Any<Date>,
},
Object {
"createdAt": Any<Date>,
},
]
`;
This is incorrect since the name properties are missing, but the test still passes (Jest v23.4.2). The test passes even if the names are changed and additional properties are added.
Even more interesting was that as soon as this test executed, any following tests using property matchers were adversely affected. For example, placing this test ahead of the the test looping over the objects changed those snapshots to the following:
exports[`sportsBallPeople 1`] = `
Object {
"createdAt": Any<Date>,
}
`;
exports[`sportsBallPeople 2`] = `
Object {
"createdAt": Any<Date>,
}
`;
In summary, directly passing an array to use with property matchers does not work and can negatively affect other snapshot tests using property matchers.
toMatchObject works for an array too because arrays are objects
expect(receivedArray).toMatchObject(expectedArray)
To apply snapshot property matcher to each array entry without creating a separate assertion for each, we may create an array with length equal to the value's length filled with the matcher definition:
it('should return first 10 notes by default', async () => {
const noteMatcher = {
createdAt: expect.any(String),
updatedAt: expect.any(String),
}
const response = await app.inject({
method: 'GET',
url: `/examples/users/1/notes`,
})
const payload = response.json()
const { length } = payload.data
expect(response.statusCode).toMatchInlineSnapshot(`200`)
expect(length).toBe(10)
expect(payload).toMatchSnapshot({
data: new Array(length).fill(noteMatcher),
})
})
Will result in the following snapshot:
exports[`should return first 10 notes by default 2`] = `
Object {
"data": Array [
Object {
"createdAt": Any<String>,
"deletedAt": null,
"id": 1,
"note": "Note 1",
"title": "Title 1",
"updatedAt": Any<String>,
"userID": 1,
},
Object {
"createdAt": Any<String>,
"deletedAt": null,
"id": 2,
"note": "Note 2",
"title": "Title 2",
"updatedAt": Any<String>,
"userID": 1,
},
// 8 omitted entries
],
"success": true,
}
`;
Create an array of Property Matcher (using Array(n).fill(matcher) for example), of the same size as the result object you want to match (n=sportsBallPeople.length). matcher representing here the Property Matcher of one item of your array.
That way:
It will check each element of the array with the property matcher.
It will create only one snapshot with the full array.
If the result is not the same size as the last snapshot, the test will fail because the snapshot will be different. So it will fail even if the new result is bigger
(others answers may not fail when the array grow if they create one snapshot per item, as new snapshot are usually silently created in CI and doesn't trigger a test failure)
const sportsBallPeople = [
{
createdAt: new Date(),
name: 'That one famous guy from Cleveland'
},
{
createdAt: new Date(),
name: 'That tall guy'
}
];
const itemMatcher = {
createdAt: expect.any(Date),
}
const arrayMatcher = Array(sportsBallPeople.length).fill(itemMatcher)
expect(sportsBallPeople).toMatchSnapshot(arrayMatcher);
or, simply:
expect(sportsBallPeople).toMatchSnapshot(Array(sportsBallPeople.length).fill({
createdAt: expect.any(Date),
}));
Resulting snapshot will be:
exports[`snapshot 1`] = `
Array [
Object {
"createdAt": Any<Date>,
"name": "That one famous guy from Cleveland",
},
Object {
"createdAt": Any<Date>,
"name": "That tall guy",
},
]`
Thanks for the tips.
Often, being a test you can control the inputs making something like the following viable.
describe.only('Something', () => {
it.only('should do something', () => {
const x = {
a: false,
b: true,
c: 157286400,
};
const results = functionBeingTesting(x, 84);
expect(results[0]).toMatchInlineSnapshot({
createdAt: expect.any(Number),
updatedAt: expect.any(Number)
},
`
Object {
"createdAt": Any<Number>,
"a": false,
"b": true,
"updatedAt": Any<Number>,
"value": "0",
}
`,
);
expect(results[1]).toMatchInlineSnapshot({
createdAt: expect.any(Number),
updatedAt: expect.any(Number)
},
`
Object {
"createdAt": Any<Number>,
"a": false,
"b": true,
"updatedAt": Any<Number>,
"value": "1",
}
`,
);
expect(results[2]).toMatchInlineSnapshot({
createdAt: expect.any(Number),
updatedAt: expect.any(Number)
},
`
Object {
"createdAt": Any<Number>,
"a": false,
"b": true,
"updatedAt": Any<Number>,
"value": "1",
}
`,
);
});
});

writing a pure function to update deep field in a map

I'm building a treeview with react/redux. When collapsing/expanding a node, an action is dispatched so that "toggled" field is set to true/false.
The below array is part of my redux state.
Tree:{
"id" : 1,
"name": "Demo",
"toggled": true,
"active": false,
"children": [
{
"id" : 21,
"name": "example",
"active": false,
"toggled": false,
children:[...]
}
}
The below function is not good, it only returns tree with nodes of first levels updated, not the nested ones
function createNodes(children,corporation,type){
if (!Array.isArray(children))
{
children = children ? [children] : [];
}
return children.map(node => {
createNodes(node.children,corporation,type);
if(node.id!==corporation.id){
return ({...node,active:false}: node);
}
return ({...node,toggled:corporation.toggled});
}
Any idea how I could do that efficiently ?
cheers
It's generally recommended that you keep your state as flat as possible. I would try changing your state to a flat array and then updating the children properties to contain lists of ids instead of the children themselves. Something like this:
State
[
{
"id" : 1,
"name": "Demo",
"toggled": true,
"active": false,
"children": [ "21" ]
},
{
"id" : 21,
"name": "example",
"active": false,
"toggled": false,
children:[...]
}
]
Your reducer would then become a trivial list update:
Reducer
function updateCorporation(state, newCorp) {
return state.map(function(corp) {
return corp.id === newCorp.id ? newCorp : corp
})
}
Deep update
However, if you're unable to use a flat state and need to do an update on a deep tree, here's an attempt to fix your original createNodes function. I believe the primary issue is that you're not using recursion to generate the return value, you're just calling the function inside of itself. Try something like this (I've dropped type as you're not using it):
function createNodes(state, corporation) {
if (state.id === corporation.id) {
return { ...state, toggled: corporation.toggled }
} else if (Array.isArray(state.children)) {
const children = state.children.map(function(child){
return createNodes(child, corporation)
})
return { ...state, children }
} else {
return state
}
}

Categories