How to test localStorage with Jest - javascript

Ive mocked localStorage as suggested on another threat but i cant get the tests to work i tried multiple times with no success.
This is the mock
class LocalStorageMock {
constructor() {
this.store = {};
}
clear() {
this.store = {};
}
getItem(key) {
return this.store[key] || null;
}
setItem(key, value) {
this.store[key] = value.toString();
}
removeItem(key) {
delete this.store[key];
}
}
This is the function that im trying to test.
const setToLS = (target, value) => {
localStorage.setItem(target, JSON.stringify(value));
};
const saveToLS = (target, item) => {
let items;
if (localStorage.getItem(target)) {
items = utilities.addItem(JSON.parse(localStorage.getItem(target)), item);
} else {
items = utilities.addItem([], item);
}
setToLS(target, items);
};
This the test that i cant get to work.
describe('utilitiesLS', () => {
describe('saveToLS', () => {
it('should save item to LS')', () => {
const target = 'recipes';
const item = { name: 'salat', ingredients: 'spinach', id: 1 };
utilitiesLS.saveToLS(target, item)
expect(localStorage.store).toMatch( '{"recipes": [{"id": 1, "ingredient": "spinach", "recipeName": "salat"}]}'
)
});
});
});
And this is the error.
expect(string)[.not].toMatch(expected)
string value must be a string.
Received: undefined
29 | const item = { recipeName: 'salat', ingredients: 'spinach', id: 1 };
30 | utilitiesLS.saveToLS(target, item)
> 31 | expect(localStorage.store).toMatch( '{"recipes": [{"id": 1, "ingredient": "spinach", "recipe
Name": "salat"}]}'
| ^
32 | )
33 | });
34 | });

The problem is your test.
LocalStorageMock.store is an object, but your test expect(localStorage.store).toMatch( '{"reci... is testing it to see if it's a string. This is why your test is not passing, because objects do not match strings.
To fix this you should test:
expect(localStorage.store).toEqual({"recipes": [{"id": 1, "ingredient": "spinach", "recipeName": "salat"}]})
Noting that localStorage.store is undefined indicates that you are also not getting the constructed instance of your mock that your test is using.
n.b. If you're trying to mock local storage, consider one of the pre-built, tested and documented approaches such as:
https://www.npmjs.com/package/jest-localstorage-mock

Related

Recursive function only returning first result

I'm trying to implement a file upload feature using native DnD and the browser API.
I have to functions like so:
export function buildFileTree(allObjects) {
let fileTree = {}
allObjects.forEach((item, index) => {
traverseFileTree(fileTree, null, item)
})
return fileTree
}
export function traverseFileTree(fileTree, parent = null, item) {
let parentId = !!parent ? fileTree[parent].id : parent
if (item.isFile) {
item.file(file => {
fileTree[item.fullPath] = {
id: uuidv4(),
name: item.name,
parent_id: parentId,
file: item
}
})
}
if (item.isDirectory) {
fileTree[item.fullPath] = {
id: uuidv4(),
name: item.name,
is_directory: true,
parent_id: parentId,
file: null
}
let dirReader = item.createReader()
dirReader.readEntries(entries => {
entries.forEach((entry, index) => {
traverseFileTree(fileTree, item.fullPath, entry)
})
})
}
}
Which I use like so, and I'm getting very weird results, when I console.log the object, it shows the correct data, but when I try to stringify it, access any other attributes, or iterate over it; it's only showing the first result.
const handleUploadDrop = (files) => {
const finalFiles = buildFileTree(files)
console.log(finalFiles) // This shows the correct data
console.log(JSON.stringify(finalFiles)) // This only shows 1 item!!!
console.log(Object.keys(finalFiles).length) // This prints 1
}
I'm very confused by this and any help would be greatly appreciated.

React extracting a nested json object

How can I extract the 'jobs' object from a nested json list like this:
result:
{
person:
[
{
name: ""
address: ""
jobs: [
{
company:""
},
{
company:""
}
]
}
]
}
Thank you
Write a generic method to extract object properties.
function onExtract(key, data) {
if (isObject(data)) {
for (let item in data) {
if (key === item) {
return data[item];
}
const res = onExtract(key, data[item]);
if (res !== null) return res;
}
}
if (isArray(data)) {
for (let item of data) {
const res = onExtract(key, item);
if (res !== null) return res;
}
}
return null;
}
function isObject(obj) {
return Object.prototype.toString.call(obj) === "[object Object]";
}
function isArray(arr) {
return Object.prototype.toString.call(arr) === "[object Array]";
}
// test
const data = {
person: [
{
name: "",
address: "",
jobs: [
{
company: ""
},
{
company: ""
}
]
}
]
};
console.log(onExtract("jobs", data));
let's say you have a return var that contains this json value
let mappedCompanies = return.person.map(person =>
person.jobs.map(job => job.company)
).flatMap(m => m)
mappedCompanies would contain an array with all the companies names for each one of the registers in "person", all as one array of strings
you can read more about Array.map() here: https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Global_Objects/Array/map
A dynamic way to query the person[] and find jobs, is to use the javascript map() method.
Here is the code without comments.
const personsJobs = (personName, personAddress) => {
const jobs = result.person.map((el) => {
if (el.name === personName && el.address === personAddress) {
return el.jobs;
} else {
return null;
}
})
.filter((el) => el !== null);
return jobs;
};
console.log(personsJobs("wyatt", "1234 test ln"));
Here is the code with comments to explain how the personsJob function works.
// Blow is an ES6 arrow function with the parameters 'personName' and 'personAddress',
// which represents the person in which you are querying for jobs (using both a persons
// name and address so in the case of persons with the same name, you only find the jobs
// of the person you want).
const personsJobs = (personName, personAddress) => {
// Since 'person' is an array, we can use the 'map' method as stated before, which
// will create a new array (jobs) that will store the jobs a specific person has.
const jobs = result.person.map((el) => {
// el stands for the current position in the person array.
// if el's (the current person) name and address values are equal to that of the
// parameters personName and personAddress, then that persons jobs are added to the jobs // array, however, if el does not satisfy the two parameters, null is added to the jobs
// array.
// The array, upon completion, will look something like this: ["programmer", null, null]
if (el.name === personName && el.address === personAddress) {
return el.jobs;
} else {
return null;
}
})
// Finally, the filter method is called to remove all null values so that you will
// only have the persons job in the jobs array.
// After filtering, the array will look like this: ["programmer"]
.filter((el) => el !== null);
return jobs;
};
// Prints the array of wyatt's jobs
console.log(personsJobs("wyatt", "1234 test ln"));
So, following the conclusion of the function, you will have dynamically found the jobs of a specific person.
you can use flatMap function like:
const jobsData = result.person.flatMap(item => item.jobs);
Here is a flexible solution using object-scan
// const objectScan = require('object-scan');
const data = { person: [{ name: '', address: '', jobs: [{ company: '' }, { company: '' }] }] };
console.log(objectScan(['person[*].jobs'], { reverse: false, rtn: 'value' })(data));
// => [ [ { company: '' }, { company: '' } ] ]
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#14.0.0"></script>
Disclaimer: I'm the author of object-scan

track path of recursive function

I'm trying to track path of a deep nested value in json object but having hard time getting the path. Each Item is an array of objects and can have child items. If the object c exists in the json data it is always located in the last item array.
item: [
{
a:5,
item: [
{
item: [
{c:1},
{x:4},
],
...
},
{},
{}
]
},
{},
{}
]
const findPath = (items) => {
let path = []
items.forEach((item,i) => {
if('item' in item){
path = path.concat(findPath(item.item))
}
else if('c' in item) {
path.push(i)
}
})
return path
}
if I have 3 c objects with different item depths, then I would have:
[
[0,0,0], //item[0].item[0].item[0].c
[1,0], //item[1].item[0].c
[4]] , //item[4].c
Any help?
Your main problem here is that you don't track the common case. You store the index only when you found a leaf, but you want all the steps in between. This being recursion, you also have to carry your return values with you, or you end up stepping on them. This works:
objects = [
{},
{
item: [
{},
{},
{
a:5,
item: [
{
item: [
{c:1},
{x:4},
]
},
{},
{}
]
},
{}
]
}
]
const findPath = (items, current_path, matching_paths) => {
items.forEach((item,i) => {
if('item' in item){
current_path.push(i);
current_path = current_path.concat(
findPath(item.item, current_path, matching_paths)
);
}
else if('c' in item) {
current_path.push(i);
matching_paths.push( current_path.slice() );
current_path = [];
}
})
}
var path = [];
var paths = [];
findPath(objects, path, paths);
console.log(paths); //[[1, 2, 0, 0]]
If C is found push a path object to the path array and update that path object for the rest of the paths.
const findPath = (items) => {
let path = []
items.forEach((item,i) => {
if('item' in item){
let item_path = findPath(item.item)
if(item_path.length > 0){
item_path[0].path.push(i)
path.push(item_path[0])
}
}
else if('c' in item){
path.push({path:[i], c:item.c})
}
})
return path
}
The function must be recursive, which means it should call itself with different parameters and not loop forever.
Below is what you are looking for. I made it in TypeScript to make sure I typed it correctly, but just take off all type definitions and it becomes JavaScript:
const trackPath: number[][] = [];
function findPath(topItem: any, path: number[], position: number): void
{
const currentPath = path.slice();
currentPath.push(position);
const newTopItem = topItem['item'];
if (Array.isArray(newTopItem)) {
// here is the recursion for each subitem
newTopItem.forEach((item, i) => findPath(item, currentPath, i));
}
if ('c' in topItem) {
trackPath.push(currentPath);
}
}
// this is the main method to call
function actuallyGetThePath(myTopItem: any): number[][] {
findPath(myTopItem, [], 0);
return trackPath;
}
Good luck!

filter array to product check in record for events

I have an activity feed, it contains a number of different types of activity for our site.
one type of activity is checkin. which logs when a user checks in and checkouts of a site.
The record entries look like so
Entryable_id | Entry_type | Action | timestamp
1 Ticket Update 12:01
3 Ticket New 12:07
4 CheckIn ClockedIn 14:30
4 CheckIn ClockedOut 17:30
What I want to do is create an array with entries in it like so
Entryable_id | ClockedIn| ClockedOut
4 14:30 17:30
so far what I have is
{
let staffCheckins = []
let checkinRecord = []
if (this.DiaryStore.entries.length) {
this.DiaryStore.entries.forEach(function(val) {
if (val.entryable_type === 'CheckIn') {
staffCheckins.push(val);
return val
}
})
}
staffCheckins.forEach(function(val) {
if (val.action === "ClockedIn") {
checkinRecord[val.entryable_id] = {
clockedIn: val.created_at,
user: val.user
}
}
if (val.action === "ClockedOut") {
checkinRecord[val.entryable_id] = {
clockedOut: val.created_at
}
}
})
console.log(completeCheckin)
},
which gives
1: clockedIn: "2019-07-22T10:26:45.000000Z",
2: clockedIn: "2019-07-22T12:38:02.000000Z"
so I assume that it is not appending to the key when i do
checkinRecord[val.entryable_id] = {clockedOut: val.created_at}
On top of that this all feels like a mess. is there a better way to filter and get what I need?
Thanks
You need to merge attribute, instead of assign to new object
staffCheckins.forEach(function(val) {
if (!checkinRecord[val.entryable_id]) {
checkinRecord[val.entryable_id] = {}
}
if (val.action === "ClockedIn") {
checkinRecord[val.entryable_id] = {
...checkinRecord[val.entryable_id],
clockedIn: val.created_at,
user: val.user
}
} else (val.action === "ClockedOut") {
checkinRecord[val.entryable_id] = {
...checkinRecord[val.entryable_id],
clockedOut: val.created_at
}
}
}
so I haven't gotten to test it because I'm out and about but you could try something like this. If they object entryable_id doesnt exist in the current object in the array, then it will create a new object with the members, otherwise it will find the object and update the fields
{
let staffCheckins = [];
let checkinRecord = [];
if (this.DiaryStore.entries.length) {
staffCheckins = this.DiaryStore.filter(val => val.entryable_type.toLowerCase() === 'checkin');
}
staffCheckins.forEach(function(val, i) {
let { action, entryable_id, created_at, user } = val;
if (!entryable_id in checkinRecord[i]) {
checkinRecord[i] = {
clockedIn: created_at,
clockedOut: created_at,
user
}
}
if (action.toLowerCase() === 'clockedin') {
checkinRecord[i] = {
...checkinRecord[i],
clockedIn: created_at,
user
}
} else if (action.toLowerCase() === 'clockedout') {
checkinRecord[i] = {
...checkinRecord[i],
clockedOut: created_at
}
}
});
}
apologies if I understood wrong but I'm also no currently at my actual computer to test any of it
You could do this whole operation in a filter reduce combination and create a groupBy object using the Entryable_id as keys.
Once loop completes get values array of that object
const checkinGroup = data.filter(({Entry_type}) => Entry_type === 'CheckIn')
.reduce((a, c)=>{
let {Entryable_id:id, Action, timestamp} = c;
a[id] = a[id] || {Entryable_id: id, ClockedIn: null, ClockedOut: null};
a[id][Action] = timestamp;
return a;
},{});
const res = Object.values(checkinGroup)
console.log(res)
<script>
const data = [{
Entryable_id: 1,
Entry_type: 'Ticket',
Action: 'Update',
timestamp: '12:01'
},
{
Entryable_id: 3,
Entry_type: 'Ticket',
Action: 'New',
timestamp: '12:07'
},
{
Entryable_id: 4,
Entry_type: 'CheckIn',
Action: 'ClockedIn',
timestamp: '14:30'
},
{
Entryable_id: 4,
Entry_type: 'CheckIn',
Action: 'ClockedOut',
timestamp: '17:30'
}
]
</script>

Accessing JSON by using a constant

My JSON looks as following:
{
flight_number: 1,
mission_name: "FalconSat",
rocket: {
rocket_id: "falcon1",
rocket_name: "Falcon 1",
rocket_type: "Merlin A",
first_stage: {
cores: [
{
core_serial: "Merlin1A",
flight: 1
}
]
},
fairings: {
reused: false,
recovery_attempt: false,
recovered: false,
ship: null
}
},
links: {
video_link: "https://www.youtube.com/watch?v=0a_00nJ_Y88",
flickr_images: [ ]
},
details: "Engine failure at 33 seconds and loss of vehicle"
}
To access the video link, I want to use a constant and I've been trying to use:
const links = "links";
const video1 = links + ".video_link"; // this doesn't work
const video2 = "links.video_link"; // this doesn't work
const det = "details";
getVideoURL(something) {
console.log(something.video1); // undefined
console.log(something.links.video_link); // this works
console.log(something.det); // this works
return something.video2;
}
getDetails(something) {
return something.det; // this works
}
// the jsonObject is retrieved by an async function, i just simplified it. I hope this makes sense.
const jsonObject = await axios("https://api.spacexdata.com/v3/launches");
let url = getVideoURL(jsonObject);
let det = getDetails(something);
console.log(url) // undefined
console.log(det) // prints out details.
Is there a way I can get the video URL like how I've done it with getDetails by using return something.video1 or return something.video2?
Use an array instead of a string, and then you can use reduce to iterate over each key in the array, accessing the appropriate nested property value, passing in the entire object as the initial value of the accumulator a:
const obj = {flight_number:1,mission_name:"FalconSat",rocket:{rocket_id:"falcon1",rocket_name:"Falcon 1",rocket_type:"Merlin A",first_stage:{cores:[{core_serial:"Merlin1A",flight:1}]},fairings:{reused:!1,recovery_attempt:!1,recovered:!1,ship:null}},links:{video_link:"https://www.youtube.com/watch?v=0a_00nJ_Y88",flickr_images:[]},details:"Engine failure at 33 seconds and loss of vehicle"};
const getVideoURL = obj => path.reduce((a, prop) => a[prop], obj);
const path = ['links', 'video_link'];
const url = getVideoURL(obj);
console.log(url);
Alternatively, if you wanted to use a string, you would have to split on .s first:
const pathString = "links.video_link";
const getVideoURL = obj => pathString
.split('.')
.reduce((a, prop) => a[prop], obj);

Categories

Resources