How to match called with an array with order?
For example, I have tried, but failed as it ignore order:
expect(fn()).toBeCalledWith(expect.arrayContaining([1,2,3]))
My goal is success when calling with [1,2,3] [1,2,3,4] [1,10,2,7,8,9,3] but not [3,2,1]
The above code give me pass when called with [3,2,1], how can I achieve this goal?
This end up can be done with expect.extend, which I can create my custom matcher.
https://jestjs.io/docs/expect#expectextendmatchers
expect.extend({
arrayContainingWithOrder(received, sample) {
let index = 0;
for (let i = 0; i < received.length; i++) {
if (received[i] === sample[index]) {
index++;
}
}
const pass = index === sample.length;
if (pass) {
return {
message: () =>
`expected ${received} not to be contain ${sample} with order`,
pass: true,
};
} else {
return {
message: () =>
`expected ${received} to be contain ${sample} with order`,
pass: false,
};
}
},
});
You can convert your expected array and called argument to strings like 1,2,3. And then you can use expect.stringContaining instead of expect.arrayContaining. For example
const mockedFunction = jest.fn()
expect(mockedFunction).toHaveBeenCalled() //make sure your mocked function get called
const calledArray = mockedFunction.mock.calls[0][0] //get first argument which is your array
const expectedArray = [1,2,3]
const calledArrayString = calledArray.filter(value => expectedArray.includes(value)).join(",") //only keep contained values
const expectedArrayString = expectedArray.join(",")
expect(calledArrayString).toEqual(expect.stringContaining(expectedArrayString))
Related
I am unable to pass any object or arrays to IPCRenderer.
I am getting error when passing an object or array through ipcs, I have even tried to send by converting to string using JSON.stringify but it converts it into empty object string.
I have tried passing a fileList, an array of object & even an object nothing passes. only string or handwritten objects are working.
I've read that it uses Structured Clone Algorithm and fileList & Array is allowed by this algorithm
ERROR:
electron/js2c/renderer_init.js:74 Uncaught Error: An object could not be cloned.
at EventEmitter.i.send.i.send (electron/js2c/renderer_init.js:74)
at HTMLButtonElement.compressNow (ImageHandling.js:190)
I have tried many possible solutions but nothing worked
code:
const compressNow = () => {
ipcRenderer.send("image:compress", filess). ///This is the error.
// filess is a variable containing an array of selected files from an HTML input.
}
Now i have tried to send filess as JSON.stringify, i tried to send it as an object but nothing works unless i manually write a dummy object or string.
Here's My Github Repo for this project
Files With ErrorJ:-
ImageHandling.js
const fs = window.require('fs');
const {ipcRenderer} = require("electron")
const SELECT = (target) => document.querySelector(`${target}`)
var filess = []
const imgUploadInput = SELECT("#imgUploadInput")
const warning = SELECT("#warning")
const setImgBase64 = (imgEl, file) => {
const ReadAbleFile = fs.readFileSync(file.path).toString('base64')
let src = "data:image/png;base64," + ReadAbleFile
imgEl.setAttribute("src", src)
// El.src=src
// console.log(`FIXED IMAGE # ${imgEl} `,ReadAbleFile)
}
const renderImages = () => {
const files = filess && Array.from(filess)
const defaultImg = SELECT("#defaultImg")
const addImgBtn = SELECT("#addImgBtn")
imgUploadInput.disabled = true;
let numOfFiles = files.length
if (numOfFiles < 1) {
SELECT("#compressContainer").style.visibility = "hidden"
} else {
SELECT("#compressContainer").style.visibility = "visible"
}
if (numOfFiles > 49) {
warning.innerHTML = `<b style="font-weight:bold; color:red;">WARNING:</b><br/>
<span style="padding:10px;text-align:left">
Your processor/computer may not be able to process ${numOfFiles} Images at once, We recommend selecting less than 50 Images at once for better performance.
</span>
`;
}
addImgBtn.innerHTML = `LOADING.....`
if (defaultImg && numOfFiles > 0)
defaultImg.remove();
setTimeout(() => {
if (files && numOfFiles > 0) {
let displayImages = SELECT("#displayImages")
displayImages.innerHTML = ""
files ?. forEach((file, i) => {
let divEl = document.createElement("div")
let imgEl = document.createElement("img")
imgEl.src = file.path
imgEl.id = `PNG_${i}_${
btoa(file.name)
}`
divEl.className = "displayedImg"
imgEl.setAttribute("onclick", `document.getElementById('ImageView').src=this.src`)
const a = document.createElement("a")
a.appendChild(imgEl)
a.setAttribute("href", `#ViewImage`)
a.className = "perfundo__link"
divEl.appendChild(a)
divEl.className = "displayedImg perfundo"
displayImages.appendChild(divEl)
if (i == files.length - 1) {
warning.innerHTML = "";
updateNumOfImages();
}
imgEl.onerror = () => setImgBase64(imgEl, file) // converting to base64 only on error, this make performance better and help us avoid freezes. (before this i was converting all images to base64 wither errored or not that was making computer freez)
})
addImgBtn.innerHTML = "+ Add MORE"
imgUploadInput.disabled = false
findDuplicate()
}
}, 0);
}
const hasDuplicate=()=>{
let FileNames = [... filess.map(f => f.name)]
let duplicateFiles = filess.filter((file, i) => FileNames.indexOf(file.name) !== i)
return {FileNames,duplicateFiles,FilesLength:duplicateFiles.length}
}
const findDuplicate = (forceAlert = false) => {
if (filess && filess.length) {
let {FileNames} = hasDuplicate()
let {duplicateFiles} = hasDuplicate()
if (duplicateFiles.length) { // alert(``)
let countFiles = duplicateFiles.length
let fileStr = countFiles > 1 ? "files" : "file"
console.log("result from removeDup=> ", filess, " \n dupfilename=> ", FileNames, " \n dupfiles=> ", duplicateFiles)
let shouldNotAsk = localStorage.getItem("NeverAsk")
let msg = `You've selected ${
countFiles > 1 ? countFiles : "a"
} duplicate ${fileStr}`
let duplInner = `<span style='color:red'>
<b>WARNING</b>
<p style="margin:0px;line-height:1"> ${msg} . <button onClick="findDuplicate(true)" type="button" class="btn btn-danger btn-rounded btn-sm">REMOVE DUPLICATE</button></p>
</span>`
if (! shouldNotAsk || forceAlert) {
swal("DUPLICATE FILES DETECTED", `${msg} , Would you like to un-select duplicate ${fileStr} having same name?`, {
icon: 'warning',
dangerMode: true,
buttons: {
cancel: true,
...forceAlert ? {} : {
never: "Never Ask"
},
confirm: "Yes !"
}
}).then((Yes) => {
if (Yes == "never") {
localStorage.setItem("NeverAsk", true)
warning.innerHTML=duplInner
} else if (Yes) {
removeDuplicates()
}
})
} else {
warning.innerHTML=duplInner
}
}
}
}
const removeDuplicates = (showAlert=true) => {
let {FileNames} = hasDuplicate()
let {duplicateFiles} = hasDuplicate()
let duplicateFileNames = duplicateFiles.map(f => f.name)
let uniqueFiles = filess.filter((file) => ! duplicateFileNames.includes(file.name))
filess = [
... uniqueFiles,
... duplicateFiles
]
console.log("result from removeDup=> ", filess, " \n filename=> ", FileNames, " \n dupfiles=> ", duplicateFiles, "\n unique fil=> ", uniqueFiles)
renderImages()
if(showAlert){
swal("DONE", "Removed Duplicate Files ", {icon: 'success'}).then(() =>{
renderImages()
setTimeout(() => {
let hasDuplicateFiles = hasDuplicate().FilesLength
if(hasDuplicate){//Re-check if any duplicate files left after the current removal process.
removeDuplicates(false) //Re-run the function to remove remaining. false will make sure that this alert does not show and the loop does not continue.
}
renderImages()
}, 10);
})
}
}
const updateNumOfImages = () => {
warning.innerHTML = `
<span style="text-align:left; color:green">
Selected ${
filess.length
} Image(s)
</span>
`;
}
const compressNow = () => {
ipcRenderer.send("image:compress", filess)
// alert("WOW")
}
CompressBtn.addEventListener("click", compressNow)
imgUploadInput.addEventListener("change", (e) => {
let SelectedFiles = e.target.files
if (SelectedFiles && SelectedFiles.length) {
filess = [
... filess,
... SelectedFiles
]
renderImages()
}
})
// SELECT("#imgUploadInput").addEventListener("drop",(e)=>console.log("DROP=> ",e))
UPDATE:-
I REPLACED THIS:
const compressNow = () => {
ipcRenderer.send("image:compress",filess)
}
INTO THIS:-
const compressNow = () => {
filess.forEach(file => {
ipcRenderer.send("image:compress",file.path )
});
}
Now here i am sending the files one by one via forEach, actually its sending string "file path" so thats how its working i am still confused why do i have to do this? why can't i send whole fileList i assume that this loop method is a bad practice because it will consume more CPU its one additional loop however it won't be necessary if i am able to send the whole array.
See Behavior Changed: Sending non-JS objects over IPC now throws an exception. DOM objects etc. are not serializable. Electron 9.0 (and newer) throws "object could not be cloned" error when unserializable objects are sent.
In your code, File and FileList are DOM objects.
If you want to avoid using forEach, try this code:
const compressNow = () => {
const paths = filess.map(f => f.path);
ipcRenderer.send("image:compress", paths);
}
Can refer to electron github issue tracker for this issue (already closed)
Error: An object could not be cloned #26338
Docs for ipcRenderer.send(channel, ...args)
This issue mainly comes when we have non-cloneable values like function within an object in data we are sending via IPC, to avoid that we can use JSON.stringify() before sending and JSON.parse() later on receiving end, but doing so will cause to lose some of the values eg:
const obj = {
x :10,
foo : ()=>{
console.log('This is non-cloneable value')
}
}
console.log(JSON.stringify(obj))
output:{"x":10}
Instead of sending the images save them in fs and send the path
The simplest thing that could possibly work is to use lodash cloneDeep()
ipcMain.handle('stuffgetList', async () => {
return _.cloneDeep(await stuffStore.getList())
})
in the windows JSON.stringify()
in the main.js JSON.parse()
Remove :compress from. .send method and try
Is there a way to make this if conditions look cleaner and easily to add more Query search in the future as in Open–closed principle?
For example:
if (event.queryParameters["name"]) {
result = await getResultByName(event.queryParameters["name"]);
} else if (event.queryParameters["emailAddress"]) {
result = await getResultByEmail(event.queryParameters["emailAddress"]);
} else if (event.queryParameters["param1"]) {
result = await getResultByParam1(event.queryParameters["param1"]);
} else if (event.queryParameters["something1"] && event.queryParameters["something2"]) {
result = await getResultBySomething(event.queryParameters["something1"], event.queryParameters["something2"]);
}
As you can see it look really messy.
Make a table of entries and use Array.prototype.find():
const lut = [
{ keys: ['name'], getResultBy: getResultByName },
{ keys: ['emailAddress'], getResultBy: getResultByEmail },
{ keys: ['param1'], getResultBy: getResultByParam1 },
{ keys: ['something1', 'something2'], getResultBy: getResultBySomething }
]
const params = event.queryParameters
const entry = lut.find(
({ keys }) => keys.every(key => key in params)
)
if (entry) {
const { keys, getResultBy } = entry
const result = await getResultBy(...keys.map(key => params[key]))
...
}
The problem with the original code is that it isn't DRY, and so any incremental modification will inevitably repeat what was already written.
Compare the following two incremental changes:
...
{ keys: ['fizz', 'buzz', 'fizzbuzz'], getResultBy: getResultByFizzBuzz }
...
else if (params.fizz && params.buzz && params.fizzbuzz) {
result = await getResultByFizzBuzz(params.fizz, params.buzz, params.fizzbuzz);
}
And tell me which one you'd rather be typing every time you go back and add a new function.
Since values are different and functions are different, there's not much place for improvement.
There's no necessity for bracket notation and there's no reason to reference event object every time.
It could be written as:
const { queryParameters } = event;
if (queryParameters.name) {
result = await getResultByName(queryParameters.name);
} else if ...
No other improvements can be made, unless the same case occurs in several places and could be DRYed up:
const paramHandlers = [
{ handler: getResultByName, paramNames: ['name'] },
...
];
Then paramHandlers can be iterated to check if paramNames match event.queryParameters properties.
So what you have looks perfectly readable, simple, and clean. You could create an event handler list if you need more flexibility:
eventHandlers = [nameHandler, emailHandler, ...];
var result;
for (var handler of eventHandlers) {
if (result = handler(event)) break;
}
In this example, the event handlers are functions that return a result if the event was consumed and processing should end. In your case your result can be a Promise or any arbitrary value.
I declared an array, But when I push elements inside it, it remains Empty. Here's my Code :
var catsObjectId = new Array();
var data = new Array();
Recipe.find((err,doc3)=> {
data = doc3;
for (var i = 0; i < data.length; i++) {
catsObjectId.push([]);
data[i]['categories'].forEach((item, index) => {
Recipecat.findOne({_id: item}, (err,result)=> {
item = result.name;
catsObjectId.push(item);
});
})
}
console.log(catsObjectId);
});
Here's the Recipe schema :
var recipeSchema = Schema({
categories: [{
type: Schema.Types.ObjectId,
ref: 'RecipeCat',
}]
});
and Here's the Recipecat schema :
var recipecatSchema = new Schema({
name: {
type: String,
required: true
}
});
I want to replace objectIds for recipeCats with their names.
When I log 'catsObjectId', It shows an empty array.
What Seems to be the Problem?
Thanks In advance!
(I understand this question is a bit old, but if you still need help)
That's because you're pushing to an array which is outside the callback and the async nature of JavaScript kicking in.
Here's simple explanation why it's empty
var catsObjectId = new Array();
var data = new Array();
Recipe.find((err,doc3)=> {
// say execution 1
for (var i = 0; i < data.length; i++) {
catsObjectId.push([]);
data[i]['categories'].forEach((item, index) => {
// say execution 2
Recipecat.findOne({_id: item}, (err,result)=> {
item = result.name;
catsObjectId.push(item);
});
})
}
// say execution 3
console.log(catsObjectId);
});
First execution 1 is executed. Within this forEach iterates over each item and fires execution 2. Then continues to execute execution 3.
The problem is execution 2 is asynchronous and the value is returned sometime in the future. This future is after excution 3 is executed. When Recipecat.findOne finishes execution, the callback within then(result.. is called. But console.log(catsObjectId) is already executed and catsObjectId was empty at the time of execution.
You should either use catsObjectId within the callback .then((data) => // use data here) or use the async/await to make it sync like.
Note await is only valid inside async function
async function getSomeNames() {
try {
const data = await Recipe.find();
// docs is an array of promises
const docs = data.map((item, index) => {
Recipecat.findOne({_id: item})
});
// items is an array of documents returned by findOne
const items = await Promise.all(docs);
// now you can map and get the names
const names = items.map(item => item.name);
} catch (e) {
// handle error
console.error(e);
}
}
getSomeNames()
Your pushing an empty array every time it goes through the for loop. Try deleting this line.
catsObjectId.push([]);
You have to use promises in order to control your code. Try the following code and tell me if an error exists.
Recipe.find().then(doc3 => {
data = doc3;
for (var i = 0; i < data.length; i++) {
data[i]['categories'].forEach((item, index) => {
Recipecat.findOne({_id: item}).then(result => {
item = result.name;
catsObjectId.push(item);
});
})
}
console.log(catsObjectId);
})
.catch(err => {
console.log(err);
});
Recently ran into a similar problem. Fix for me was to replace the forEach loop with a simple for loop. It turned out, that the forEach loop is not bothering about async-await, but the for loop is.
Here is my code snippet:
let orders = await order_db.find({ profileID: req.body.id }).exec();
let final_orders = [];
for(let i=0; i<orders.length; i++){
let order = orders[i];
if (order.shopID != null) {
let shop = await shop_db.find({ _id: order.shopID }).exec();
let shopName = shop[0].name;
let shopEmail = shop[0].email;
let shopAddress = shop[0].address;
let shopPhone = shop[0].phone;
let updated_order = { ...order._doc, shopName, shopEmail, shopAddress, shopPhone };
final_orders.push(updated_order);
}
else {
let shopName = "";
let shopEmail = "";
let shopPhone = "";
let shopAddress = "";
let updated_order = { ...order._doc, shopName, shopEmail, shopAddress, shopPhone };
final_orders.push(updated_order);
}
};
I am building a vue app that will search YouTube channels based on which options are selected.
When the option is TRUE, I push that string into an array which holds the URLs of the axios.get() requests.
I am looping through that array and running axios.get() and returning the value. I am getting a response under Promise{} with and object inside [[PromiseValue]].
At the end I am combining the responses into a vue data element(catAndDogResults) but I am getting an undefined in the end.
Is there a better way to do what I am trying to do?
data () {
return {
catVideos: true,
dogVideos: true,
catResults: [],
dogResults: [],
catAndDogResults: []
}
},
methods:
search: function() {
var that = this
var cats = 'https://www.googleapis.com/youtube/v3/search?part=snippet&q=cats'
var dogs = 'https://www.googleapis.com/youtube/v3/search?part=snippet&q=dogs'
var urls = []
if (this.catVideos == true) {
urls.push(cats)
}
if (this.dogVideos == true) {
urls.push(dogs)
}
function getVideos() {
for (var i = 0; i < urls.length; i++) {
console.log(axios.get(urls[i])) // returns under [[PromiseValue]]:object
return axios.get(urls[i])
}
}
axios.all([
getVideos()
])
.then(axios.spread(function (catRes, dogRes) {
that.catResults = catRes.data.items
that.dogResults = dogRes.data.items
that.catAndDogResults = that.catResults.concat(that.dogResults)
}))
}
EDITS
Fixed spelling mistakes
Try changing your getVideos() method to return the array after the for loop:
function getVideos() {
var requests = [];
for (var i = 0; i < urls.length; i++) {
requests.push(axios.get(urls[i]));
}
return requests;
}
And then, call it like that:
axios.all(getVideos())
.then(function (catRes, dogRes) { ... })
The answer provided by #tiagodws should fix the issue. I just wanted to rewrite the getVideos function, you could write it using ES6 syntax like the following:
var getVideos = () => urls.map(url => axios.get(url))
I'm just starting to play around with Puppeteer (Headless Chrome) and Nodejs. I'm scraping some test sites, and things work great when all the values are present, but if the value is missing I get an error like:
Cannot read property 'src' of null (so in the code below, the first two passes might have all values, but the third pass, there is no picture, so it just errors out).
Before I was using if(!picture) continue; but I think it's not working now because of the for loop.
Any help would be greatly appreciated, thanks!
for (let i = 1; i <= 3; i++) {
//...Getting to correct page and scraping it three times
const result = await page.evaluate(() => {
let title = document.querySelector('h1').innerText;
let article = document.querySelector('.c-entry-content').innerText;
let picture = document.querySelector('.c-picture img').src;
if (!document.querySelector('.c-picture img').src) {
let picture = 'No Link'; } //throws error
let source = "The Verge";
let categories = "Tech";
if (!picture)
continue; //throws error
return {
title,
article,
picture,
source,
categories
}
});
}
let picture = document.querySelector('.c-picture img').src;
if (!document.querySelector('.c-picture img').src) {
let picture = 'No Link'; } //throws error
If there is no picture, then document.querySelector() returns null, which does not have a src property. You need to check that your query found an element before trying to read the src property.
Moving the null-check to the top of the function has the added benefit of saving unnecessary calculations when you are just going to bail out anyway.
async function scrape3() {
// ...
for (let i = 1; i <= 3; i++) {
//...Getting to correct page and scraping it three times
const result = await page.evaluate(() => {
const pictureElement = document.querySelector('.c-picture img');
if (!pictureElement) return null;
const picture = pictureElement.src;
const title = document.querySelector('h1').innerText;
const article = document.querySelector('.c-entry-content').innerText;
const source = "The Verge";
const categories = "Tech";
return {
title,
article,
picture,
source,
categories
}
});
if (!result) continue;
// ... do stuff with result
}
Answering comment question: "Is there a way just to skip anything blank, and return the rest?"
Yes. You just need to check the existence of each element that could be missing before trying to read a property off of it. In this case we can omit the early return since you're always interested in all the results.
async function scrape3() {
// ...
for (let i = 1; i <= 3; i++) {
const result = await page.evaluate(() => {
const img = document.querySelector('.c-picture img');
const h1 = document.querySelector('h1');
const content = document.querySelector('.c-entry-content');
const picture = img ? img.src : '';
const title = h1 ? h1.innerText : '';
const article = content ? content.innerText : '';
const source = "The Verge";
const categories = "Tech";
return {
title,
article,
picture,
source,
categories
}
});
// ...
}
}
Further thoughts
Since I'm still on this question, let me take this one step further, and refactor it a bit with some higher level techniques you might be interested in. Not sure if this is exactly what you are after, but it should give you some ideas about writing more maintainable code.
// Generic reusable helper to return an object property
// if object exists and has property, else a default value
//
// This is a curried function accepting one argument at a
// time and capturing each parameter in a closure.
//
const maybeGetProp = default => key => object =>
(object && object.hasOwnProperty(key)) ? object.key : default
// Pass in empty string as the default value
//
const getPropOrEmptyString = maybeGetProp('')
// Apply the second parameter, the property name, making 2
// slightly different functions which have a default value
// and a property name pre-loaded. Both functions only need
// an object passed in to return either the property if it
// exists or an empty string.
//
const maybeText = getPropOrEmptyString('innerText')
const maybeSrc = getPropOrEmptyString('src')
async function scrape3() {
// ...
// The _ parameter name is acknowledging that we expect a
// an argument passed in but saying we plan to ignore it.
//
const evaluate = _ => page.evaluate(() => {
// Attempt to retrieve the desired elements
//
const img = document.querySelector('.c-picture img');
const h1 = document.querySelector('h1')
const content = document.querySelector('.c-entry-content')
// Return the results, with empty string in
// place of any missing properties.
//
return {
title: maybeText(h1),
article: maybeText(article),
picture: maybeSrc(img),
source: 'The Verge',
categories: 'Tech'
}
}))
// Start with an empty array of length 3
//
const evaluations = Array(3).fill()
// Then map over that array ignoring the undefined
// input and return a promise for a page evaluation
//
.map(evaluate)
// All 3 scrapes are occuring concurrently. We'll
// wait for all of them to finish.
//
const results = await Promise.all(evaluations)
// Now we have an array of results, so we can
// continue using array methods to iterate over them
// or otherwise manipulate or transform them
//
results
.filter(result => result.title && result.picture)
.forEach(result => {
//
// Do something with each result
//
})
}
Try-catch worked for me:
try {
if (await page.$eval('element')!==null) {
const name = await page.$eval('element')
}
}catch(error){
name = ''
}